diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index f3bb2586..4350dff6 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -245,6 +245,7 @@ def add_tool( fn: AnyFunction, name: str | None = None, description: str | None = None, + skip_names: Sequence[str] = (), ) -> None: """Add a tool to the server. @@ -255,11 +256,21 @@ def add_tool( fn: The function to register as a tool name: Optional name for the tool (defaults to function name) description: Optional description of what the tool does + skip_names: A list of parameter names to skip. These will not be included in + the model. """ - self._tool_manager.add_tool(fn, name=name, description=description) + self._tool_manager.add_tool( + fn, + name=name, + description=description, + skip_names=skip_names, + ) def tool( - self, name: str | None = None, description: str | None = None + self, + name: str | None = None, + description: str | None = None, + skip_names: Sequence[str] = (), ) -> Callable[[AnyFunction], AnyFunction]: """Decorator to register a tool. @@ -270,6 +281,8 @@ def tool( Args: name: Optional name for the tool (defaults to function name) description: Optional description of what the tool does + skip_names: A list of parameter names to skip. These will not be included in + the model. Example: @server.tool() @@ -294,7 +307,7 @@ async def async_tool(x: int, context: Context) -> str: ) def decorator(fn: AnyFunction) -> AnyFunction: - self.add_tool(fn, name=name, description=description) + self.add_tool(fn, name=name, description=description, skip_names=skip_names) return fn return decorator diff --git a/src/mcp/server/fastmcp/tools/base.py b/src/mcp/server/fastmcp/tools/base.py index 92a216f5..6e3a9cdf 100644 --- a/src/mcp/server/fastmcp/tools/base.py +++ b/src/mcp/server/fastmcp/tools/base.py @@ -1,7 +1,7 @@ from __future__ import annotations as _annotations import inspect -from collections.abc import Callable +from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, Any, get_origin from pydantic import BaseModel, Field @@ -38,6 +38,7 @@ def from_function( name: str | None = None, description: str | None = None, context_kwarg: str | None = None, + skip_names: Sequence[str] = (), ) -> Tool: """Create a Tool from a function.""" from mcp.server.fastmcp import Context @@ -59,10 +60,10 @@ def from_function( context_kwarg = param_name break - func_arg_metadata = func_metadata( - fn, - skip_names=[context_kwarg] if context_kwarg is not None else [], - ) + if context_kwarg is not None: + skip_names = (context_kwarg, *skip_names) + + func_arg_metadata = func_metadata(fn, skip_names=skip_names) parameters = func_arg_metadata.arg_model.model_json_schema() return cls( diff --git a/src/mcp/server/fastmcp/tools/tool_manager.py b/src/mcp/server/fastmcp/tools/tool_manager.py index 4d6ac268..893051dc 100644 --- a/src/mcp/server/fastmcp/tools/tool_manager.py +++ b/src/mcp/server/fastmcp/tools/tool_manager.py @@ -1,6 +1,6 @@ from __future__ import annotations as _annotations -from collections.abc import Callable +from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, Any from mcp.server.fastmcp.exceptions import ToolError @@ -35,9 +35,15 @@ def add_tool( fn: Callable[..., Any], name: str | None = None, description: str | None = None, + skip_names: Sequence[str] = (), ) -> Tool: """Add a tool to the server.""" - tool = Tool.from_function(fn, name=name, description=description) + tool = Tool.from_function( + fn, + name=name, + description=description, + skip_names=skip_names, + ) existing = self._tools.get(tool.name) if existing: if self.warn_on_duplicate_tools: diff --git a/src/mcp/server/fastmcp/utilities/func_metadata.py b/src/mcp/server/fastmcp/utilities/func_metadata.py index 37439132..66f7d923 100644 --- a/src/mcp/server/fastmcp/utilities/func_metadata.py +++ b/src/mcp/server/fastmcp/utilities/func_metadata.py @@ -130,12 +130,12 @@ def func_metadata( dynamic_pydantic_model_params: dict[str, Any] = {} globalns = getattr(func, "__globals__", {}) for param in params.values(): + if param.name in skip_names: + continue if param.name.startswith("_"): raise InvalidSignature( f"Parameter {param.name} of {func.__name__} cannot start with '_'" ) - if param.name in skip_names: - continue annotation = param.annotation # `x: None` / `x: None = None` diff --git a/tests/server/fastmcp/test_func_metadata.py b/tests/server/fastmcp/test_func_metadata.py index b1828ffe..64ed0eca 100644 --- a/tests/server/fastmcp/test_func_metadata.py +++ b/tests/server/fastmcp/test_func_metadata.py @@ -180,18 +180,26 @@ def test_skip_names(): """Test that skipped parameters are not included in the model""" def func_with_many_params( - keep_this: int, skip_this: str, also_keep: float, also_skip: bool + keep_this: int, + skip_this: str, + also_keep: float, + also_skip: bool, + _skip_this_too: int = 0, ): - return keep_this, skip_this, also_keep, also_skip + return keep_this, skip_this, also_keep, also_skip, _skip_this_too # Skip some parameters - meta = func_metadata(func_with_many_params, skip_names=["skip_this", "also_skip"]) + meta = func_metadata( + func_with_many_params, + skip_names=["skip_this", "also_skip", "_skip_this_too"], + ) # Check model fields assert "keep_this" in meta.arg_model.model_fields assert "also_keep" in meta.arg_model.model_fields assert "skip_this" not in meta.arg_model.model_fields assert "also_skip" not in meta.arg_model.model_fields + assert "_skip_this_too" not in meta.arg_model.model_fields # Validate that we can call with only non-skipped parameters model: BaseModel = meta.arg_model.model_validate({"keep_this": 1, "also_keep": 2.5}) # type: ignore