diff --git a/fastmcp_slim/fastmcp/server/providers/filesystem_discovery.py b/fastmcp_slim/fastmcp/server/providers/filesystem_discovery.py index fbebe9562..0c1340ef5 100644 --- a/fastmcp_slim/fastmcp/server/providers/filesystem_discovery.py +++ b/fastmcp_slim/fastmcp/server/providers/filesystem_discovery.py @@ -343,7 +343,6 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]: annotations=meta.annotations, meta=meta.meta, task=resolved_task, - exclude_args=meta.exclude_args, timeout=meta.timeout, auth=meta.auth, run_in_thread=meta.run_in_thread, diff --git a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py index ba08f5a0a..ee61b5a14 100644 --- a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py +++ b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py @@ -158,7 +158,6 @@ class ToolDecoratorMixin: annotations=fmeta.annotations, meta=tool_meta, task=resolved_task, - exclude_args=fmeta.exclude_args, timeout=fmeta.timeout, auth=fmeta.auth, run_in_thread=fmeta.run_in_thread, @@ -184,7 +183,6 @@ class ToolDecoratorMixin: tags: set[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, annotations: ToolAnnotations | dict[str, Any] | None = None, - exclude_args: list[str] | None = None, meta: dict[str, Any] | None = None, enabled: bool = True, task: bool | TaskConfig | None = None, @@ -206,7 +204,6 @@ class ToolDecoratorMixin: tags: set[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, annotations: ToolAnnotations | dict[str, Any] | None = None, - exclude_args: list[str] | None = None, meta: dict[str, Any] | None = None, enabled: bool = True, task: bool | TaskConfig | None = None, @@ -215,10 +212,9 @@ class ToolDecoratorMixin: run_in_thread: bool = True, ) -> Callable[[F], F]: ... - # NOTE: This method mirrors fastmcp.tools.tool() but adds registration, - # the `enabled` param, and supports the deprecated `exclude_args` param. - # When deprecated params are removed, this should delegate to the standalone - # decorator to reduce duplication. + # NOTE: This method mirrors fastmcp.tools.tool() but adds registration and + # the `enabled` param. It could delegate to the standalone decorator to + # reduce duplication. def tool( self: LocalProvider, name_or_fn: str | AnyFunction | None = None, @@ -231,7 +227,6 @@ class ToolDecoratorMixin: tags: set[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, annotations: ToolAnnotations | dict[str, Any] | None = None, - exclude_args: list[str] | None = None, meta: dict[str, Any] | None = None, enabled: bool = True, task: bool | TaskConfig | None = None, @@ -261,7 +256,6 @@ class ToolDecoratorMixin: 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 meta: Optional meta information about the tool enabled: Whether the tool is enabled (default True). If False, adds to blocklist. task: Optional task configuration for background execution @@ -327,7 +321,6 @@ class ToolDecoratorMixin: tags=tags, output_schema=output_schema, annotations=annotations, - exclude_args=exclude_args, meta=meta, task=resolved_task, timeout=timeout, @@ -353,7 +346,6 @@ class ToolDecoratorMixin: annotations=annotations, meta=meta, task=task, - exclude_args=exclude_args, timeout=timeout, auth=auth, enabled=enabled, @@ -394,7 +386,6 @@ class ToolDecoratorMixin: tags=tags, output_schema=output_schema, annotations=annotations, - exclude_args=exclude_args, meta=meta, enabled=enabled, task=task, diff --git a/fastmcp_slim/fastmcp/server/server.py b/fastmcp_slim/fastmcp/server/server.py index dc64c44c2..79beeeda5 100644 --- a/fastmcp_slim/fastmcp/server/server.py +++ b/fastmcp_slim/fastmcp/server/server.py @@ -1716,7 +1716,6 @@ class FastMCP( tags: set[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, annotations: ToolAnnotations | dict[str, Any] | None = None, - exclude_args: list[str] | None = None, meta: dict[str, Any] | None = None, app: AppConfig | dict[str, Any] | bool | None = None, task: bool | TaskConfig | None = None, @@ -1738,7 +1737,6 @@ class FastMCP( tags: set[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, annotations: ToolAnnotations | dict[str, Any] | None = None, - exclude_args: list[str] | None = None, meta: dict[str, Any] | None = None, app: AppConfig | dict[str, Any] | bool | None = None, task: bool | TaskConfig | None = None, @@ -1759,7 +1757,6 @@ class FastMCP( tags: set[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, annotations: ToolAnnotations | dict[str, Any] | None = None, - exclude_args: list[str] | None = None, meta: dict[str, Any] | None = None, app: AppConfig | dict[str, Any] | bool | None = None, task: bool | TaskConfig | None = None, @@ -1791,8 +1788,6 @@ class FastMCP( 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. - Deprecated: Use `Depends()` for dependency injection instead. meta: Optional meta information about the tool Examples: @@ -1838,7 +1833,6 @@ class FastMCP( tags=tags, output_schema=output_schema, annotations=annotations, - exclude_args=exclude_args, meta=meta, task=task if task is not None else self._support_tasks_by_default, timeout=timeout, diff --git a/fastmcp_slim/fastmcp/tools/base.py b/fastmcp_slim/fastmcp/tools/base.py index 2620816cb..de725e8eb 100644 --- a/fastmcp_slim/fastmcp/tools/base.py +++ b/fastmcp_slim/fastmcp/tools/base.py @@ -256,7 +256,6 @@ class Tool(FastMCPComponent): icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, - exclude_args: list[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, @@ -276,7 +275,6 @@ class Tool(FastMCPComponent): icons=icons, tags=tags, annotations=annotations, - exclude_args=exclude_args, output_schema=output_schema, meta=meta, task=task, diff --git a/fastmcp_slim/fastmcp/tools/function_parsing.py b/fastmcp_slim/fastmcp/tools/function_parsing.py index 054ed9815..9f653a336 100644 --- a/fastmcp_slim/fastmcp/tools/function_parsing.py +++ b/fastmcp_slim/fastmcp/tools/function_parsing.py @@ -21,7 +21,6 @@ from fastmcp.utilities.types import ( Audio, File, Image, - create_function_without_params, get_cached_typeadapter, is_class_member_of_type, replace_type, @@ -178,7 +177,6 @@ class ParsedFunction: def from_function( cls, fn: Callable[..., Any], - exclude_args: list[str] | None = None, validate: bool = True, wrap_non_object_output_schema: bool = True, ) -> ParsedFunction: @@ -193,19 +191,6 @@ class ParsedFunction: "Functions with **kwargs are not supported as tools" ) - # Reject exclude_args that don't exist in the function or don't have a default value - if exclude_args: - for arg_name in exclude_args: - if arg_name not in sig.parameters: - raise ValueError( - f"Parameter '{arg_name}' in exclude_args does not exist in function." - ) - param = sig.parameters[arg_name] - if param.default == inspect.Parameter.empty: - raise ValueError( - f"Parameter '{arg_name}' in exclude_args must have a default value." - ) - # collect name and description before we potentially modify the function fn_name = getattr(fn, "__name__", None) or fn.__class__.__name__ outer_docstring = parse_docstring(fn) @@ -241,19 +226,10 @@ class ParsedFunction: # Handle injected parameters (Context, Docket dependencies) wrapper_fn = without_injected_parameters(fn) - # Also handle exclude_args with non-serializable types (issue #2431) - # This must happen before Pydantic tries to serialize the parameters - if exclude_args: - wrapper_fn = create_function_without_params(wrapper_fn, list(exclude_args)) - input_type_adapter = get_cached_typeadapter(wrapper_fn) input_schema = input_type_adapter.json_schema() - # Compress and handle exclude_args - prune_params = list(exclude_args) if exclude_args else None - input_schema = compress_schema( - input_schema, prune_params=prune_params, prune_titles=True - ) + input_schema = compress_schema(input_schema, prune_titles=True) # Inject parameter descriptions from the docstring into the schema. # Explicit annotations (Field(description=...), Annotated[x, "..."]) diff --git a/fastmcp_slim/fastmcp/tools/function_tool.py b/fastmcp_slim/fastmcp/tools/function_tool.py index 3473032d8..bfe8b9c0a 100644 --- a/fastmcp_slim/fastmcp/tools/function_tool.py +++ b/fastmcp_slim/fastmcp/tools/function_tool.py @@ -169,7 +169,6 @@ class ToolMeta: meta: dict[str, Any] | None = None app: Any = None task: bool | TaskConfig | None = None - exclude_args: list[str] | None = None timeout: float | None = None auth: AuthCheck | list[AuthCheck] | None = None enabled: bool = True @@ -234,7 +233,6 @@ class FunctionTool(Tool): icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, - exclude_args: list[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, @@ -271,7 +269,6 @@ class FunctionTool(Tool): ] ) or output_schema is not NotSet - or exclude_args is not None ) if metadata is not None and individual_params_provided: @@ -298,22 +295,12 @@ class FunctionTool(Tool): annotations=annotations, meta=meta, task=task, - exclude_args=exclude_args, timeout=timeout, auth=auth, run_in_thread=True if run_in_thread is None else run_in_thread, ) - if metadata.exclude_args and fastmcp.settings.deprecation_warnings: - warnings.warn( - "The `exclude_args` parameter is deprecated as of FastMCP 2.14. " - "Use dependency injection with `Depends()` instead for better lifecycle management. " - "See https://gofastmcp.com/servers/dependency-injection#using-depends for examples.", - FastMCPDeprecationWarning, - stacklevel=2, - ) - - parsed_fn = ParsedFunction.from_function(fn, exclude_args=metadata.exclude_args) + parsed_fn = ParsedFunction.from_function(fn) func_name = metadata.name or parsed_fn.name if func_name == "": @@ -591,7 +578,6 @@ def tool( annotations: ToolAnnotations | dict[str, Any] | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, - exclude_args: list[str] | None = None, timeout: float | None = None, auth: AuthCheck | list[AuthCheck] | None = None, run_in_thread: bool = True, @@ -610,7 +596,6 @@ def tool( annotations: ToolAnnotations | dict[str, Any] | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, - exclude_args: list[str] | None = None, timeout: float | None = None, auth: AuthCheck | list[AuthCheck] | None = None, run_in_thread: bool = True, @@ -630,7 +615,6 @@ def tool( annotations: ToolAnnotations | dict[str, Any] | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, - exclude_args: list[str] | None = None, timeout: float | None = None, auth: AuthCheck | list[AuthCheck] | None = None, run_in_thread: bool = True, @@ -672,7 +656,6 @@ def tool( annotations=annotations, meta=meta, task=resolve_task_config(task), - exclude_args=exclude_args, timeout=timeout, auth=auth, run_in_thread=run_in_thread, @@ -691,7 +674,6 @@ def tool( annotations=annotations, meta=meta, task=task, - exclude_args=exclude_args, timeout=timeout, auth=auth, run_in_thread=run_in_thread, diff --git a/tests/deprecated/test_exclude_args.py b/tests/deprecated/test_exclude_args.py deleted file mode 100644 index f01d5f6b7..000000000 --- a/tests/deprecated/test_exclude_args.py +++ /dev/null @@ -1,122 +0,0 @@ -from typing import Any - -import pytest -from mcp.server.session import ServerSession - -from fastmcp import Client, FastMCP -from fastmcp.tools.base import Tool - - -async def test_tool_exclude_args(): - """Test that tool args are excluded.""" - mcp = FastMCP("Test Server") - - @mcp.tool(exclude_args=["state"]) - def echo(message: str, state: dict[str, Any] | None = None) -> str: - """Echo back the message provided.""" - if state: - # State was read - pass - return message - - tools = await mcp.list_tools() - assert len(tools) == 1 - assert "state" not in tools[0].parameters["properties"] - - -async def test_tool_exclude_args_without_default_value_raises_error(): - """Test that excluding args without default values raises ValueError""" - mcp = FastMCP("Test Server") - - with pytest.raises(ValueError): - - @mcp.tool(exclude_args=["state"]) - def echo(message: str, state: dict[str, Any] | None) -> str: - """Echo back the message provided.""" - if state: - # State was read - pass - return message - - -async def test_add_tool_method_exclude_args(): - """Test that tool exclude_args work with the add_tool method.""" - mcp = FastMCP("Test Server") - - def create_item( - name: str, value: int, state: dict[str, Any] | None = None - ) -> dict[str, Any]: - """Create a new item.""" - if state: - # State was read - pass - return {"name": name, "value": value} - - tool = Tool.from_function( - create_item, - name="create_item", - exclude_args=["state"], - ) - mcp.add_tool(tool) - - # Check tool via public API - tools = await mcp.list_tools() - assert len(tools) == 1 - assert "state" not in tools[0].parameters["properties"] - - -async def test_tool_functionality_with_exclude_args(): - """Test that tool functionality is preserved when using exclude_args.""" - mcp = FastMCP("Test Server") - - def create_item( - name: str, value: int, state: dict[str, Any] | None = None - ) -> dict[str, Any]: - """Create a new item.""" - if state: - # state was read - pass - return {"name": name, "value": value} - - tool = Tool.from_function( - create_item, - name="create_item", - exclude_args=["state"], - ) - mcp.add_tool(tool) - - # Use the tool to verify functionality is preserved - async with Client(mcp) as client: - result = await client.call_tool( - "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"]