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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions