Skip to content
Merged
88 changes: 81 additions & 7 deletions docs/_ext/fastmcp_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,10 +881,14 @@ def _resolve_tool_refs(
doctree: nodes.document,
fromdocname: str,
) -> None:
"""Resolve ``{tool}`` and ``{toolref}`` placeholders into links.
"""Resolve ``{tool}``, ``{toolref}``, and ``{toolicon*}`` placeholders.

``{tool}`` renders as ``code`` + safety badge.
``{tool}`` renders as ``code`` + safety badge (text + icon).
``{toolref}`` renders as ``code`` only (no badge).
``{toolicon}``/``{tooliconl}`` — icon-only badge left of code.
``{tooliconr}`` — icon-only badge right of code.
``{tooliconil}`` — icon-only badge inside code, left of text.
``{tooliconir}`` — icon-only badge inside code, right of text.

Runs at ``doctree-resolved`` — after all labels are registered and
standard ``{ref}`` resolution is done.
Expand All @@ -896,6 +900,7 @@ def _resolve_tool_refs(
for node in list(doctree.findall(_tool_ref_placeholder)):
target = node.get("reftarget", "")
show_badge = node.get("show_badge", True)
icon_pos = node.get("icon_pos", "")
label_info = domain.labels.get(target)
if label_info is None:
node.replace_self(nodes.literal("", target.replace("-", "_")))
Expand All @@ -914,13 +919,44 @@ def _resolve_tool_refs(
newnode["classes"].append("reference")
newnode["classes"].append("internal")

newnode += nodes.literal("", tool_name)

if show_badge:
if icon_pos:
tool_info = tool_data.get(tool_name)
badge = None
if tool_info:
newnode += nodes.Text(" ")
newnode += _safety_badge(tool_info.safety)
badge = _safety_badge(tool_info.safety)
badge["classes"].append("icon-only")
if icon_pos.startswith("inline"):
badge["classes"].append("icon-only-inline")
badge.children.clear()
badge += nodes.Text("")

if icon_pos == "left":
if badge:
newnode += badge
newnode += nodes.literal("", tool_name)
elif icon_pos == "right":
newnode += nodes.literal("", tool_name)
if badge:
newnode += badge
elif icon_pos == "inline-left":
code_node = nodes.literal("", "")
if badge:
code_node += badge
code_node += nodes.Text(tool_name)
newnode += code_node
elif icon_pos == "inline-right":
code_node = nodes.literal("", "")
code_node += nodes.Text(tool_name)
if badge:
code_node += badge
newnode += code_node
else:
newnode += nodes.literal("", tool_name)
if show_badge:
tool_info = tool_data.get(tool_name)
if tool_info:
newnode += nodes.Text(" ")
newnode += _safety_badge(tool_info.safety)

node.replace_self(newnode)

Expand Down Expand Up @@ -962,6 +998,39 @@ def _toolref_role(
return [node], []


def _make_toolicon_role(
icon_pos: str,
) -> t.Callable[..., tuple[list[nodes.Node], list[nodes.system_message]]]:
"""Create an icon-only tool reference role for a given position."""

def role_fn(
name: str,
rawtext: str,
text: str,
lineno: int,
inliner: object,
options: dict[str, object] | None = None,
content: list[str] | None = None,
) -> tuple[list[nodes.Node], list[nodes.system_message]]:
target = text.strip().replace("_", "-")
node = _tool_ref_placeholder(
rawtext,
reftarget=target,
show_badge=False,
icon_pos=icon_pos,
)
return [node], []

return role_fn


_toolicon_role = _make_toolicon_role("left")
_tooliconl_role = _make_toolicon_role("left")
_tooliconr_role = _make_toolicon_role("right")
_tooliconil_role = _make_toolicon_role("inline-left")
_tooliconir_role = _make_toolicon_role("inline-right")


def _badge_role(
name: str,
rawtext: str,
Expand All @@ -987,6 +1056,11 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.connect("doctree-resolved", _resolve_tool_refs)
app.add_role("tool", _tool_role)
app.add_role("toolref", _toolref_role)
app.add_role("toolicon", _toolicon_role)
app.add_role("tooliconl", _tooliconl_role)
app.add_role("tooliconr", _tooliconr_role)
app.add_role("tooliconil", _tooliconil_role)
app.add_role("tooliconir", _tooliconir_role)
app.add_role("badge", _badge_role)
app.add_directive("fastmcp-tool", FastMCPToolDirective)
app.add_directive("fastmcp-tool-input", FastMCPToolInputDirective)
Expand Down
69 changes: 69 additions & 0 deletions docs/_static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,75 @@ h4:has(> .sd-badge) {
.sd-badge.sd-bg-warning::before { content: "✏️"; } /* mutating */
.sd-badge.sd-bg-danger::before { content: "💣"; } /* destructive */

/* ── Icon-only badge ({toolicon*} roles) ─────────────────
* Outside variants (l/r): colored square box with emoji.
* Inside variants (il/ir): bare emoji, no box — blends
* into the code chip as a seamless annotation.
* ────────────────────────────────────────────────────────── */

/* Outside icon links: flexbox collapses whitespace nodes */
a.reference:has(> .sd-badge.icon-only) {
display: inline-flex;
align-items: center;
gap: 3px;
}

a.reference:has(> .sd-badge.icon-only) > code {
margin: 0;
}

/* Outside variants: colored square box */
.sd-badge.icon-only {
display: inline-flex !important;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
box-sizing: border-box;
border-radius: 3px;
gap: 0;
font-size: 0;
line-height: 1;
min-width: 0;
min-height: 0;
margin: 0;
}

.sd-badge.icon-only::before {
font-size: 10px;
line-height: 1;
font-style: normal;
font-weight: normal;
margin: 0;
display: block;
opacity: 0.9;
}

/* Inline variants (inside <code>): bare emoji, no box */
.sd-badge.icon-only-inline {
background: transparent !important;
border: none !important;
padding: 0;
width: auto;
height: auto;
border-radius: 0;
vertical-align: -0.01em;
margin-right: 0.12em;
margin-left: 0;
}

.sd-badge.icon-only-inline::before {
font-size: 0.78rem;
opacity: 0.85;
}

/* Inline-right: tighter than inline-left */
code.docutils .sd-badge.icon-only-inline:last-child {
margin-left: 0.1em;
margin-right: 0;
}

/* ── Context-aware badge sizing ─────────────────────────── */
h2 .sd-badge,
h3 .sd-badge {
Expand Down
16 changes: 16 additions & 0 deletions docs/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ Standalone badges via `{badge}`:

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

### `{tooliconl}` — icon left, outside code

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

### `{tooliconr}` — icon right, outside code

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

### `{tooliconil}` — icon inline-left, inside code

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

### `{tooliconir}` — icon inline-right, inside code

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

### `{ref}` — plain text link

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