Skip to content

sphinx-autodoc-fastmcp: ToolCollector.tool() rejects forward-compat kwargs (e.g. meta) #35

@tony

Description

@tony

Problem

sphinx_autodoc_fastmcp._collector.ToolCollector.tool() mocks FastMCP.tool() for docstring extraction at docs-build time. Its current signature accepts only title, annotations, and tags:

def tool(
    self,
    title: str = "",
    annotations: dict[str, bool] | None = None,
    tags: set[str] | None = None,
) -> Callable[..., Callable[..., Any]]:

FastMCP.tool() itself accepts additional keyword arguments — most notably meta, used for per-tool MCP metadata (e.g. the anthropic/alwaysLoad discovery hint introduced in Claude Code v2.1.121).

When a project's tool registration passes any kwarg the mock does not accept, the mock raises TypeError: tool() got an unexpected keyword argument 'meta'. Because ToolCollector runs during Sphinx import, the failure is silent at the docs-build level — the entire enclosing module's tools drop from the docs catalog. The visible symptom is a spike in autodoc warnings (50 → 100+) without an obvious culprit at the call site.

Workaround currently in use downstream

Monkey-patch the mock from docs/conf.py:

from sphinx_autodoc_fastmcp._collector import ToolCollector
import typing as t

_orig_tool_collector_tool = ToolCollector.tool


def _patched_tool_collector_tool(self, **kwargs):
    """Drop ``meta`` from kwargs so the mock signature still binds."""
    kwargs.pop("meta", None)
    return _orig_tool_collector_tool(self, **kwargs)


t.cast(t.Any, ToolCollector).tool = _patched_tool_collector_tool

Works, but is fragile to upstream API changes — every new kwarg the mock learns about needs a downstream coordination, and the cast/setattr dance is line-noise that a reader has to mentally peel away to see the actual fix.

Proposed fix

Add **kwargs: t.Any to the mock signature so it forward-compat swallows anything FastMCP.tool() accepts now or in future:

def tool(
    self,
    title: str = "",
    annotations: dict[str, bool] | None = None,
    tags: set[str] | None = None,
    **kwargs: t.Any,  # NEW
) -> Callable[..., Callable[..., Any]]:

The **kwargs are intentionally ignored — the docs collector only extracts title, annotations, and tags. The change is one line plus a brief docstring note explaining the forward-compat intent. No behavior change for existing callers.

Why now

Downstream projects targeting Claude Code v2.1.121+ are starting to ship meta={"anthropic/alwaysLoad": True} per-tool hints. Each one hits this exact wall on first docs build, has to debug the silent-drop symptom, and ends up writing the same kwargs.pop("meta", None) shim.

Cadence

No rush — this is a quality-of-life upgrade, not a release blocker. Tagging for an evening / weekend slot whenever convenient.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions