Merge pull request #4453 from PrefectHQ/remove/deprecated-params

Remove 3.x deprecated parameters and object-mode decorators
This commit is contained in:
Jeremiah Lowin 2026-07-07 07:59:53 -04:00 committed by GitHub
commit 7832f884c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 77 additions and 970 deletions

View file

@ -75,7 +75,7 @@ BREAKING CHANGES (will crash at import or runtime):
9. ENV VAR: FASTMCP_SHOW_CLI_BANNER renamed to FASTMCP_SHOW_SERVER_BANNER.
10. DECORATORS: @mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name, .description, or other component attributes on the decorated result will crash with AttributeError.
Fix: set FASTMCP_DECORATOR_MODE=object for v2 compat (itself deprecated).
Fix: access component objects via the server (e.g. await mcp.get_tool("name")) instead of the decorated function. The FASTMCP_DECORATOR_MODE=object escape hatch that existed in v3 was removed in FastMCP 4.0.
11. OAUTH STORAGE: Default OAuth client storage changed from DiskStore to FileTreeStore due to pickle deserialization vulnerability in diskcache (CVE-2025-69872). Clients using default storage will re-register automatically on first connection. If using DiskStore explicitly, switch to FileTreeStore (with key/collection sanitization strategies) or add pip install 'py-key-value-aio[disk]'.
@ -317,7 +317,7 @@ def greet(name: str) -> str:
greet("World") # Works! Returns "Hello, World!"
```
If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), set `FASTMCP_DECORATOR_MODE=object` for v2 compatibility. This escape hatch is itself deprecated and will be removed in a future release.
If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), the v2-compatible object-returning behavior was available in v3 via `FASTMCP_DECORATOR_MODE=object`. That escape hatch was removed in FastMCP 4.0 — decorators always return the original function now.
**Background tasks require optional dependency**

View file

@ -97,5 +97,4 @@ When setting Docket values in a `.env` file, use a **double** underscore: `FASTM
| `FASTMCP_HOME` | `Path` | Platform default | Data directory for FastMCP. Defaults to the platform-specific user data directory. |
| `FASTMCP_ENV_FILE` | `str` | `.env` | Path to the `.env` file to load settings from. Must be set as an environment variable (see above). |
| `FASTMCP_SERVER_DEPENDENCIES` | `list[str]` | `[]` | Additional dependencies to install in the server environment. |
| `FASTMCP_DECORATOR_MODE` | `Literal["function", "object"]` | `function` | Controls what `@tool`, `@resource`, and `@prompt` decorators return. `function` returns the original function (default); `object` returns component objects (deprecated, will be removed). |
| `FASTMCP_TEST_MODE` | `bool` | `false` | Enable test mode. |

View file

@ -1,12 +1,9 @@
"""Provides a base mixin class and decorators for easy registration of class methods with FastMCP."""
import inspect
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import fastmcp
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.prompts.base import Prompt
from fastmcp.resources.base import Resource
from fastmcp.tools.base import Tool
@ -73,15 +70,6 @@ def mcp_tool(
f"Valid keyword arguments are: {sorted(_TOOL_VALID_KWARGS)}"
)
if "serializer" in kwargs and fastmcp.settings.deprecation_warnings:
warnings.warn(
"The `serializer` parameter is deprecated. "
"Return ToolResult from your tools for full control over serialization. "
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
FastMCPDeprecationWarning,
stacklevel=2,
)
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
call_args: dict[str, Any] = {"name": name or get_fn_name(func), **kwargs}
if enabled is not None:

View file

@ -2,7 +2,6 @@
from __future__ import annotations as _annotations
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload
@ -29,7 +28,6 @@ from mcp_types import PromptArgument as SDKPromptArgument
from pydantic import Field
from pydantic.json_schema import SkipJsonSchema
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
@ -414,27 +412,3 @@ __all__ = [
"PromptArgument",
"PromptResult",
]
def __getattr__(name: str) -> Any:
"""Deprecated re-exports for backwards compatibility."""
deprecated_exports = {
"FunctionPrompt": "FunctionPrompt",
"prompt": "prompt",
}
if name in deprecated_exports:
import fastmcp
if fastmcp.settings.deprecation_warnings:
warnings.warn(
f"Importing {name} from fastmcp.prompts.prompt is deprecated. "
f"Import from fastmcp.prompts.function_prompt instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
from fastmcp.prompts import function_prompt
return getattr(function_prompt, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View file

@ -5,7 +5,6 @@ from __future__ import annotations
import functools
import inspect
import json
import warnings
from collections.abc import Callable
from dataclasses import dataclass, field
from types import MethodType
@ -24,9 +23,7 @@ import pydantic_core
from mcp_types import Icon
from pydantic.json_schema import SkipJsonSchema
import fastmcp
from fastmcp.decorators import resolve_task_config
from fastmcp.exceptions import FastMCPDeprecationWarning, FastMCPError, PromptError
from fastmcp.exceptions import FastMCPError, PromptError
from fastmcp.prompts.base import Prompt, PromptArgument, PromptResult
from fastmcp.utilities.async_utils import (
call_sync_fn_in_threadpool,
@ -457,23 +454,6 @@ def prompt(
"See https://gofastmcp.com/servers/prompts#using-with-methods"
)
def create_prompt(
fn: Callable[..., Any], prompt_name: str | None
) -> FunctionPrompt:
# Create metadata first, then pass it
prompt_meta = PromptMeta(
name=prompt_name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
meta=meta,
task=resolve_task_config(task),
auth=auth,
)
return FunctionPrompt.from_function(fn, metadata=prompt_meta)
def attach_metadata(fn: F, prompt_name: str | None) -> F:
metadata = PromptMeta(
name=prompt_name,
@ -491,14 +471,6 @@ def prompt(
return fn
def decorator(fn: F, prompt_name: str | None) -> F:
if fastmcp.settings.decorator_mode == "object":
warnings.warn(
"decorator_mode='object' is deprecated and will be removed in a future version. "
"Decorators now return the original function with metadata attached.",
FastMCPDeprecationWarning,
stacklevel=4,
)
return create_prompt(fn, prompt_name) # type: ignore[return-value] # ty:ignore[invalid-return-type]
return attach_metadata(fn, prompt_name)
if inspect.isroutine(name_or_fn):

View file

@ -30,7 +30,6 @@ from pydantic import (
from pydantic.json_schema import SkipJsonSchema
from typing_extensions import Self
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.tasks import TaskConfig, TaskMeta
@ -469,29 +468,3 @@ __all__ = [
"ResourceContent",
"ResourceResult",
]
def __getattr__(name: str) -> Any:
"""Deprecated re-exports for backwards compatibility."""
deprecated_exports = {
"FunctionResource": "FunctionResource",
"resource": "resource",
}
if name in deprecated_exports:
import warnings
import fastmcp
if fastmcp.settings.deprecation_warnings:
warnings.warn(
f"Importing {name} from fastmcp.resources.resource is deprecated. "
f"Import from fastmcp.resources.function_resource instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
from fastmcp.resources import function_resource
return getattr(function_resource, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View file

@ -4,7 +4,6 @@ from __future__ import annotations
import functools
import inspect
import warnings
from collections.abc import Callable
from dataclasses import dataclass, field
from types import MethodType
@ -22,9 +21,6 @@ from mcp_types import Annotations, Icon
from pydantic import AnyUrl
from pydantic.json_schema import SkipJsonSchema
import fastmcp
from fastmcp.decorators import resolve_task_config
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.resources.base import Resource, ResourceResult
from fastmcp.utilities.async_utils import (
call_sync_fn_in_threadpool,
@ -37,7 +33,6 @@ from fastmcp.utilities.tasks import TaskConfig
if TYPE_CHECKING:
from docket import Docket
from fastmcp.resources.template import ResourceTemplate
F = TypeVar("F", bound=Callable[..., Any])
@ -275,51 +270,6 @@ def resource(
"Use @resource('uri') instead of @resource"
)
def create_resource(fn: Callable[..., Any]) -> FunctionResource | ResourceTemplate:
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.dependencies import without_injected_parameters
resolved = resolve_task_config(task)
has_uri_params = "{" in uri and "}" in uri
wrapper_fn = without_injected_parameters(fn)
has_func_params = bool(inspect.signature(wrapper_fn).parameters)
# Create metadata first
resource_meta = ResourceMeta(
uri=uri,
name=name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
mime_type=mime_type,
annotations=annotations,
meta=meta,
task=resolved,
auth=auth,
)
if has_uri_params or has_func_params:
# ResourceTemplate doesn't have metadata support yet, so pass individual params
return ResourceTemplate.from_function(
fn=fn,
uri_template=uri,
name=name,
version=version,
title=title,
description=description,
icons=icons,
mime_type=mime_type,
tags=tags,
annotations=annotations,
meta=meta,
task=resolved,
auth=auth,
)
else:
return FunctionResource.from_function(fn, metadata=resource_meta)
def attach_metadata(fn: F) -> F:
metadata = ResourceMeta(
uri=uri,
@ -340,14 +290,6 @@ def resource(
return fn
def decorator(fn: F) -> F:
if fastmcp.settings.decorator_mode == "object":
warnings.warn(
"decorator_mode='object' is deprecated and will be removed in a future version. "
"Decorators now return the original function with metadata attached.",
FastMCPDeprecationWarning,
stacklevel=3,
)
return create_resource(fn) # type: ignore[return-value] # ty:ignore[invalid-return-type]
return attach_metadata(fn)
return decorator

View file

@ -343,8 +343,6 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
annotations=meta.annotations,
meta=meta.meta,
task=resolved_task,
exclude_args=meta.exclude_args,
serializer=meta.serializer,
timeout=meta.timeout,
auth=meta.auth,
run_in_thread=meta.run_in_thread,

View file

@ -13,7 +13,6 @@ from typing import TYPE_CHECKING, Any, TypeVar, overload
import mcp_types
import fastmcp
from fastmcp.prompts.base import Prompt
from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.server.auth.authorization import AuthCheck
@ -189,44 +188,24 @@ class PromptDecoratorMixin:
f"See https://gofastmcp.com/servers/prompts#using-with-methods"
)
resolved_task: bool | TaskConfig = task if task is not None else False
from fastmcp.prompts.function_prompt import PromptMeta
if fastmcp.settings.decorator_mode == "object":
prompt_obj = Prompt.from_function(
fn,
name=prompt_name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
meta=meta,
task=resolved_task,
auth=auth,
)
self._add_component(prompt_obj)
if not enabled:
self.disable(keys={prompt_obj.key})
return prompt_obj
else:
from fastmcp.prompts.function_prompt import PromptMeta
metadata = PromptMeta(
name=prompt_name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
meta=meta,
task=task,
auth=auth,
enabled=enabled,
)
target = fn.__func__ if hasattr(fn, "__func__") else fn
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
self.add_prompt(fn)
return fn
metadata = PromptMeta(
name=prompt_name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
meta=meta,
task=task,
auth=auth,
enabled=enabled,
)
target = fn.__func__ if hasattr(fn, "__func__") else fn
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
self.add_prompt(fn)
return fn
if inspect.isroutine(name_or_fn):
return decorate_and_register(name_or_fn, name)

View file

@ -13,9 +13,7 @@ from typing import TYPE_CHECKING, Any, TypeVar
import mcp_types
from mcp_types import Annotations
import fastmcp
from fastmcp.resources.base import Resource
from fastmcp.resources.function_resource import resource as standalone_resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig
@ -167,8 +165,6 @@ class ResourceDecoratorMixin:
"Use @resource('uri') instead of @resource"
)
resolved_task: bool | TaskConfig = task if task is not None else False
def decorator(fn: AnyFunction) -> Any:
# Check for unbound method
try:
@ -190,54 +186,26 @@ class ResourceDecoratorMixin:
f"See https://gofastmcp.com/servers/resources#using-with-methods"
)
if fastmcp.settings.decorator_mode == "object":
create_resource = standalone_resource(
uri,
name=name,
version=version,
title=title,
description=description,
icons=icons,
mime_type=mime_type,
tags=tags,
annotations=annotations,
meta=meta,
task=resolved_task,
auth=auth,
)
obj = create_resource(fn)
# In legacy mode, standalone_resource always returns a component
assert isinstance(obj, (Resource, ResourceTemplate))
if isinstance(obj, ResourceTemplate):
self.add_template(obj)
if not enabled:
self.disable(keys={obj.key})
else:
self.add_resource(obj)
if not enabled:
self.disable(keys={obj.key})
return obj
else:
from fastmcp.resources.function_resource import ResourceMeta
from fastmcp.resources.function_resource import ResourceMeta
metadata = ResourceMeta(
uri=uri,
name=name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
mime_type=mime_type,
annotations=annotations,
meta=meta,
task=task,
auth=auth,
enabled=enabled,
)
target = fn.__func__ if hasattr(fn, "__func__") else fn
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
self.add_resource(fn)
return fn
metadata = ResourceMeta(
uri=uri,
name=name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
mime_type=mime_type,
annotations=annotations,
meta=meta,
task=task,
auth=auth,
enabled=enabled,
)
target = fn.__func__ if hasattr(fn, "__func__") else fn
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
self.add_resource(fn)
return fn
return decorator

View file

@ -8,7 +8,6 @@ from __future__ import annotations
import inspect
import types
import warnings
from collections.abc import Callable
from functools import partial
from typing import (
@ -26,8 +25,6 @@ from typing import (
import mcp_types
from mcp_types import ToolAnnotations
import fastmcp
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.base import Tool
@ -44,7 +41,6 @@ except ImportError:
if TYPE_CHECKING:
from fastmcp.server.providers.local_provider import LocalProvider
from fastmcp.tools.base import ToolResultSerializerType
F = TypeVar("F", bound=Callable[..., Any])
@ -161,8 +157,6 @@ class ToolDecoratorMixin:
annotations=fmeta.annotations,
meta=tool_meta,
task=resolved_task,
exclude_args=fmeta.exclude_args,
serializer=fmeta.serializer,
timeout=fmeta.timeout,
auth=fmeta.auth,
run_in_thread=fmeta.run_in_thread,
@ -188,11 +182,9 @@ 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,
serializer: ToolResultSerializerType | None = None, # Deprecated
timeout: float | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
run_in_thread: bool = True,
@ -211,20 +203,17 @@ 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,
serializer: ToolResultSerializerType | None = None, # Deprecated
timeout: float | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
run_in_thread: bool = True,
) -> Callable[[F], F]: ...
# NOTE: This method mirrors fastmcp.tools.tool() but adds registration,
# the `enabled` param, and supports deprecated params (serializer, exclude_args).
# 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,
@ -237,11 +226,9 @@ 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,
serializer: ToolResultSerializerType | None = None, # Deprecated
timeout: float | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
run_in_thread: bool = True,
@ -268,11 +255,9 @@ 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
serializer: Deprecated. Return ToolResult from your tools for full control over serialization.
Returns:
The registered FunctionTool or a decorator function.
@ -290,14 +275,6 @@ class ToolDecoratorMixin:
return str(x)
```
"""
if serializer is not None and fastmcp.settings.deprecation_warnings:
warnings.warn(
"The `serializer` parameter is deprecated. "
"Return ToolResult from your tools for full control over serialization. "
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
FastMCPDeprecationWarning,
stacklevel=2,
)
if isinstance(annotations, dict):
annotations = ToolAnnotations(**annotations)
@ -330,57 +307,28 @@ class ToolDecoratorMixin:
f"See https://gofastmcp.com/servers/tools#using-with-methods"
)
resolved_task: bool | TaskConfig = task if task is not None else False
from fastmcp.tools.function_tool import ToolMeta
if fastmcp.settings.decorator_mode == "object":
tool_obj = Tool.from_function(
fn,
name=tool_name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
output_schema=output_schema,
annotations=annotations,
exclude_args=exclude_args,
meta=meta,
serializer=serializer,
task=resolved_task,
timeout=timeout,
auth=auth,
run_in_thread=run_in_thread,
)
self._add_component(tool_obj)
if not enabled:
self.disable(keys={tool_obj.key})
_maybe_apply_prefab_ui(self, tool_obj)
return tool_obj
else:
from fastmcp.tools.function_tool import ToolMeta
metadata = ToolMeta(
name=tool_name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
output_schema=output_schema,
annotations=annotations,
meta=meta,
task=task,
exclude_args=exclude_args,
serializer=serializer,
timeout=timeout,
auth=auth,
enabled=enabled,
run_in_thread=run_in_thread,
)
target = fn.__func__ if hasattr(fn, "__func__") else fn
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
tool_obj = self.add_tool(fn)
return fn
metadata = ToolMeta(
name=tool_name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
output_schema=output_schema,
annotations=annotations,
meta=meta,
task=task,
timeout=timeout,
auth=auth,
enabled=enabled,
run_in_thread=run_in_thread,
)
target = fn.__func__ if hasattr(fn, "__func__") else fn
target.__fastmcp__ = metadata # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
self.add_tool(fn)
return fn
if inspect.isroutine(name_or_fn):
return decorate_and_register(name_or_fn, name)
@ -412,11 +360,9 @@ class ToolDecoratorMixin:
tags=tags,
output_schema=output_schema,
annotations=annotations,
exclude_args=exclude_args,
meta=meta,
enabled=enabled,
task=task,
serializer=serializer,
timeout=timeout,
auth=auth,
run_in_thread=run_in_thread,

View file

@ -4,16 +4,12 @@ from __future__ import annotations
import json
import re
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import httpx
from mcp_types import ToolAnnotations
from pydantic.networks import AnyUrl
import fastmcp
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.resources import (
Resource,
ResourceContent,
@ -151,16 +147,7 @@ class OpenAPITool(Tool):
output_schema: dict[str, Any] | None = None,
tags: set[str] | None = None,
annotations: ToolAnnotations | None = None,
serializer: Callable[[Any], str] | None = None, # Deprecated
):
if serializer is not None and fastmcp.settings.deprecation_warnings:
warnings.warn(
"The `serializer` parameter is deprecated. "
"Return ToolResult from your tools for full control over serialization. "
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
FastMCPDeprecationWarning,
stacklevel=2,
)
super().__init__(
name=name,
description=description,
@ -168,7 +155,6 @@ class OpenAPITool(Tool):
output_schema=output_schema,
tags=tags or set(),
annotations=annotations,
serializer=serializer,
)
self._client = client
self._route = route

View file

@ -1649,7 +1649,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,
@ -1671,7 +1670,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,
@ -1692,7 +1690,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,
@ -1724,8 +1721,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:
@ -1771,7 +1766,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,

View file

@ -396,20 +396,3 @@ class Settings(BaseSettings):
),
),
] = "stable"
decorator_mode: Annotated[
Literal["function", "object"],
Field(
description=inspect.cleandoc(
"""
Controls what decorators (@tool, @resource, @prompt) return.
- "function" (default): Decorators return the original function unchanged.
The function remains callable and is registered with the server normally.
- "object" (deprecated): Decorators return component objects (FunctionTool,
FunctionResource, FunctionPrompt). This was the default behavior in v2 and
will be removed in a future version.
"""
),
),
] = "function"

View file

@ -1,13 +1,11 @@
from __future__ import annotations
import warnings
from collections.abc import Callable
from typing import (
TYPE_CHECKING,
Annotated,
Any,
ClassVar,
TypeAlias,
overload,
)
@ -26,7 +24,6 @@ from mcp_types import Tool as MCPTool
from pydantic import BaseModel, Field, model_validator
from pydantic.json_schema import SkipJsonSchema
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
@ -59,9 +56,6 @@ if TYPE_CHECKING:
logger = get_logger(__name__)
ToolResultSerializerType: TypeAlias = Callable[[Any], str]
def resolve_serialize_by_alias(value: Any) -> bool:
"""Resolve the effective ``by_alias`` setting for serializing *value*.
@ -196,12 +190,6 @@ class Tool(FastMCPComponent):
ToolExecution | None,
Field(description="Task execution configuration (SEP-1686)"),
] = None
serializer: Annotated[
SkipJsonSchema[ToolResultSerializerType | None],
Field(
description="Deprecated. Return ToolResult from your tools for full control over serialization."
),
] = None
auth: Annotated[
SkipJsonSchema[AuthCheck | list[AuthCheck] | None],
Field(description="Authorization checks for this tool", exclude=True),
@ -266,9 +254,7 @@ 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,
serializer: ToolResultSerializerType | None = None, # Deprecated
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
timeout: float | None = None,
@ -287,9 +273,7 @@ class Tool(FastMCPComponent):
icons=icons,
tags=tags,
annotations=annotations,
exclude_args=exclude_args,
output_schema=output_schema,
serializer=serializer,
meta=meta,
task=task,
timeout=timeout,
@ -313,7 +297,7 @@ class Tool(FastMCPComponent):
"""Convert a raw result to ToolResult.
Handles ToolResult passthrough and converts raw values using the tool's
attributes (serializer, output_schema) for proper conversion.
attributes (output_schema) for proper conversion.
"""
if isinstance(raw_value, ToolResult):
return raw_value
@ -330,7 +314,7 @@ class Tool(FastMCPComponent):
fastmcp_app_name=_get_fastmcp_app_name(self),
)
content = _convert_to_content(raw_value, serializer=self.serializer)
content = _convert_to_content(raw_value)
# Bytes can't be represented as structured JSON content
if isinstance(raw_value, bytes):
@ -460,7 +444,6 @@ class Tool(FastMCPComponent):
tags: set[str] | None = None,
annotations: ToolAnnotations | NotSetT | None = NotSet,
output_schema: dict[str, Any] | NotSetT | None = NotSet,
serializer: ToolResultSerializerType | None = None, # Deprecated
meta: dict[str, Any] | NotSetT | None = NotSet,
transform_args: dict[str, ArgTransform] | None = None,
transform_fn: Callable[..., Any] | None = None,
@ -479,7 +462,6 @@ class Tool(FastMCPComponent):
tags=tags,
annotations=annotations,
output_schema=output_schema,
serializer=serializer,
meta=meta,
)
@ -505,25 +487,8 @@ class Tool(FastMCPComponent):
}
def _serialize_with_fallback(
result: Any, serializer: ToolResultSerializerType | None = None
) -> str:
if serializer is not None:
try:
return serializer(result)
except Exception as e:
logger.warning(
"Error serializing tool result: %s",
e,
exc_info=True,
)
return default_serializer(result)
def _convert_to_single_content_block(
item: Any,
serializer: ToolResultSerializerType | None = None,
) -> ContentBlock:
if isinstance(item, ContentBlock):
return item
@ -548,7 +513,7 @@ def _convert_to_single_content_block(
return TextContent(type="text", text=base64.b64encode(item).decode("ascii"))
return TextContent(type="text", text=_serialize_with_fallback(item, serializer))
return TextContent(type="text", text=default_serializer(item))
_PREFAB_TEXT_FALLBACK = "[Rendered Prefab UI]"
@ -599,7 +564,6 @@ def _prefab_to_tool_result(app: Any, fastmcp_app_name: str | None = None) -> Too
def _convert_to_content(
result: Any,
serializer: ToolResultSerializerType | None = None,
) -> list[ContentBlock]:
"""Convert a result to a sequence of content objects."""
@ -607,7 +571,7 @@ def _convert_to_content(
return []
if not isinstance(result, (list | tuple)):
return [_convert_to_single_content_block(result, serializer)]
return [_convert_to_single_content_block(result)]
# If all items are ContentBlocks, return them as is
if all(isinstance(item, ContentBlock) for item in result):
@ -617,38 +581,13 @@ def _convert_to_content(
# without aggregating them
if any(isinstance(item, ContentBlock | Image | Audio | File) for item in result):
return [
_convert_to_single_content_block(item, serializer)
_convert_to_single_content_block(item)
if not isinstance(item, ContentBlock)
else item
for item in result
]
# If none of the items are ContentBlocks, aggregate all items into a single TextContent
return [TextContent(type="text", text=_serialize_with_fallback(result, serializer))]
return [TextContent(type="text", text=default_serializer(result))]
__all__ = ["Tool", "ToolResult"]
def __getattr__(name: str) -> Any:
"""Deprecated re-exports for backwards compatibility."""
deprecated_exports = {
"FunctionTool": "FunctionTool",
"ParsedFunction": "ParsedFunction",
"tool": "tool",
}
if name in deprecated_exports:
import fastmcp
if fastmcp.settings.deprecation_warnings:
warnings.warn(
f"Importing {name} from fastmcp.tools.tool is deprecated. "
f"Import from fastmcp.tools.function_tool instead.",
FastMCPDeprecationWarning,
stacklevel=2,
)
from fastmcp.tools import function_tool
return getattr(function_tool, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View file

@ -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, "..."])

View file

@ -5,7 +5,6 @@ from __future__ import annotations
import functools
import inspect
import logging
import warnings
from collections.abc import Callable
from dataclasses import dataclass, field
from functools import lru_cache
@ -30,13 +29,11 @@ from pydantic import Field, TypeAdapter
from pydantic import ValidationError as PydanticValidationError
from pydantic.json_schema import SkipJsonSchema
import fastmcp
from fastmcp.decorators import get_fastmcp_meta, resolve_task_config
from fastmcp.exceptions import FastMCPDeprecationWarning, ValidationError
from fastmcp.decorators import get_fastmcp_meta
from fastmcp.exceptions import ValidationError
from fastmcp.tools.base import (
Tool,
ToolResult,
ToolResultSerializerType,
)
from fastmcp.tools.function_parsing import ParsedFunction, _is_object_schema
from fastmcp.utilities.async_utils import (
@ -170,8 +167,6 @@ class ToolMeta:
meta: dict[str, Any] | None = None
app: Any = None
task: bool | TaskConfig | None = None
exclude_args: list[str] | None = None
serializer: Any | None = None
timeout: float | None = None
auth: AuthCheck | list[AuthCheck] | None = None
enabled: bool = True
@ -236,9 +231,7 @@ 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,
serializer: ToolResultSerializerType | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
timeout: float | None = None,
@ -268,14 +261,12 @@ class FunctionTool(Tool):
annotations,
meta,
task,
serializer,
timeout,
auth,
run_in_thread,
]
)
or output_schema is not NotSet
or exclude_args is not None
)
if metadata is not None and individual_params_provided:
@ -302,31 +293,12 @@ class FunctionTool(Tool):
annotations=annotations,
meta=meta,
task=task,
exclude_args=exclude_args,
serializer=serializer,
timeout=timeout,
auth=auth,
run_in_thread=True if run_in_thread is None else run_in_thread,
)
if metadata.serializer is not None and fastmcp.settings.deprecation_warnings:
warnings.warn(
"The `serializer` parameter is deprecated. "
"Return ToolResult from your tools for full control over serialization. "
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
FastMCPDeprecationWarning,
stacklevel=2,
)
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 == "<lambda>":
@ -389,7 +361,6 @@ class FunctionTool(Tool):
output_schema=final_output_schema,
annotations=metadata.annotations,
tags=metadata.tags or set(),
serializer=metadata.serializer,
meta=metadata.meta,
task_config=task_config,
timeout=metadata.timeout,
@ -605,8 +576,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,
serializer: Any | None = None,
timeout: float | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
run_in_thread: bool = True,
@ -625,8 +594,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,
serializer: Any | None = None,
timeout: float | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
run_in_thread: bool = True,
@ -646,8 +613,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,
serializer: Any | None = None,
timeout: float | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
run_in_thread: bool = True,
@ -676,27 +641,6 @@ def tool(
"See https://gofastmcp.com/servers/tools#using-with-methods"
)
def create_tool(fn: Callable[..., Any], tool_name: str | None) -> FunctionTool:
# Create metadata first, then pass it
tool_meta = ToolMeta(
name=tool_name,
version=version,
title=title,
description=description,
icons=icons,
tags=tags,
output_schema=output_schema,
annotations=annotations,
meta=meta,
task=resolve_task_config(task),
exclude_args=exclude_args,
serializer=serializer,
timeout=timeout,
auth=auth,
run_in_thread=run_in_thread,
)
return FunctionTool.from_function(fn, metadata=tool_meta)
def attach_metadata(fn: F, tool_name: str | None) -> F:
metadata = ToolMeta(
name=tool_name,
@ -709,8 +653,6 @@ def tool(
annotations=annotations,
meta=meta,
task=task,
exclude_args=exclude_args,
serializer=serializer,
timeout=timeout,
auth=auth,
run_in_thread=run_in_thread,
@ -720,14 +662,6 @@ def tool(
return fn
def decorator(fn: F, tool_name: str | None) -> F:
if fastmcp.settings.decorator_mode == "object":
warnings.warn(
"decorator_mode='object' is deprecated and will be removed in a future version. "
"Decorators now return the original function with metadata attached.",
FastMCPDeprecationWarning,
stacklevel=4,
)
return create_tool(fn, tool_name) # type: ignore[return-value] # ty:ignore[invalid-return-type]
return attach_metadata(fn, tool_name)
if inspect.isroutine(name_or_fn):

View file

@ -1,7 +1,6 @@
from __future__ import annotations
import inspect
import warnings
from collections.abc import Callable
from contextvars import ContextVar
from copy import deepcopy
@ -15,8 +14,6 @@ from pydantic.fields import Field
from pydantic.functional_validators import BeforeValidator
from pydantic.json_schema import SkipJsonSchema
import fastmcp
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.tools.base import (
Tool,
ToolResult,
@ -343,9 +340,7 @@ class TransformedTool(Tool):
# Otherwise convert to content and create ToolResult with proper structured content
unstructured_result = _convert_to_content(
result, serializer=self.serializer
)
unstructured_result = _convert_to_content(result)
structured_output = None
# First handle structured content based on output schema, if any
@ -393,7 +388,6 @@ class TransformedTool(Tool):
transform_args: dict[str, ArgTransform] | None = None,
annotations: ToolAnnotations | NotSetT | None = NotSet,
output_schema: dict[str, Any] | NotSetT | None = NotSet,
serializer: Callable[[Any], str] | NotSetT | None = NotSet, # Deprecated
meta: dict[str, Any] | NotSetT | None = NotSet,
) -> TransformedTool:
"""Create a transformed tool from a parent tool.
@ -415,7 +409,6 @@ class TransformedTool(Tool):
output_schema: Control output schema for structured outputs:
- None (default): Inherit from transform_fn if available, then parent tool
- dict: Use custom output schema
serializer: Deprecated. Return ToolResult from your tools for full control over serialization.
meta: Control meta information:
- NotSet (default): Inherit from parent tool
- dict: Use custom meta information
@ -470,18 +463,6 @@ class TransformedTool(Tool):
"""
tool = Tool._ensure_tool(tool)
if (
serializer is not NotSet
and serializer is not None
and fastmcp.settings.deprecation_warnings
):
warnings.warn(
"The `serializer` parameter is deprecated. "
"Return ToolResult from your tools for full control over serialization. "
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
FastMCPDeprecationWarning,
stacklevel=2,
)
transform_args = transform_args or {}
if transform_fn is not None:
@ -601,9 +582,6 @@ class TransformedTool(Tool):
final_annotations = (
annotations if not isinstance(annotations, NotSetT) else tool.annotations
)
final_serializer = (
serializer if not isinstance(serializer, NotSetT) else tool.serializer
)
transformed_tool = cls(
fn=final_fn,
@ -617,7 +595,6 @@ class TransformedTool(Tool):
output_schema=final_output_schema,
tags=tags or tool.tags,
annotations=final_annotations,
serializer=final_serializer,
meta=final_meta,
transform_args=transform_args,
auth=tool.auth,

View file

@ -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"]

View file

@ -1,121 +0,0 @@
"""Test that deprecated import paths for function components still work."""
import warnings
import pytest
from fastmcp.utilities.tests import temporary_settings
class TestDeprecatedFunctionToolImports:
def test_function_tool_from_tool_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.tools.function_tool"
):
from fastmcp.tools.base import FunctionTool
# Verify it's the real class
from fastmcp.tools.function_tool import (
FunctionTool as CanonicalFunctionTool,
)
assert FunctionTool is CanonicalFunctionTool
def test_parsed_function_from_tool_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.tools.function_tool"
):
from fastmcp.tools.base import ParsedFunction
from fastmcp.tools.function_tool import (
ParsedFunction as CanonicalParsedFunction,
)
assert ParsedFunction is CanonicalParsedFunction
def test_tool_decorator_from_tool_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.tools.function_tool"
):
from fastmcp.tools.base import tool
from fastmcp.tools.function_tool import tool as canonical_tool
assert tool is canonical_tool
def test_no_warning_when_disabled(self):
with temporary_settings(deprecation_warnings=False):
with warnings.catch_warnings():
warnings.simplefilter("error")
from fastmcp.tools.base import FunctionTool # noqa: F401
class TestDeprecatedFunctionResourceImports:
def test_function_resource_from_resource_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning,
match="Import from fastmcp.resources.function_resource",
):
from fastmcp.resources.base import FunctionResource
from fastmcp.resources.function_resource import (
FunctionResource as CanonicalFunctionResource,
)
assert FunctionResource is CanonicalFunctionResource
def test_resource_decorator_from_resource_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning,
match="Import from fastmcp.resources.function_resource",
):
from fastmcp.resources.base import resource
from fastmcp.resources.function_resource import (
resource as canonical_resource,
)
assert resource is canonical_resource
def test_no_warning_when_disabled(self):
with temporary_settings(deprecation_warnings=False):
with warnings.catch_warnings():
warnings.simplefilter("error")
from fastmcp.resources.base import FunctionResource # noqa: F401
class TestDeprecatedFunctionPromptImports:
def test_function_prompt_from_prompt_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.prompts.function_prompt"
):
from fastmcp.prompts.base import FunctionPrompt
from fastmcp.prompts.function_prompt import (
FunctionPrompt as CanonicalFunctionPrompt,
)
assert FunctionPrompt is CanonicalFunctionPrompt
def test_prompt_decorator_from_prompt_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.prompts.function_prompt"
):
from fastmcp.prompts.base import prompt
from fastmcp.prompts.function_prompt import prompt as canonical_prompt
assert prompt is canonical_prompt
def test_no_warning_when_disabled(self):
with temporary_settings(deprecation_warnings=False):
with warnings.catch_warnings():
warnings.simplefilter("error")
from fastmcp.prompts.base import FunctionPrompt # noqa: F401

View file

@ -1,178 +0,0 @@
"""Tests for deprecated tool serializer functionality.
These tests verify that serializer parameters still work but are deprecated.
All serializer-related tests should be moved here.
"""
import warnings
import pytest
from inline_snapshot import snapshot
from mcp_types import TextContent
from fastmcp import FastMCP
from fastmcp.contrib.mcp_mixin import mcp_tool
from fastmcp.server.providers import LocalProvider
from fastmcp.tools.base import Tool, _convert_to_content
from fastmcp.tools.tool_transform import TransformedTool
from fastmcp.utilities.tests import temporary_settings
class TestToolSerializerDeprecated:
"""Tests for deprecated serializer functionality."""
async def test_tool_serializer(self):
"""Test that a tool's serializer is used to serialize the result."""
def custom_serializer(data) -> str:
return f"Custom serializer: {data}"
def process_list(items: list[int]) -> int:
return sum(items)
tool = Tool.from_function(process_list, serializer=custom_serializer)
result = await tool.run(arguments={"items": [1, 2, 3, 4, 5]})
# Custom serializer affects unstructured content
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Custom serializer: 15"
# Structured output should have the raw value
assert result.structured_content == {"result": 15}
def test_custom_serializer(self):
"""Test that a custom serializer is used for non-MCP types."""
def custom_serializer(data):
return f"Serialized: {data}"
result = _convert_to_content({"a": 1}, serializer=custom_serializer)
assert result == snapshot(
[TextContent(type="text", text="Serialized: {'a': 1}")]
)
def test_custom_serializer_error_fallback(self, caplog):
"""Test that if a custom serializer fails, it falls back to the default."""
def custom_serializer_that_fails(data):
raise ValueError("Serialization failed")
result = _convert_to_content({"a": 1}, serializer=custom_serializer_that_fails)
assert isinstance(result, list)
assert result == snapshot([TextContent(type="text", text='{"a":1}')])
assert "Error serializing tool result" in caplog.text
class TestSerializerDeprecationWarnings:
"""Tests that deprecation warnings are raised when serializer is used."""
def test_tool_from_function_serializer_warning(self):
"""Test that Tool.from_function warns when serializer is provided."""
def custom_serializer(data) -> str:
return f"Custom: {data}"
def my_tool(x: int) -> int:
return x * 2
with temporary_settings(deprecation_warnings=True):
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
Tool.from_function(my_tool, serializer=custom_serializer)
def test_tool_from_function_serializer_no_warning_when_disabled(self):
"""Test that no warning is raised when deprecation_warnings is False."""
def custom_serializer(data) -> str:
return f"Custom: {data}"
def my_tool(x: int) -> int:
return x * 2
with temporary_settings(deprecation_warnings=False):
with warnings.catch_warnings():
warnings.simplefilter("error")
# Should not raise
Tool.from_function(my_tool, serializer=custom_serializer)
def test_local_provider_tool_serializer_warning(self):
"""Test that LocalProvider.tool warns when serializer is provided."""
provider = LocalProvider()
def custom_serializer(data) -> str:
return f"Custom: {data}"
def my_tool(x: int) -> int:
return x * 2
with temporary_settings(deprecation_warnings=True):
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
provider.tool(my_tool, serializer=custom_serializer)
def test_local_provider_tool_decorator_serializer_warning(self):
"""Test that LocalProvider.tool decorator warns when serializer is provided."""
provider = LocalProvider()
def custom_serializer(data) -> str:
return f"Custom: {data}"
with temporary_settings(deprecation_warnings=True):
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
@provider.tool(serializer=custom_serializer)
def my_tool(x: int) -> int:
return x * 2
def test_fastmcp_tool_serializer_warning(self):
"""Test that FastMCP.tool warns when serializer is provided via LocalProvider."""
def custom_serializer(data) -> str:
return f"Custom: {data}"
def my_tool(x: int) -> int:
return x * 2
# FastMCP.tool doesn't accept serializer directly, it goes through LocalProvider
# So we test LocalProvider.tool which is what FastMCP uses internally
provider = LocalProvider()
with temporary_settings(deprecation_warnings=True):
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
provider.tool(my_tool, serializer=custom_serializer)
def test_fastmcp_tool_serializer_parameter_raises_type_error(self):
"""Test that FastMCP tool_serializer parameter raises TypeError."""
def custom_serializer(data) -> str:
return f"Custom: {data}"
with pytest.raises(TypeError, match="no longer accepts `tool_serializer`"):
FastMCP("TestServer", tool_serializer=custom_serializer)
def test_transformed_tool_from_tool_serializer_warning(self):
"""Test that TransformedTool.from_tool warns when serializer is provided."""
def custom_serializer(data) -> str:
return f"Custom: {data}"
def my_tool(x: int) -> int:
return x * 2
parent_tool = Tool.from_function(my_tool)
with temporary_settings(deprecation_warnings=True):
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
TransformedTool.from_tool(parent_tool, serializer=custom_serializer)
def test_mcp_mixin_tool_serializer_warning(self):
"""Test that mcp_tool decorator warns when serializer is provided."""
def custom_serializer(data) -> str:
return f"Custom: {data}"
with temporary_settings(deprecation_warnings=True):
with pytest.warns(DeprecationWarning, match="serializer.*deprecated"):
@mcp_tool(serializer=custom_serializer)
def my_tool(x: int) -> int:
return x * 2