Skip to content

fastmcp(fix[ids]): avoid duplicate IDs on same-slug headings#49

Merged
tony merged 5 commits into
mainfrom
duplicate-id-fix
Jun 6, 2026
Merged

fastmcp(fix[ids]): avoid duplicate IDs on same-slug headings#49
tony merged 5 commits into
mainfrom
duplicate-id-fix

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented Jun 6, 2026

Summary

  • Fix: a page heading whose slug matches a tool's bare cross-reference alias no longer produces docutils Duplicate ID diagnostics or an ambiguous HTML fragment (sphinx-autodoc-fastmcp: avoid duplicate IDs when tool alias matches page heading slug #48)
  • Decouple: tool cards keep a single canonical physical anchor (fastmcp-tool-<slug>); the bare slug survives purely as a Sphinx label alias pointing at the canonical anchor
  • Resolve: {tool}, {toolref}, {toolicon*}, and bare {ref} targets keep working and now resolve to the canonical fragment; fastmcp-tool-summary table links target it too
  • Test: a heading-collision integration scenario locks in the contract — no duplicate-ID diagnostics, sole heading ownership of the bare anchor, canonical-fragment link resolution

Before/After

Delete buffer
=============

.. fastmcp-tool:: buffer_tools.delete_buffer

Before: the build emitted CRITICAL: Duplicate ID: "delete-buffer". [docutils], the rendered page carried two delete-buffer anchors, and tool links resolved to the ambiguous bare fragment.

After: the heading owns #delete-buffer exclusively; the card is reachable at #fastmcp-tool-delete-buffer; label-based references ({ref}, tool roles) resolve there.

Design decisions

  • Alias as pure label, not conditional physical ID: dropping the bare id only on collision would make card anchors unstable — adding a heading to a page would silently move the tool's anchor. The canonical anchor is deterministic; the bare alias always points at it.
  • Incremental rebuilds: accepted aliases ride the pickled doctree as a node attribute, so the doctree-read hook restores alias labels from cache without re-running the directive.
  • Trade-off: raw-URL fragments #<slug> no longer land on the card. Label-based references are unaffected; the bare fragment was ambiguous in exactly the colliding case this fixes.

Test plan

  • test_heading_collision_emits_no_duplicate_id_diagnostic — same-slug heading + tool card builds without duplicate-ID diagnostics
  • test_heading_collision_anchor_counts — heading owns the sole bare anchor; {toolref} and fastmcp-tool-summary links target #fastmcp-tool-<slug>
  • test_heading_collision_card_renders — card classes and badges render under the collision
  • Full suite, lint, and types green: uv run pytest, uv run ruff check ., uv run mypy
  • just build-docs — shipped docs build cleanly; tool-role links resolve to canonical anchors

tony added 2 commits June 6, 2026 15:27
why: A page heading whose slug matches a tool's bare compatibility
alias makes the tool card claim the same physical ID the heading
already owns; docutils reports a duplicate ID and the HTML fragment
target is ambiguous (#48).

what:
- Add heading-collision scenario: "Delete buffer" heading +
  fastmcp-tool delete_buffer on one page
- Add strict-xfail tests: no duplicate-ID diagnostic; bare anchor
  owned solely by the heading; toolref and tool-summary links target
  the canonical fastmcp-tool-<slug> fragment
- Add stable assertions that the card classes/badges keep rendering
why: A tool card carrying both fastmcp-tool-<slug> and the bare
<slug> in section["ids"] collides with any same-slug page heading,
producing docutils duplicate-ID diagnostics and ambiguous HTML
fragments (#48). The bare slug only needs to survive as a Sphinx
label for historical {ref}/tool-role targets, not as a second
physical anchor.

what:
- Tool card sections keep a single canonical physical ID
- Bare-slug aliases register as std-domain labels pointing at the
  canonical ID; collision guard unchanged
- register_tool_labels restores alias labels from the
  fastmcp_alias_labels node attribute on incremental rebuilds
- fastmcp-tool-summary links target the canonical fragment
- Remove strict-xfail markers from the reproduction tests; update
  the toolref href expectation to the canonical anchor
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jun 6, 2026

Codecov Report

❌ Patch coverage is 97.50000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.90%. Comparing base (707dc0c) to head (6551238).

Files with missing lines Patch % Lines
...-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #49      +/-   ##
==========================================
+ Coverage   91.88%   91.90%   +0.01%     
==========================================
  Files         233      233              
  Lines       18191    18223      +32     
==========================================
+ Hits        16714    16747      +33     
+ Misses       1477     1476       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony
Copy link
Copy Markdown
Member Author

tony commented Jun 6, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

tony added 3 commits June 6, 2026 18:10
why: Users of published releases hit docutils duplicate-ID warnings
when a page heading's slug matched a tool's bare cross-reference
alias; the fix relocates the card anchor, which readers should learn
about at upgrade time.

what:
- Add Fixes entry under 0.0.1a27 (unreleased): tool cards expose a
  single canonical fastmcp-tool-<slug> anchor; bare {ref} and
  tool-role links keep resolving and target the canonical anchor
- Illustrate the heading-vs-card landing with an ASCII page-anatomy
  diagram (ASCII only: IBM Plex Mono lacks rounded box-drawing
  glyphs, so Unicode boxes drift via font fallback)
- Cite the PR
why: Issue #48's suggested coverage names both tool-role and bare
{ref} resolution; the collision scenario only exercised :toolref:.
Both read the same std.labels alias entry, but {ref} resolves through
Sphinx's StandardDomain while :toolref: goes through the package's
resolve_tool_refs transform, so each path deserves its own needle.

what:
- Add a bare :ref:`delete-buffer` link to the collision index
- Split the canonical-anchor needle into a toolref case (<code>
  body) and a bare-ref case (<span> body), one match each
why: The bare slug never becomes a physical HTML id anymore; the
FastMCPToolDirective docstring's "bare-slug alias" predates that
and reads as if a second anchor were still involved.

what:
- Say "bare-slug label alias" in the :no-index: contract description
@tony tony force-pushed the duplicate-id-fix branch from 7edea77 to 6551238 Compare June 6, 2026 23:10
@tony tony merged commit 4b577fc into main Jun 6, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants