Skip to content

Commit e12def5

Browse files
authored
feat(docs): Add {toolicon} role variants — icon-only tool refs (#4)
Add 5 icon-only tool reference roles to the Sphinx extension for lightweight safety-tier indicators next to tool code chips: - {tooliconl} / {toolicon} — colored square left of code - {tooliconr} — colored square right of code - {tooliconil} — bare emoji inside code, left of text - {tooliconir} — bare emoji inside code, right of text CSS: flexbox on parent <a> for consistent icon-to-code spacing (gap: 3px), 16px colored squares for outside variants, bare emoji for inline variants. Recipes page switched from {tool} (full badge) to {tooliconl} (icon-only). Demo page updated with all variant showcases.
1 parent b1d0799 commit e12def5

4 files changed

Lines changed: 199 additions & 40 deletions

File tree

docs/_ext/fastmcp_autodoc.py

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -881,10 +881,14 @@ def _resolve_tool_refs(
881881
doctree: nodes.document,
882882
fromdocname: str,
883883
) -> None:
884-
"""Resolve ``{tool}`` and ``{toolref}`` placeholders into links.
884+
"""Resolve ``{tool}``, ``{toolref}``, and ``{toolicon*}`` placeholders.
885885
886-
``{tool}`` renders as ``code`` + safety badge.
886+
``{tool}`` renders as ``code`` + safety badge (text + icon).
887887
``{toolref}`` renders as ``code`` only (no badge).
888+
``{toolicon}``/``{tooliconl}`` — icon-only badge left of code.
889+
``{tooliconr}`` — icon-only badge right of code.
890+
``{tooliconil}`` — icon-only badge inside code, left of text.
891+
``{tooliconir}`` — icon-only badge inside code, right of text.
888892
889893
Runs at ``doctree-resolved`` — after all labels are registered and
890894
standard ``{ref}`` resolution is done.
@@ -896,6 +900,7 @@ def _resolve_tool_refs(
896900
for node in list(doctree.findall(_tool_ref_placeholder)):
897901
target = node.get("reftarget", "")
898902
show_badge = node.get("show_badge", True)
903+
icon_pos = node.get("icon_pos", "")
899904
label_info = domain.labels.get(target)
900905
if label_info is None:
901906
node.replace_self(nodes.literal("", target.replace("-", "_")))
@@ -914,13 +919,44 @@ def _resolve_tool_refs(
914919
newnode["classes"].append("reference")
915920
newnode["classes"].append("internal")
916921

917-
newnode += nodes.literal("", tool_name)
918-
919-
if show_badge:
922+
if icon_pos:
920923
tool_info = tool_data.get(tool_name)
924+
badge = None
921925
if tool_info:
922-
newnode += nodes.Text(" ")
923-
newnode += _safety_badge(tool_info.safety)
926+
badge = _safety_badge(tool_info.safety)
927+
badge["classes"].append("icon-only")
928+
if icon_pos.startswith("inline"):
929+
badge["classes"].append("icon-only-inline")
930+
badge.children.clear()
931+
badge += nodes.Text("")
932+
933+
if icon_pos == "left":
934+
if badge:
935+
newnode += badge
936+
newnode += nodes.literal("", tool_name)
937+
elif icon_pos == "right":
938+
newnode += nodes.literal("", tool_name)
939+
if badge:
940+
newnode += badge
941+
elif icon_pos == "inline-left":
942+
code_node = nodes.literal("", "")
943+
if badge:
944+
code_node += badge
945+
code_node += nodes.Text(tool_name)
946+
newnode += code_node
947+
elif icon_pos == "inline-right":
948+
code_node = nodes.literal("", "")
949+
code_node += nodes.Text(tool_name)
950+
if badge:
951+
code_node += badge
952+
newnode += code_node
953+
else:
954+
newnode += nodes.literal("", tool_name)
955+
if show_badge:
956+
tool_info = tool_data.get(tool_name)
957+
if tool_info:
958+
newnode += nodes.Text(" ")
959+
newnode += _safety_badge(tool_info.safety)
924960

925961
node.replace_self(newnode)
926962

@@ -962,6 +998,39 @@ def _toolref_role(
962998
return [node], []
963999

9641000

1001+
def _make_toolicon_role(
1002+
icon_pos: str,
1003+
) -> t.Callable[..., tuple[list[nodes.Node], list[nodes.system_message]]]:
1004+
"""Create an icon-only tool reference role for a given position."""
1005+
1006+
def role_fn(
1007+
name: str,
1008+
rawtext: str,
1009+
text: str,
1010+
lineno: int,
1011+
inliner: object,
1012+
options: dict[str, object] | None = None,
1013+
content: list[str] | None = None,
1014+
) -> tuple[list[nodes.Node], list[nodes.system_message]]:
1015+
target = text.strip().replace("_", "-")
1016+
node = _tool_ref_placeholder(
1017+
rawtext,
1018+
reftarget=target,
1019+
show_badge=False,
1020+
icon_pos=icon_pos,
1021+
)
1022+
return [node], []
1023+
1024+
return role_fn
1025+
1026+
1027+
_toolicon_role = _make_toolicon_role("left")
1028+
_tooliconl_role = _make_toolicon_role("left")
1029+
_tooliconr_role = _make_toolicon_role("right")
1030+
_tooliconil_role = _make_toolicon_role("inline-left")
1031+
_tooliconir_role = _make_toolicon_role("inline-right")
1032+
1033+
9651034
def _badge_role(
9661035
name: str,
9671036
rawtext: str,
@@ -987,6 +1056,11 @@ def setup(app: Sphinx) -> ExtensionMetadata:
9871056
app.connect("doctree-resolved", _resolve_tool_refs)
9881057
app.add_role("tool", _tool_role)
9891058
app.add_role("toolref", _toolref_role)
1059+
app.add_role("toolicon", _toolicon_role)
1060+
app.add_role("tooliconl", _tooliconl_role)
1061+
app.add_role("tooliconr", _tooliconr_role)
1062+
app.add_role("tooliconil", _tooliconil_role)
1063+
app.add_role("tooliconir", _tooliconir_role)
9901064
app.add_role("badge", _badge_role)
9911065
app.add_directive("fastmcp-tool", FastMCPToolDirective)
9921066
app.add_directive("fastmcp-tool-input", FastMCPToolInputDirective)

docs/_static/css/custom.css

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,75 @@ h4:has(> .sd-badge) {
309309
.sd-badge.sd-bg-warning::before { content: "✏️"; } /* mutating */
310310
.sd-badge.sd-bg-danger::before { content: "💣"; } /* destructive */
311311

312+
/* ── Icon-only badge ({toolicon*} roles) ─────────────────
313+
* Outside variants (l/r): colored square box with emoji.
314+
* Inside variants (il/ir): bare emoji, no box — blends
315+
* into the code chip as a seamless annotation.
316+
* ────────────────────────────────────────────────────────── */
317+
318+
/* Outside icon links: flexbox collapses whitespace nodes */
319+
a.reference:has(> .sd-badge.icon-only) {
320+
display: inline-flex;
321+
align-items: center;
322+
gap: 3px;
323+
}
324+
325+
a.reference:has(> .sd-badge.icon-only) > code {
326+
margin: 0;
327+
}
328+
329+
/* Outside variants: colored square box */
330+
.sd-badge.icon-only {
331+
display: inline-flex !important;
332+
align-items: center;
333+
justify-content: center;
334+
width: 16px;
335+
height: 16px;
336+
padding: 0;
337+
box-sizing: border-box;
338+
border-radius: 3px;
339+
gap: 0;
340+
font-size: 0;
341+
line-height: 1;
342+
min-width: 0;
343+
min-height: 0;
344+
margin: 0;
345+
}
346+
347+
.sd-badge.icon-only::before {
348+
font-size: 10px;
349+
line-height: 1;
350+
font-style: normal;
351+
font-weight: normal;
352+
margin: 0;
353+
display: block;
354+
opacity: 0.9;
355+
}
356+
357+
/* Inline variants (inside <code>): bare emoji, no box */
358+
.sd-badge.icon-only-inline {
359+
background: transparent !important;
360+
border: none !important;
361+
padding: 0;
362+
width: auto;
363+
height: auto;
364+
border-radius: 0;
365+
vertical-align: -0.01em;
366+
margin-right: 0.12em;
367+
margin-left: 0;
368+
}
369+
370+
.sd-badge.icon-only-inline::before {
371+
font-size: 0.78rem;
372+
opacity: 0.85;
373+
}
374+
375+
/* Inline-right: tighter than inline-left */
376+
code.docutils .sd-badge.icon-only-inline:last-child {
377+
margin-left: 0.1em;
378+
margin-right: 0;
379+
}
380+
312381
/* ── Context-aware badge sizing ─────────────────────────── */
313382
h2 .sd-badge,
314383
h3 .sd-badge {

docs/demo.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ Standalone badges via `{badge}`:
2424

2525
{toolref}`capture-pane` · {toolref}`send-keys` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`kill-pane` · {toolref}`create-session` · {toolref}`split-window`
2626

27+
### `{tooliconl}` — icon left, outside code
28+
29+
{tooliconl}`capture-pane` · {tooliconl}`send-keys` · {tooliconl}`search-panes` · {tooliconl}`wait-for-text` · {tooliconl}`kill-pane` · {tooliconl}`create-session` · {tooliconl}`split-window`
30+
31+
### `{tooliconr}` — icon right, outside code
32+
33+
{tooliconr}`capture-pane` · {tooliconr}`send-keys` · {tooliconr}`search-panes` · {tooliconr}`wait-for-text` · {tooliconr}`kill-pane` · {tooliconr}`create-session` · {tooliconr}`split-window`
34+
35+
### `{tooliconil}` — icon inline-left, inside code
36+
37+
{tooliconil}`capture-pane` · {tooliconil}`send-keys` · {tooliconil}`search-panes` · {tooliconil}`wait-for-text` · {tooliconil}`kill-pane` · {tooliconil}`create-session` · {tooliconil}`split-window`
38+
39+
### `{tooliconir}` — icon inline-right, inside code
40+
41+
{tooliconir}`capture-pane` · {tooliconir}`send-keys` · {tooliconir}`search-panes` · {tooliconir}`wait-for-text` · {tooliconir}`kill-pane` · {tooliconir}`create-session` · {tooliconir}`split-window`
42+
2743
### `{ref}` — plain text link
2844

2945
{ref}`capture-pane` · {ref}`send-keys` · {ref}`search-panes` · {ref}`wait-for-text` · {ref}`kill-pane` · {ref}`create-session` · {ref}`split-window`

0 commit comments

Comments
 (0)