"""Tool registration helpers. One shape, one place -------------------- Every tool in this project returns a JSON string, and that is a decision rather than a default: :func:`mcp_tool` registers with ``structured_output=False`` so a result travels as a text block and nothing else. The alternative — letting the SDK infer a structured output schema from the ``-> str`` annotation — publishes a ``{"result": "..."}`` envelope and sends the same JSON twice, once as ``structuredContent`` and once as text. Half the clients read only one of the two, and the other half reads both and pays for the duplication twice. So a tool is registered by:: @mcp_tool(server, title="列出论文") def paper_list(...) -> str: \"\"\"Short description — this text is what the model reads.\"\"\" The docstring becomes the tool description verbatim, which makes it the most expensive string in the project: it is sent with every request. Keep it to the one or two lines a model needs to choose the tool, and put the reasoning in the module docstring instead, where it costs nothing per request. """ from __future__ import annotations from collections.abc import Callable from typing import Any, TypeVar from mcp.server.mcpserver import MCPServer F = TypeVar("F", bound=Callable[..., Any]) def mcp_tool( server: MCPServer, *, name: str | None = None, title: str | None = None, ) -> Callable[[F], F]: """Register one tool as unstructured text output.""" def decorator(fn: F) -> F: server.add_tool( fn, name=name or fn.__name__, title=title, structured_output=False, ) return fn return decorator __all__ = ["mcp_tool"]