diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index c31f29d4c..3b87215b1 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -321,6 +321,7 @@ def add_tool( name: str | None = None, description: str | None = None, annotations: ToolAnnotations | None = None, + skip_names: Sequence[str] = (), ) -> None: """Add a tool to the server. @@ -332,9 +333,15 @@ def add_tool( name: Optional name for the tool (defaults to function name) description: Optional description of what the tool does annotations: Optional ToolAnnotations providing additional tool information + 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, annotations=annotations + fn, + name=name, + description=description, + annotations=annotations, + skip_names=skip_names, ) def tool( @@ -342,6 +349,7 @@ def tool( name: str | None = None, description: str | None = None, annotations: ToolAnnotations | None = None, + skip_names: Sequence[str] = (), ) -> Callable[[AnyFunction], AnyFunction]: """Decorator to register a tool. @@ -353,6 +361,8 @@ def tool( name: Optional name for the tool (defaults to function name) description: Optional description of what the tool does annotations: Optional ToolAnnotations providing additional tool information + skip_names: A list of parameter names to skip. These will not be included in + the model. Example: @server.tool() @@ -378,7 +388,11 @@ async def async_tool(x: int, context: Context) -> str: def decorator(fn: AnyFunction) -> AnyFunction: self.add_tool( - fn, name=name, description=description, annotations=annotations + fn, + name=name, + description=description, + annotations=annotations, + skip_names=skip_names, ) return fn diff --git a/src/mcp/server/fastmcp/tools/base.py b/src/mcp/server/fastmcp/tools/base.py index 21eb1841d..ff8629637 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 @@ -43,6 +43,7 @@ def from_function( description: str | None = None, context_kwarg: str | None = None, annotations: ToolAnnotations | None = None, + skip_names: Sequence[str] = (), ) -> Tool: """Create a Tool from a function.""" from mcp.server.fastmcp.server import Context @@ -64,10 +65,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 cfdaeb350..758f10acf 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 @@ -37,10 +37,15 @@ def add_tool( name: str | None = None, description: str | None = None, annotations: ToolAnnotations | None = None, + skip_names: Sequence[str] = (), ) -> Tool: """Add a tool to the server.""" tool = Tool.from_function( - fn, name=name, description=description, annotations=annotations + fn, + name=name, + description=description, + annotations=annotations, + skip_names=skip_names, ) existing = self._tools.get(tool.name) if existing: diff --git a/src/mcp/server/fastmcp/utilities/func_metadata.py b/src/mcp/server/fastmcp/utilities/func_metadata.py index 374391325..66f7d923a 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 b1828ffe9..64ed0eca6 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