mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 20:44:17 +02:00
Fix exclude_args with non-serializable types
Fixes issue #2431 where exclude_args fails when excluded parameters have non-serializable types (e.g., ServerSession). The fix excludes parameters from function annotations before Pydantic tries to serialize them. Also adds deprecation notice that exclude_args will be deprecated in FastMCP 2.14 in favor of dependency injection.
This commit is contained in:
parent
be90d6eb60
commit
52100b08ff
5 changed files with 105 additions and 3 deletions
|
|
@ -354,7 +354,9 @@ Field provides several validation and documentation features:
|
|||
|
||||
You can exclude certain arguments from the tool schema shown to the LLM. This is useful for arguments that are injected at runtime (such as `state`, `user_id`, or credentials) and should not be exposed to the LLM or client. Only arguments with default values can be excluded; attempting to exclude a required argument will raise an error.
|
||||
|
||||
Example:
|
||||
**Note:** `exclude_args` will be deprecated in FastMCP 2.14 in favor of dependency injection with `Depends()` for better lifecycle management and more explicit dependency handling. `exclude_args` will continue to work until then.
|
||||
|
||||
Example with `exclude_args`:
|
||||
|
||||
```python
|
||||
@mcp.tool(
|
||||
|
|
|
|||
|
|
@ -1434,7 +1434,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
tags: Optional set of tags for categorizing the tool
|
||||
output_schema: Optional JSON schema for the tool's output
|
||||
annotations: Optional annotations about the tool's behavior
|
||||
exclude_args: Optional list of argument names to exclude from the tool schema
|
||||
exclude_args: Optional list of argument names to exclude from the tool schema.
|
||||
Note: `exclude_args` will be deprecated in FastMCP 2.14 in favor of dependency
|
||||
injection with `Depends()` for better lifecycle management.
|
||||
meta: Optional meta information about the tool
|
||||
enabled: Optional boolean to enable or disable the tool
|
||||
|
||||
|
|
@ -1485,6 +1487,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
tool_name = name # Use keyword name if provided, otherwise None
|
||||
|
||||
# Register the tool immediately and return the tool object
|
||||
# Note: Deprecation warning for exclude_args is handled in Tool.from_function
|
||||
tool = Tool.from_function(
|
||||
fn,
|
||||
name=tool_name,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ from fastmcp.utilities.types import (
|
|||
Image,
|
||||
NotSet,
|
||||
NotSetT,
|
||||
create_function_without_params,
|
||||
find_kwarg_by_type,
|
||||
get_cached_typeadapter,
|
||||
replace_type,
|
||||
|
|
@ -271,6 +272,16 @@ class FunctionTool(Tool):
|
|||
enabled: bool | None = None,
|
||||
) -> FunctionTool:
|
||||
"""Create a Tool from a function."""
|
||||
if exclude_args and fastmcp.settings.deprecation_warnings:
|
||||
warnings.warn(
|
||||
"The `exclude_args` parameter will be deprecated in FastMCP 2.14. "
|
||||
"We recommend using dependency injection with `Depends()` instead, which provides "
|
||||
"better lifecycle management and is more explicit. "
|
||||
"`exclude_args` will continue to work until then. "
|
||||
"See https://gofastmcp.com/docs/servers/tools for examples.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
parsed_fn = ParsedFunction.from_function(fn, exclude_args=exclude_args)
|
||||
|
||||
|
|
@ -442,7 +453,14 @@ class ParsedFunction:
|
|||
if exclude_args:
|
||||
prune_params.extend(exclude_args)
|
||||
|
||||
input_type_adapter = get_cached_typeadapter(fn)
|
||||
# Create a function without excluded parameters in annotations
|
||||
# This prevents Pydantic from trying to serialize non-serializable types
|
||||
# before we can exclude them in compress_schema
|
||||
fn_for_typeadapter = fn
|
||||
if prune_params:
|
||||
fn_for_typeadapter = create_function_without_params(fn, prune_params)
|
||||
|
||||
input_type_adapter = get_cached_typeadapter(fn_for_typeadapter)
|
||||
input_schema = input_type_adapter.json_schema()
|
||||
input_schema = compress_schema(
|
||||
input_schema, prune_params=prune_params, prune_titles=True
|
||||
|
|
|
|||
|
|
@ -175,6 +175,55 @@ def find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def create_function_without_params(
|
||||
fn: Callable[..., Any], exclude_params: list[str]
|
||||
) -> Callable[..., Any]:
|
||||
"""
|
||||
Create a new function with the same code but without the specified parameters in annotations.
|
||||
|
||||
This is used to exclude parameters from type adapter processing when they can't be serialized.
|
||||
The excluded parameters are removed from the function's __annotations__ dictionary.
|
||||
"""
|
||||
import types
|
||||
|
||||
if inspect.ismethod(fn):
|
||||
actual_func = fn.__func__
|
||||
code = actual_func.__code__ # ty: ignore[unresolved-attribute]
|
||||
globals_dict = actual_func.__globals__ # ty: ignore[unresolved-attribute]
|
||||
name = actual_func.__name__ # ty: ignore[unresolved-attribute]
|
||||
defaults = actual_func.__defaults__ # ty: ignore[unresolved-attribute]
|
||||
closure = actual_func.__closure__ # ty: ignore[unresolved-attribute]
|
||||
else:
|
||||
code = fn.__code__ # ty: ignore[unresolved-attribute]
|
||||
globals_dict = fn.__globals__ # ty: ignore[unresolved-attribute]
|
||||
name = fn.__name__ # ty: ignore[unresolved-attribute]
|
||||
defaults = fn.__defaults__ # ty: ignore[unresolved-attribute]
|
||||
closure = fn.__closure__ # ty: ignore[unresolved-attribute]
|
||||
|
||||
# Create a copy of annotations without the excluded parameters
|
||||
original_annotations = getattr(fn, "__annotations__", {})
|
||||
new_annotations = {
|
||||
k: v for k, v in original_annotations.items() if k not in exclude_params
|
||||
}
|
||||
|
||||
new_func = types.FunctionType(
|
||||
code,
|
||||
globals_dict,
|
||||
name,
|
||||
defaults,
|
||||
closure,
|
||||
)
|
||||
new_func.__dict__.update(fn.__dict__)
|
||||
new_func.__module__ = fn.__module__
|
||||
new_func.__qualname__ = getattr(fn, "__qualname__", fn.__name__) # ty: ignore[unresolved-attribute]
|
||||
new_func.__annotations__ = new_annotations
|
||||
|
||||
if inspect.ismethod(fn):
|
||||
return types.MethodType(new_func, fn.__self__)
|
||||
else:
|
||||
return new_func
|
||||
|
||||
|
||||
class Image:
|
||||
"""Helper class for returning images from tools."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from mcp.server.session import ServerSession
|
||||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.tools.tool import Tool
|
||||
|
|
@ -92,3 +93,32 @@ async def test_tool_functionality_with_exclude_args():
|
|||
"create_item", {"name": "test_item", "value": 42}
|
||||
)
|
||||
assert result.data == {"name": "test_item", "value": 42}
|
||||
|
||||
|
||||
async def test_exclude_args_with_non_serializable_type():
|
||||
"""Test that exclude_args works even when the excluded parameter type can't be serialized.
|
||||
|
||||
This test ensures that exclude_args works correctly when the excluded parameter
|
||||
has a type that Pydantic cannot serialize (like ServerSession). The bug was that
|
||||
get_cached_typeadapter would try to serialize all parameters before compress_schema
|
||||
could exclude them, causing a PydanticSchemaGenerationError.
|
||||
"""
|
||||
|
||||
def my_tool(message: str, session: ServerSession | None = None) -> str:
|
||||
"""A tool that takes a non-serializable Session parameter."""
|
||||
return message
|
||||
|
||||
# This should not raise an error even though ServerSession can't be serialized
|
||||
tool = Tool.from_function(
|
||||
my_tool,
|
||||
name="my_tool",
|
||||
exclude_args=["session"],
|
||||
)
|
||||
|
||||
# Verify the tool was created successfully
|
||||
assert tool is not None
|
||||
assert tool.name == "my_tool"
|
||||
|
||||
# Verify the session parameter is excluded from the schema
|
||||
assert "session" not in tool.parameters["properties"]
|
||||
assert "message" in tool.parameters["properties"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue