mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 05:24:18 +02:00
Merge pull request #310 from jlowin/serialize-fallback
Ensure that tool serialization has a graceful fallback
This commit is contained in:
commit
752d8ad197
4 changed files with 78 additions and 3 deletions
|
|
@ -207,6 +207,8 @@ main_mcp.mount(
|
|||
|
||||
### Direct vs. Proxy Mounting
|
||||
|
||||
<VersionBadge version="2.2.7" />
|
||||
|
||||
FastMCP supports two modes for mounting servers:
|
||||
|
||||
1. **Direct Mounting** (default): The parent server directly accesses the mounted server's objects in memory for optimal performance and observability. In this mode:
|
||||
|
|
|
|||
|
|
@ -295,6 +295,40 @@ print(mcp.settings.on_duplicate_tools) # Output: "error"
|
|||
|
||||
All of these can be configured directly as parameters when creating the `FastMCP` instance.
|
||||
|
||||
### Custom Tool Serialization
|
||||
|
||||
<VersionBadge version="2.2.7" />
|
||||
|
||||
By default, FastMCP serializes tool return values to JSON when they need to be converted to text. You can customize this behavior by providing a `tool_serializer` function when creating your server:
|
||||
|
||||
```python
|
||||
import yaml
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Define a custom serializer that formats dictionaries as YAML
|
||||
def yaml_serializer(data):
|
||||
return yaml.dump(data, sort_keys=False)
|
||||
|
||||
# Create a server with the custom serializer
|
||||
mcp = FastMCP(name="MyServer", tool_serializer=yaml_serializer)
|
||||
|
||||
@mcp.tool()
|
||||
def get_config():
|
||||
"""Returns configuration in YAML format."""
|
||||
return {"api_key": "abc123", "debug": True, "rate_limit": 100}
|
||||
```
|
||||
|
||||
The serializer function takes any data object and returns a string representation. This is applied to **all non-string return values** from your tools. Tools that already return strings bypass the serializer.
|
||||
|
||||
This customization is useful when you want to:
|
||||
- Format data in a specific way (like YAML or custom formats)
|
||||
- Control specific serialization options (like indentation or sorting)
|
||||
- Add metadata or transform data before sending it to clients
|
||||
|
||||
<Tip>
|
||||
If the serializer function raises an exception, the tool will fall back to the default JSON serialization to avoid breaking the server.
|
||||
</Tip>
|
||||
|
||||
## Authentication
|
||||
|
||||
<VersionBadge version="2.2.7" />
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from pydantic import BaseModel, BeforeValidator, Field
|
|||
|
||||
from fastmcp.exceptions import ToolError
|
||||
from fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import (
|
||||
Image,
|
||||
_convert_set_defaults,
|
||||
|
|
@ -23,6 +24,12 @@ if TYPE_CHECKING:
|
|||
|
||||
from fastmcp.server import Context
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def default_serializer(data: Any) -> str:
|
||||
return pydantic_core.to_json(data, fallback=str, indent=2).decode()
|
||||
|
||||
|
||||
class Tool(BaseModel):
|
||||
"""Internal tool registration info."""
|
||||
|
|
@ -182,9 +189,17 @@ def _convert_to_content(
|
|||
return other_content + mcp_types
|
||||
|
||||
if not isinstance(result, str):
|
||||
if serializer is not None:
|
||||
result = serializer(result)
|
||||
if serializer is None:
|
||||
result = default_serializer(result)
|
||||
else:
|
||||
result = pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
||||
try:
|
||||
result = serializer(result)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Error serializing tool result: %s",
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
result = default_serializer(result)
|
||||
|
||||
return [TextContent(type="text", text=result)]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Annotated, Any
|
||||
|
||||
import pydantic_core
|
||||
import pytest
|
||||
from mcp.server.session import ServerSessionT
|
||||
from mcp.shared.context import LifespanContextT
|
||||
|
|
@ -415,6 +417,28 @@ class TestCallTools:
|
|||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].text == 'CUSTOM:{"key": "value", "number": 123}'
|
||||
|
||||
async def test_custom_serializer_fallback_on_error(self):
|
||||
"""Test that a broken custom serializer gracefully falls back."""
|
||||
|
||||
uuid_result = uuid.uuid4()
|
||||
|
||||
def custom_serializer(data: Any) -> str:
|
||||
return json.dumps(data)
|
||||
|
||||
mcp = FastMCP(tool_serializer=custom_serializer)
|
||||
manager = mcp._tool_manager
|
||||
|
||||
def get_data() -> uuid.UUID:
|
||||
return uuid_result
|
||||
|
||||
manager.add_tool_from_fn(get_data)
|
||||
|
||||
result = await manager.call_tool("get_data", {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].text == pydantic_core.to_json(uuid_result).decode()
|
||||
|
||||
|
||||
class TestToolSchema:
|
||||
async def test_context_arg_excluded_from_schema(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue