Remove deprecated tool-level serializer parameter

This commit is contained in:
Jeremiah Lowin 2026-07-06 21:34:42 -04:00
commit 7f032bb82e
No known key found for this signature in database
8 changed files with 8 additions and 306 deletions

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

@ -344,7 +344,6 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
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

@ -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 (
@ -27,7 +26,6 @@ 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 +42,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])
@ -162,7 +159,6 @@ class ToolDecoratorMixin:
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,
@ -192,7 +188,6 @@ class ToolDecoratorMixin:
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,
@ -215,14 +210,13 @@ class ToolDecoratorMixin:
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).
# 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.
def tool(
@ -241,7 +235,6 @@ class ToolDecoratorMixin:
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,
@ -272,7 +265,6 @@ class ToolDecoratorMixin:
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 +282,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)
@ -345,7 +329,6 @@ class ToolDecoratorMixin:
annotations=annotations,
exclude_args=exclude_args,
meta=meta,
serializer=serializer,
task=resolved_task,
timeout=timeout,
auth=auth,
@ -371,7 +354,6 @@ class ToolDecoratorMixin:
meta=meta,
task=task,
exclude_args=exclude_args,
serializer=serializer,
timeout=timeout,
auth=auth,
enabled=enabled,
@ -416,7 +398,6 @@ class ToolDecoratorMixin:
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

@ -7,7 +7,6 @@ from typing import (
Annotated,
Any,
ClassVar,
TypeAlias,
overload,
)
@ -59,9 +58,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 +192,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),
@ -268,7 +258,6 @@ class Tool(FastMCPComponent):
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,
@ -289,7 +278,6 @@ class Tool(FastMCPComponent):
annotations=annotations,
exclude_args=exclude_args,
output_schema=output_schema,
serializer=serializer,
meta=meta,
task=task,
timeout=timeout,
@ -313,7 +301,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 +318,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 +448,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 +466,6 @@ class Tool(FastMCPComponent):
tags=tags,
annotations=annotations,
output_schema=output_schema,
serializer=serializer,
meta=meta,
)
@ -505,25 +491,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 +517,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 +568,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 +575,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,13 +585,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"]

View file

@ -36,7 +36,6 @@ from fastmcp.exceptions import FastMCPDeprecationWarning, 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 (
@ -171,7 +170,6 @@ class ToolMeta:
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
@ -238,7 +236,6 @@ class FunctionTool(Tool):
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,7 +265,6 @@ class FunctionTool(Tool):
annotations,
meta,
task,
serializer,
timeout,
auth,
run_in_thread,
@ -303,20 +299,11 @@ class FunctionTool(Tool):
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. "
@ -389,7 +376,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,
@ -606,7 +592,6 @@ def tool(
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,
@ -626,7 +611,6 @@ def tool(
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,
@ -647,7 +631,6 @@ def tool(
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,
@ -690,7 +673,6 @@ def tool(
meta=meta,
task=resolve_task_config(task),
exclude_args=exclude_args,
serializer=serializer,
timeout=timeout,
auth=auth,
run_in_thread=run_in_thread,
@ -710,7 +692,6 @@ def tool(
meta=meta,
task=task,
exclude_args=exclude_args,
serializer=serializer,
timeout=timeout,
auth=auth,
run_in_thread=run_in_thread,

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,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