Skip to content

feat(func_metadata): expose skip_names argument #550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand All @@ -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()
Expand All @@ -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
Expand Down
11 changes: 6 additions & 5 deletions src/mcp/server/fastmcp/tools/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
10 changes: 8 additions & 2 deletions src/mcp/server/fastmcp/tools/tool_manager.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/server/fastmcp/utilities/func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
14 changes: 11 additions & 3 deletions tests/server/fastmcp/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading