Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a0ca35e
fastmcp(feat[safety]): Let a project name its own tiers
tony Aug 29, 2026
d8685d8
docs(fastmcp): Show how to name your own tiers
tony Aug 29, 2026
544eac3
fastmcp(feat[toolsets]): Rename safety to toolset
tony Aug 29, 2026
0afe29c
fastmcp(fix[badges]): Colour toolset badges by tone
tony Aug 29, 2026
a1fb465
fastmcp(fix[badges]): Omit the toolset badge for an untagged tool
tony Aug 30, 2026
370c116
fastmcp(refactor[summary]): Drop the vestigial toolset group seeding
tony Aug 30, 2026
be79f14
fastmcp(fix[summary]): Warn when a tool falls outside the vocabulary
tony Aug 30, 2026
93cdb0d
fastmcp(fix[summary]): Anchor summary sections on the toolset tag
tony Aug 30, 2026
96d845e
fastmcp(feat[toolsets]): Warn on a malformed toolset entry
tony Aug 30, 2026
cdf4b7b
fastmcp(refactor[css]): Drop the unused fixed-vocabulary constants
tony Aug 30, 2026
ba6fc91
fastmcp(fix[config]): Correct the toolset config descriptions
tony Aug 30, 2026
37a1061
fastmcp(docs[toolsets]): Finish the safety-to-toolset rename
tony Aug 30, 2026
5e851b7
fastmcp(test[toolsets]): Pin the vocabulary across repeated builds
tony Aug 30, 2026
d670000
docs(fastmcp): Give the demo toolsets their tones
tony Aug 30, 2026
7da0905
fastmcp(refactor[css]): Resolve badge tones through variables
tony Aug 30, 2026
45dbfc5
fastmcp(feat[axes]): Classify tools on axes a project declares
tony Aug 30, 2026
c330dbe
fastmcp(test[axes]): Cover three sources classifying one tool
tony Aug 30, 2026
65e9c32
docs(fastmcp): Show how to declare axes
tony Aug 30, 2026
f7eeaa1
docs(fastmcp): Move the demo onto a declared axis
tony Aug 30, 2026
74d67f4
ci(docs): Invalidate every path after a full sync
tony Aug 30, 2026
7efa4d6
docs(CHANGES) Record the axis taxonomy and the badge fixes
tony Aug 30, 2026
0de1662
badges(fix[css]): Keep an icon-only link inline
tony Aug 30, 2026
44975a2
docs(CHANGES) Note the icon-only link alignment fix
tony Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
run: |
aws cloudfront create-invalidation \
--distribution-id "${{ secrets.GP_SPHINX_DOCS_DISTRIBUTION }}" \
--paths "/index.html" "/objects.inv" "/searchindex.js"
--paths "/*"

- name: Purge cache on Cloudflare
if: steps.changes.outputs.publishable == 'true'
Expand Down
108 changes: 108 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,114 @@ $ uv add gp-sphinx --prerelease allow

<!-- To maintainers and contributors: Please add notes for the forthcoming version below -->

### Breaking changes

#### Tools are classified on axes, not on one vocabulary

`fastmcp_toolsets` is replaced by `fastmcp_axes`. An axis is one
independent way of classifying a tool; a tool takes at most one term per
axis and renders one badge per axis, in declaration order.

```python
# Before
fastmcp_toolsets = ("destructive", "mutating", "readonly")

# After
fastmcp_axes = (
{"name": "risk", "terms": ("destructive", "mutating", "readonly")},
)
```

`ToolInfo.safety` is now `ToolInfo.axes`, a mapping of axis name to term,
and `ToolInfo.meta` carries the tool's `meta`. `fastmcp_section_badge_map`
values accept `term` or `axis:term`. `fastmcp-tool-summary` anchors its
sections on `#fastmcp-<axis>-<term>` rather than on the rendered heading,
so links into the old anchors need updating. (#77)

#### No vocabulary is assumed

Tags previously resolved against a fixed `destructive` / `mutating` /
`readonly` set, ending in a fallback to `readonly`. A project using
different tags had every tool badged read-only, including the ones that
run commands.

Nothing is assumed now: a project declares its own axes, and a tool
matching no term on an axis renders no badge for it. Until
`fastmcp_axes` is set, tools carry no badges — which is the signal that
the setting is missing. (#77)

### What's new

#### One tool, several axes

Tags usually carry more than one idea. A read-only lifecycle tool used to
be reported as its risk or as its topic, never both, because a tool held
a single term.

Declare an axis for each and both badges render:

```python
fastmcp_axes = (
{"name": "risk", "terms": ("mutating", "readonly")},
{"name": "topic", "terms": ("lifecycle", "metrics")},
)
```

See {ref}`sphinx-autodoc-fastmcp-how-to` for the full shape. (#77)

#### Axes read MCP's own metadata

An axis names where its term comes from. `tags` matches declared terms
against the tool's tags. `annotations` reads `ToolAnnotations`, following
the MCP spec so `destructiveHint` describes a tool only once
`readOnlyHint` is false, and an unset hint says nothing rather than
defaulting. `meta:<key>` reads the mapping MCP passes through to clients.

A tool that sets no hints takes no term, so declaring an `annotations`
axis costs nothing until the tools carry them. (#77)

#### Badge presentation is per term

A term carries its own `label`, `tooltip`, `icon`, `tone`, `style`,
`fill` and `classes`. Badges render `gp-sphinx-fastmcp__axis-<axis>` and
`gp-sphinx-fastmcp__<axis>-<term>`, so a project can style one axis or
one term directly. (#77)

#### Tones extend without touching the extension

Tones resolve through three CSS layers: `:root` names the palette, a
`--tone-<name>` class maps one entry onto the badge slot, and one rule
consumes the slot. Restyling a shipped tone means redefining three
variables; adding a tone the extension does not ship means writing one
class and naming it from `conf.py`. (#77)

### Fixes

- A tool matching no declared term rendered an empty badge with a bare
`Toolset: ` tooltip from `{tool}` and `{toolref}`; it now renders as
plain text. (#77)
- `fastmcp-tool-summary` dropped tools it could not place, and rendered
nothing at all when no vocabulary was declared, both without a
diagnostic. It now warns and names the tools it left out. (#77)
- A `fastmcp_axes` entry with no name, or a term with no name, aborted
the build with a bare `KeyError`. Both now warn and are skipped. (#77)
- A tone the stylesheet has no rule for rendered an uncoloured badge with
nothing to point at the typo; it now warns and falls back. (#77)
- The `fastmcp_toolsets` config description claimed an empty value kept
the old three tags, contradicting itself two sentences later. (#77)
- An icon-only tool link rendered its code chip 3.8px taller than the
plain code beside it, hanging below the line. The link no longer
builds a flex context, so the chip keeps its inline metrics. (#77)

### Documentation

#### Classifying tools has a how-to

The rules a project follows — one axis from tags, two axes when the tags
carry two ideas, an axis from annotations or `meta`, and the two CSS
entry points for tones — are written down. See
{ref}`sphinx-autodoc-fastmcp-how-to`. (#77)

## gp-sphinx 0.1.0a37 (2026-07-26)

gp-sphinx 0.1.0a37 gives every class-level name — a field, a
Expand Down
6 changes: 3 additions & 3 deletions docs/_ext/fastmcp_demo_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def list_sessions(server: str, limit: int = 20) -> list[str]:


t.cast(t.Any, list_sessions).__fastmcp__ = types.SimpleNamespace(
name="list_sessions", title="List Sessions", tags={"readonly"}, annotations=None
name="list_sessions", title="List Sessions", tags={"inspect"}, annotations=None
)


Expand Down Expand Up @@ -78,7 +78,7 @@ def create_session(


t.cast(t.Any, create_session).__fastmcp__ = types.SimpleNamespace(
name="create_session", title="Create Session", tags={"mutating"}, annotations=None
name="create_session", title="Create Session", tags={"execute"}, annotations=None
)


Expand Down Expand Up @@ -108,6 +108,6 @@ def delete_session(name: str, force: bool = False) -> bool:
t.cast(t.Any, delete_session).__fastmcp__ = types.SimpleNamespace(
name="delete_session",
title="Delete Session",
tags={"destructive"},
tags={"teardown"},
annotations=None,
)
25 changes: 25 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,31 @@
fastmcp_area_map={
"fastmcp_demo_tools": "packages/sphinx-autodoc-fastmcp/examples",
},
fastmcp_axes=(
{
"name": "capability",
"terms": (
{
"term": "teardown",
"tone": "red",
"tooltip": "Removes objects; not reversible.",
"icon": "\N{BOMB}",
},
{
"term": "execute",
"tone": "amber",
"tooltip": "Starts or drives a process.",
"icon": "\N{PENCIL}\N{VARIATION SELECTOR-16}",
},
{
"term": "inspect",
"tone": "green",
"tooltip": "Reads state without changing it.",
"icon": "\N{LEFT-POINTING MAGNIFYING GLASS}",
},
),
},
),
fastmcp_collector_mode="introspect",
api_layout_enabled=True,
api_collapsed_threshold=10,
Expand Down
121 changes: 120 additions & 1 deletion docs/packages/sphinx-autodoc-fastmcp/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# How to

Use this extension when a FastMCP server should document its tools,
resources, prompts, generated schemas, safety metadata, and cross-reference
resources, prompts, generated schemas, toolset metadata, and cross-reference
badges from live registration data.

## Downstream `conf.py`
Expand All @@ -25,6 +25,125 @@ fastmcp_collector_mode = "register"
fastmcp_server_module = "my_project.server:mcp"
```

## Classify your tools

A tool is classified on one or more **axes**. Each axis is independent, so a
tool takes at most one term per axis and renders one badge per axis. That is
the difference from a single vocabulary: risk and topic can disagree without
one having to win.

This extension ships no project vocabulary. It documents projects whose tags it
does not choose, so a default would badge one project's tools with another's
words.

### One axis from your tags

```python
fastmcp_axes = (
{
"name": "capability",
"terms": (
{
"term": "teardown",
"tooltip": "Deletes objects; not reversible.",
"icon": "\N{BOMB}",
"tone": "red",
},
{"term": "execute", "tooltip": "Starts or drives a process."},
"manage",
"inspect",
),
},
)
```

`terms` order is precedence: a tool carrying several of them takes the first
listed. A term is a bare tag name or a mapping with `label`, `tooltip`, `icon`,
`tone`, `style`, `fill` and `classes`.

A tool matching no term renders **no** badge for that axis. Falling back to a
term nobody assigned is the one answer a badge must never give.

### Two axes at once

Tags often carry two ideas. Declare an axis for each and both badges render:

```python
fastmcp_axes = (
{"name": "risk", "terms": ("mutating", "readonly")},
{"name": "topic", "terms": ("lifecycle", "metrics", "thresholds")},
)
```

A read-only `lifecycle` tool now shows `readonly` *and* `lifecycle`, where a
single vocabulary would have to drop one.

### Axes from MCP's own metadata

`source` says where a term comes from. It defaults to `tags`:

| `source` | Reads |
| --- | --- |
| `tags` | the tool's `tags`, matched against the declared terms |
| `annotations` | `ToolAnnotations`, yielding `readonly`, `mutating` or `destructive` |
| `meta:<key>` | `meta[<key>]`, whatever the tool put there |

```python
fastmcp_axes = (
{"name": "risk", "source": "annotations"},
{"name": "since", "source": "meta:since"},
)
```

The `annotations` source follows the MCP spec: `destructiveHint` describes a
tool only once `readOnlyHint` is false, and an unset hint says nothing rather
than defaulting. A tool that sets no hints takes no term, so declaring this
axis costs nothing until your tools carry annotations.

### Colours, and adding your own

`tone` names a colour: `green`, `blue`, `amber`, `red` or `slate`, defaulting
to `slate`. Tones are three CSS layers, so you can enter at whichever you need.

Restyle a shipped tone by redefining its palette variables:

```css
:root {
--gp-sphinx-fastmcp-tone-red-bg: #7f1d1d;
--gp-sphinx-fastmcp-tone-red-border: #991b1b;
--gp-sphinx-fastmcp-tone-red-text: #fef2f2;
}
```

Add a tone the extension does not ship by defining its class, then naming it:

```css
.gp-sphinx-fastmcp__toolset--tone-teal {
--gp-sphinx-fastmcp-badge-bg: #0f766e;
--gp-sphinx-fastmcp-badge-border: #14b8a6;
--gp-sphinx-fastmcp-badge-text: #f0fdfa;
}
```

```python
{"term": "audit", "tone": "teal"}
```

Every badge also carries `gp-sphinx-fastmcp__axis-<axis>` and
`gp-sphinx-fastmcp__<axis>-<term>`, so you can style one axis or one term
directly without going through tones at all.

### Summary tables

`{fastmcp-tool-summary}` groups by one axis, defaulting to the first declared.
Name another to group by it instead:

````myst
```{eval-rst}
.. fastmcp-tool-summary:: topic
```
````

`sphinx_autodoc_fastmcp` automatically registers `sphinx_ux_badges`,
`sphinx_ux_autodoc_layout`, and `sphinx_autodoc_typehints_gp` via
{py:meth}`~sphinx.application.Sphinx.setup_extension`. You do not need to add
Expand Down
7 changes: 4 additions & 3 deletions docs/packages/sphinx-autodoc-fastmcp/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Render one tool's parameter table:
```
````

Render a summary table grouped by safety tier:
Render a summary table grouped by toolset (see the how-to guide for
declaring `fastmcp_toolsets`):

````myst
```{eval-rst}
Expand All @@ -35,8 +36,8 @@ Use {tool}`list_sessions` for a linked badge, or {toolref}`delete_session`
for a plain inline reference.
````

Prompts and resources have the same affordance (without a safety badge, which
only tools carry). `{resource}` resolves a fixed resource or a resource
Prompts and resources have the same affordance (without a toolset badge,
which only tools carry). `{resource}` resolves a fixed resource or a resource
template by name; `{prompt}` resolves a prompt:

````myst
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from sphinx.application import Sphinx

from sphinx_autodoc_fastmcp._badges import use_axes
from sphinx_autodoc_fastmcp._collector import (
collect_prompts_and_resources,
collect_tools,
Expand All @@ -28,6 +29,7 @@
FastMCPToolInputDirective,
FastMCPToolSummaryDirective,
)
from sphinx_autodoc_fastmcp._models import coerce_axes
from sphinx_autodoc_fastmcp._roles import (
_prompt_role,
_promptref_role,
Expand Down Expand Up @@ -129,8 +131,8 @@ def setup(app: Sphinx) -> dict[str, t.Any]:
"env",
description=(
'Mapping of docstring section heading (e.g. ``"Inspect"``) '
"to the safety badge it should render with (e.g. "
'``"readonly"``, ``"mutating"``, ``"destructive"``). '
'to the badge it renders with, either ``"term"`` or '
'``"axis:term"`` naming a term declared in ``fastmcp_axes``. '
"Drives the inline section pills next to grouped tool lists."
),
)
Expand All @@ -144,6 +146,21 @@ def setup(app: Sphinx) -> dict[str, t.Any]:
"section headings."
),
)
app.add_config_value(
"fastmcp_axes",
(),
"env",
description=(
"Independent ways of classifying a tool, each rendering its own "
'badge. Every entry is a mapping with ``"name"``, an optional '
'``"source"`` (``"tags"``, ``"annotations"`` or ``"meta:<key>"``) '
'and ``"terms"``. A term is a tag name or a mapping with '
'``"term"`` and optional ``"label"`` / ``"tooltip"`` / ``"icon"`` '
'/ ``"tone"`` / ``"style"`` / ``"fill"`` / ``"classes"``. Empty '
"declares no axes. A tool matching no term on an axis renders no "
"badge for it rather than being reported as the lowest term."
),
)
app.add_config_value(
"fastmcp_collector_mode",
"register",
Expand Down Expand Up @@ -173,6 +190,10 @@ def _add_static_path(app: Sphinx) -> None:
if _static_dir not in app.config.html_static_path:
app.config.html_static_path.append(_static_dir)

def _install_axes(app: Sphinx) -> None:
use_axes(coerce_axes(app.config.fastmcp_axes))

app.connect("builder-inited", _install_axes)
app.connect("builder-inited", _add_static_path)
app.add_css_file("css/sphinx_autodoc_fastmcp.css")

Expand Down
Loading