Skip to content

Commit 47a28f1

Browse files
committed
feat(docs[_ext]): Add safety badges to Inspect/Act/Destroy headings
why: Section headings grouping tools by tier should visually indicate the safety level, matching the per-tool badges. what: - Add SECTION_BADGE_MAP constant mapping heading text to safety tiers - Add _add_section_badges handler at doctree-resolved that appends colored badge nodes to matching titles - Section IDs remain untouched (frozen before transform runs) - Add 4 tests: map values, badge appended, ID preserved, non-match ignored
1 parent c28a5df commit 47a28f1

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

docs/_ext/fastmcp_autodoc.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
"env_tools": "options",
5151
}
5252

53+
SECTION_BADGE_MAP: dict[str, str] = {
54+
"Inspect": "readonly",
55+
"Act": "mutating",
56+
"Destroy": "destructive",
57+
}
58+
5359
TAG_READONLY = "readonly"
5460
TAG_MUTATING = "mutating"
5561
TAG_DESTRUCTIVE = "destructive"
@@ -739,6 +745,26 @@ def _register_tool_labels(app: Sphinx, doctree: nodes.document) -> None:
739745
domain.labels[section_id] = (docname, section_id, tool_name)
740746

741747

748+
def _add_section_badges(
749+
app: Sphinx,
750+
doctree: nodes.document,
751+
fromdocname: str,
752+
) -> None:
753+
"""Append safety badges to Inspect/Act/Destroy section headings.
754+
755+
Runs at ``doctree-resolved`` — section IDs are already frozen, so
756+
appending nodes to the title doesn't affect anchors or cross-refs.
757+
"""
758+
for section in doctree.findall(nodes.section):
759+
if not section.children or not isinstance(section[0], nodes.title):
760+
continue
761+
title_text = section[0].astext().strip()
762+
safety = SECTION_BADGE_MAP.get(title_text)
763+
if safety is not None:
764+
section[0] += nodes.Text(" ")
765+
section[0] += _safety_badge(safety)
766+
767+
742768
class _tool_ref_placeholder(nodes.General, nodes.Inline, nodes.Element):
743769
"""Placeholder node for ``{tool}`` role, resolved at doctree-resolved."""
744770

@@ -809,6 +835,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
809835
"""Register the fastmcp_autodoc extension."""
810836
app.connect("builder-inited", _collect_tools)
811837
app.connect("doctree-read", _register_tool_labels)
838+
app.connect("doctree-resolved", _add_section_badges)
812839
app.connect("doctree-resolved", _resolve_tool_refs)
813840
app.add_role("tool", _tool_role)
814841
app.add_directive("fastmcp-tool", FastMCPToolDirective)

tests/docs/_ext/test_fastmcp_autodoc.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,84 @@ def test_safety_badge_classes() -> None:
442442
assert "sd-bg-danger" in badge["classes"]
443443

444444

445+
# ---------------------------------------------------------------------------
446+
# SECTION_BADGE_MAP + _add_section_badges
447+
# ---------------------------------------------------------------------------
448+
449+
450+
def test_section_badge_map_headings() -> None:
451+
"""SECTION_BADGE_MAP maps group headings to safety tiers."""
452+
m = fastmcp_autodoc.SECTION_BADGE_MAP
453+
assert m["Inspect"] == "readonly"
454+
assert m["Act"] == "mutating"
455+
assert m["Destroy"] == "destructive"
456+
457+
458+
def test_add_section_badges_appends_badge_to_title() -> None:
459+
"""_add_section_badges appends a safety badge to matching titles."""
460+
from docutils import nodes
461+
from docutils.frontend import OptionParser
462+
from docutils.utils import new_document
463+
464+
settings = OptionParser(components=()).get_default_values()
465+
doc = new_document("test", settings)
466+
467+
section = nodes.section(ids=["inspect"])
468+
title = nodes.title("", "Inspect")
469+
section += title
470+
doc += section
471+
472+
# Simulate the handler — it expects (app, doctree, fromdocname)
473+
# but only uses doctree, so pass None for the others.
474+
fastmcp_autodoc._add_section_badges(None, doc, "")
475+
476+
# Title should now have 3 children: Text("Inspect"), Text(" "), inline(badge)
477+
assert len(title.children) == 3
478+
badge = title.children[2]
479+
assert isinstance(badge, nodes.inline)
480+
assert "sd-bg-success" in badge["classes"]
481+
assert badge.astext() == "readonly"
482+
483+
484+
def test_add_section_badges_preserves_section_id() -> None:
485+
"""_add_section_badges does not change the section ID."""
486+
from docutils import nodes
487+
from docutils.frontend import OptionParser
488+
from docutils.utils import new_document
489+
490+
settings = OptionParser(components=()).get_default_values()
491+
doc = new_document("test", settings)
492+
493+
section = nodes.section(ids=["inspect"])
494+
section += nodes.title("", "Inspect")
495+
doc += section
496+
497+
fastmcp_autodoc._add_section_badges(None, doc, "")
498+
499+
assert section["ids"] == ["inspect"]
500+
501+
502+
def test_add_section_badges_ignores_non_matching() -> None:
503+
"""_add_section_badges leaves non-matching headings untouched."""
504+
from docutils import nodes
505+
from docutils.frontend import OptionParser
506+
from docutils.utils import new_document
507+
508+
settings = OptionParser(components=()).get_default_values()
509+
doc = new_document("test", settings)
510+
511+
section = nodes.section(ids=["overview"])
512+
title = nodes.title("", "Overview")
513+
section += title
514+
doc += section
515+
516+
fastmcp_autodoc._add_section_badges(None, doc, "")
517+
518+
# Title should still have only the original text child
519+
assert len(title.children) == 1
520+
assert title.astext() == "Overview"
521+
522+
445523
# ---------------------------------------------------------------------------
446524
# Integration: collect real tools
447525
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)