"""Provides a base mixin class and decorators for easy registration of class methods with FastMCP.""" import inspect import warnings from collections.abc import Callable from typing import TYPE_CHECKING, Any import fastmcp from fastmcp.exceptions import FastMCPDeprecationWarning from fastmcp.prompts.base import Prompt from fastmcp.resources.base import Resource from fastmcp.tools.base import Tool from fastmcp.utilities.types import get_fn_name if TYPE_CHECKING: from fastmcp.server import FastMCP _MCP_REGISTRATION_TOOL_ATTR = "_mcp_tool_registration" _MCP_REGISTRATION_RESOURCE_ATTR = "_mcp_resource_registration" _MCP_REGISTRATION_PROMPT_ATTR = "_mcp_prompt_registration" _DEFAULT_SEPARATOR_TOOL = "_" _DEFAULT_SEPARATOR_RESOURCE = "+" _DEFAULT_SEPARATOR_PROMPT = "_" # Sentinel key stored in registration dicts for the mixin-only `enabled` flag. # Prefixed with an underscore to avoid collisions with any from_function parameter. _MIXIN_ENABLED_KEY = "_mixin_enabled" # Valid keyword arguments for each from_function, derived once at import time # directly from the live signatures. They stay in sync automatically whenever # the underlying signatures gain or lose parameters — no manual updates needed. _TOOL_VALID_KWARGS: frozenset[str] = frozenset( p for p in inspect.signature(Tool.from_function).parameters if p != "fn" ) _RESOURCE_VALID_KWARGS: frozenset[str] = frozenset( p for p in inspect.signature(Resource.from_function).parameters if p not in ("fn", "uri") ) _PROMPT_VALID_KWARGS: frozenset[str] = frozenset( p for p in inspect.signature(Prompt.from_function).parameters if p != "fn" ) def mcp_tool( name: str | None = None, *, enabled: bool | None = None, **kwargs: Any, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to mark a method as an MCP tool for later registration. Accepts all parameters supported by ``Tool.from_function``. Any new parameters added to ``Tool.from_function`` are automatically forwarded without requiring changes here. Args: name: Tool name. Defaults to the decorated method name. enabled: If ``False``, the tool is skipped during registration. **kwargs: Additional keyword arguments forwarded verbatim to ``Tool.from_function`` (e.g. ``description``, ``tags``, ``annotations``, ``auth``, ``timeout``, ``version``, …). Raises: TypeError: If an unrecognised keyword argument is supplied. The error is raised immediately at decoration time rather than later. """ unknown = set(kwargs) - _TOOL_VALID_KWARGS if unknown: raise TypeError( f"mcp_tool() got unexpected keyword argument(s): {sorted(unknown)!r}. " f"Valid keyword arguments are: {sorted(_TOOL_VALID_KWARGS)}" ) if "serializer" in kwargs and fastmcp.settings.deprecation_warnings: warnings.warn( "The `serializer` parameter is deprecated. " "Return ToolResult from your tools for full control over serialization. " "See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.", FastMCPDeprecationWarning, stacklevel=2, ) def decorator(func: Callable[..., Any]) -> Callable[..., Any]: call_args: dict[str, Any] = {"name": name or get_fn_name(func), **kwargs} if enabled is not None: call_args[_MIXIN_ENABLED_KEY] = enabled setattr(func, _MCP_REGISTRATION_TOOL_ATTR, call_args) return func return decorator def mcp_resource( uri: str, *, name: str | None = None, enabled: bool | None = None, **kwargs: Any, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to mark a method as an MCP resource for later registration. Accepts all parameters supported by ``Resource.from_function``. Any new parameters added to ``Resource.from_function`` are automatically forwarded without requiring changes here. Args: uri: Resource URI (required). name: Resource name. Defaults to the decorated method name. enabled: If ``False``, the resource is skipped during registration. **kwargs: Additional keyword arguments forwarded verbatim to ``Resource.from_function`` (e.g. ``description``, ``tags``, ``mime_type``, ``auth``, ``version``, …). Raises: TypeError: If an unrecognised keyword argument is supplied. The error is raised immediately at decoration time rather than later. """ unknown = set(kwargs) - _RESOURCE_VALID_KWARGS if unknown: raise TypeError( f"mcp_resource() got unexpected keyword argument(s): {sorted(unknown)!r}. " f"Valid keyword arguments are: {sorted(_RESOURCE_VALID_KWARGS)}" ) def decorator(func: Callable[..., Any]) -> Callable[..., Any]: call_args: dict[str, Any] = { "uri": uri, "name": name or get_fn_name(func), **kwargs, } if enabled is not None: call_args[_MIXIN_ENABLED_KEY] = enabled setattr(func, _MCP_REGISTRATION_RESOURCE_ATTR, call_args) return func return decorator def mcp_prompt( name: str | None = None, *, enabled: bool | None = None, **kwargs: Any, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to mark a method as an MCP prompt for later registration. Accepts all parameters supported by ``Prompt.from_function``. Any new parameters added to ``Prompt.from_function`` are automatically forwarded without requiring changes here. Args: name: Prompt name. Defaults to the decorated method name. enabled: If ``False``, the prompt is skipped during registration. **kwargs: Additional keyword arguments forwarded verbatim to ``Prompt.from_function`` (e.g. ``description``, ``tags``, ``auth``, ``version``, …). Raises: TypeError: If an unrecognised keyword argument is supplied. The error is raised immediately at decoration time rather than later. """ unknown = set(kwargs) - _PROMPT_VALID_KWARGS if unknown: raise TypeError( f"mcp_prompt() got unexpected keyword argument(s): {sorted(unknown)!r}. " f"Valid keyword arguments are: {sorted(_PROMPT_VALID_KWARGS)}" ) def decorator(func: Callable[..., Any]) -> Callable[..., Any]: call_args: dict[str, Any] = {"name": name or get_fn_name(func), **kwargs} if enabled is not None: call_args[_MIXIN_ENABLED_KEY] = enabled setattr(func, _MCP_REGISTRATION_PROMPT_ATTR, call_args) return func return decorator class MCPMixin: """Base mixin class for objects that can register tools, resources, and prompts with a FastMCP server instance using decorators. This mixin provides methods like ``register_all``, ``register_tools``, etc., which iterate over the methods of the inheriting class, find methods decorated with ``@mcp_tool``, ``@mcp_resource``, or ``@mcp_prompt``, and register them with the provided FastMCP server instance. """ def _get_methods_to_register(self, registration_type: str): """Retrieves all methods marked for a specific registration type.""" return [ ( getattr(self, method_name), getattr(getattr(self, method_name), registration_type).copy(), ) for method_name in dir(self) if callable(getattr(self, method_name)) and hasattr(getattr(self, method_name), registration_type) ] def register_tools( self, mcp_server: "FastMCP", prefix: str | None = None, separator: str = _DEFAULT_SEPARATOR_TOOL, ) -> None: """Registers all methods marked with @mcp_tool with the FastMCP server. Args: mcp_server: The FastMCP server instance to register tools with. prefix: Optional prefix to prepend to tool names. If provided, the final name will be ``f"{prefix}{separator}{original_name}"``. separator: The separator string used between prefix and original name. Defaults to ``'_'``. """ for method, registration_info in self._get_methods_to_register( _MCP_REGISTRATION_TOOL_ATTR ): if prefix: registration_info["name"] = ( f"{prefix}{separator}{registration_info['name']}" ) enabled = registration_info.pop(_MIXIN_ENABLED_KEY, True) if enabled is False: continue tool = Tool.from_function(fn=method, **registration_info) mcp_server.add_tool(tool) def register_resources( self, mcp_server: "FastMCP", prefix: str | None = None, separator: str = _DEFAULT_SEPARATOR_RESOURCE, ) -> None: """Registers all methods marked with @mcp_resource with the FastMCP server. Args: mcp_server: The FastMCP server instance to register resources with. prefix: Optional prefix to prepend to resource names and URIs. If provided, the final name will be ``f"{prefix}{separator}{original_name}"`` and the final URI will be ``f"{prefix}{separator}{original_uri}"``. separator: The separator string used between prefix and original name/URI. Defaults to ``'+'``. """ for method, registration_info in self._get_methods_to_register( _MCP_REGISTRATION_RESOURCE_ATTR ): if prefix: registration_info["name"] = ( f"{prefix}{separator}{registration_info['name']}" ) registration_info["uri"] = ( f"{prefix}{separator}{registration_info['uri']}" ) enabled = registration_info.pop(_MIXIN_ENABLED_KEY, True) if enabled is False: continue resource = Resource.from_function(fn=method, **registration_info) mcp_server.add_resource(resource) def register_prompts( self, mcp_server: "FastMCP", prefix: str | None = None, separator: str = _DEFAULT_SEPARATOR_PROMPT, ) -> None: """Registers all methods marked with @mcp_prompt with the FastMCP server. Args: mcp_server: The FastMCP server instance to register prompts with. prefix: Optional prefix to prepend to prompt names. If provided, the final name will be ``f"{prefix}{separator}{original_name}"``. separator: The separator string used between prefix and original name. Defaults to ``'_'``. """ for method, registration_info in self._get_methods_to_register( _MCP_REGISTRATION_PROMPT_ATTR ): if prefix: registration_info["name"] = ( f"{prefix}{separator}{registration_info['name']}" ) enabled = registration_info.pop(_MIXIN_ENABLED_KEY, True) if enabled is False: continue prompt = Prompt.from_function(fn=method, **registration_info) mcp_server.add_prompt(prompt) def register_all( self, mcp_server: "FastMCP", prefix: str | None = None, tool_separator: str = _DEFAULT_SEPARATOR_TOOL, resource_separator: str = _DEFAULT_SEPARATOR_RESOURCE, prompt_separator: str = _DEFAULT_SEPARATOR_PROMPT, ) -> None: """Registers all marked tools, resources, and prompts with the server. This method calls ``register_tools``, ``register_resources``, and ``register_prompts`` internally, passing the provided prefix and separators. Args: mcp_server: The FastMCP server instance to register with. prefix: Optional prefix applied to all registered items. tool_separator: Separator for tool names (defaults to ``'_'``). resource_separator: Separator for resource names/URIs (defaults to ``'+'``). prompt_separator: Separator for prompt names (defaults to ``'_'``). """ self.register_tools(mcp_server, prefix=prefix, separator=tool_separator) self.register_resources(mcp_server, prefix=prefix, separator=resource_separator) self.register_prompts(mcp_server, prefix=prefix, separator=prompt_separator)