mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-27 07:50:43 +02:00
Merge pull request #308 from strawgate/custom-serializer-example
Support custom Serializer for Tools
This commit is contained in:
commit
a553f3871a
6 changed files with 80 additions and 5 deletions
32
examples/serializer.py
Normal file
32
examples/serializer.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
|
||||
# Define a simple custom serializer
|
||||
def custom_dict_serializer(data: Any) -> str:
|
||||
return yaml.dump(data, width=100, sort_keys=False)
|
||||
|
||||
|
||||
server = FastMCP(name="CustomSerializerExample", tool_serializer=custom_dict_serializer)
|
||||
|
||||
|
||||
@server.tool()
|
||||
def get_example_data() -> dict:
|
||||
"""Returns some example data."""
|
||||
return {"name": "Test", "value": 123, "status": True}
|
||||
|
||||
|
||||
async def example_usage():
|
||||
result = await server._mcp_call_tool("get_example_data", {})
|
||||
print("Tool Result:")
|
||||
print(result)
|
||||
print("This is an example of using a custom serializer with FastMCP.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(example_usage())
|
||||
server.run()
|
||||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
import enum
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from re import Pattern
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
|
@ -127,6 +128,7 @@ class OpenAPITool(Tool):
|
|||
tags: set[str] = set(),
|
||||
timeout: float | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
serializer: Callable[[Any], str] | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
name=name,
|
||||
|
|
@ -138,6 +140,7 @@ class OpenAPITool(Tool):
|
|||
context_kwarg="context", # Default context keyword argument
|
||||
tags=tags,
|
||||
annotations=annotations,
|
||||
serializer=serializer,
|
||||
)
|
||||
self._client = client
|
||||
self._route = route
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
| None
|
||||
) = None,
|
||||
tags: set[str] | None = None,
|
||||
tool_serializer: Callable[[Any], str] | None = None,
|
||||
**settings: Any,
|
||||
):
|
||||
self.tags: set[str] = tags or set()
|
||||
|
|
@ -226,7 +227,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
lifespan=_lifespan_wrapper(self, lifespan),
|
||||
)
|
||||
self._tool_manager = ToolManager(
|
||||
duplicate_behavior=self.settings.on_duplicate_tools
|
||||
duplicate_behavior=self.settings.on_duplicate_tools,
|
||||
serializer=tool_serializer,
|
||||
)
|
||||
self._resource_manager = ResourceManager(
|
||||
duplicate_behavior=self.settings.on_duplicate_resources
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ class Tool(BaseModel):
|
|||
annotations: ToolAnnotations | None = Field(
|
||||
None, description="Additional annotations about the tool"
|
||||
)
|
||||
serializer: Callable[[Any], str] | None = Field(
|
||||
None, description="Optional custom serializer for tool results"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_function(
|
||||
|
|
@ -55,6 +58,7 @@ class Tool(BaseModel):
|
|||
context_kwarg: str | None = None,
|
||||
tags: set[str] | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
serializer: Callable[[Any], str] | None = None,
|
||||
) -> Tool:
|
||||
"""Create a Tool from a function."""
|
||||
from fastmcp import Context
|
||||
|
|
@ -100,6 +104,7 @@ class Tool(BaseModel):
|
|||
context_kwarg=context_kwarg,
|
||||
tags=tags or set(),
|
||||
annotations=annotations,
|
||||
serializer=serializer,
|
||||
)
|
||||
|
||||
async def run(
|
||||
|
|
@ -120,7 +125,7 @@ class Tool(BaseModel):
|
|||
arguments_to_validate=arguments,
|
||||
arguments_to_pass_directly=pass_args,
|
||||
)
|
||||
return _convert_to_content(result)
|
||||
return _convert_to_content(result, serializer=self.serializer)
|
||||
except Exception as e:
|
||||
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
||||
|
||||
|
|
@ -141,6 +146,7 @@ class Tool(BaseModel):
|
|||
|
||||
def _convert_to_content(
|
||||
result: Any,
|
||||
serializer: Callable[[Any], str] | None = None,
|
||||
_process_as_single_item: bool = False,
|
||||
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
||||
"""Convert a result to a sequence of content objects."""
|
||||
|
|
@ -176,6 +182,9 @@ def _convert_to_content(
|
|||
return other_content + mcp_types
|
||||
|
||||
if not isinstance(result, str):
|
||||
result = pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
||||
if serializer is not None:
|
||||
result = serializer(result)
|
||||
else:
|
||||
result = pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
||||
|
||||
return [TextContent(type="text", text=result)]
|
||||
|
|
|
|||
|
|
@ -22,8 +22,13 @@ logger = get_logger(__name__)
|
|||
class ToolManager:
|
||||
"""Manages FastMCP tools."""
|
||||
|
||||
def __init__(self, duplicate_behavior: DuplicateBehavior | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
duplicate_behavior: DuplicateBehavior | None = None,
|
||||
serializer: Callable[[Any], str] | None = None,
|
||||
):
|
||||
self._tools: dict[str, Tool] = {}
|
||||
self._serializer = serializer
|
||||
|
||||
# Default to "warn" if None is provided
|
||||
if duplicate_behavior is None:
|
||||
|
|
@ -70,6 +75,7 @@ class ToolManager:
|
|||
description=description,
|
||||
tags=tags,
|
||||
annotations=annotations,
|
||||
serializer=self._serializer,
|
||||
)
|
||||
return self.add_tool(tool)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
import logging
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Any
|
||||
|
||||
import pytest
|
||||
from mcp.server.session import ServerSessionT
|
||||
|
|
@ -392,6 +392,29 @@ class TestCallTools:
|
|||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].text == '[\n "rex",\n "gertrude"\n]'
|
||||
|
||||
async def test_call_tool_with_custom_serializer(self):
|
||||
"""Test that a custom serializer provided to FastMCP is used by tools."""
|
||||
|
||||
def custom_serializer(data: Any) -> str:
|
||||
if isinstance(data, dict):
|
||||
return f"CUSTOM:{json.dumps(data)}"
|
||||
return json.dumps(data)
|
||||
|
||||
# Instantiate FastMCP with the custom serializer
|
||||
mcp = FastMCP(tool_serializer=custom_serializer)
|
||||
manager = mcp._tool_manager
|
||||
|
||||
def get_data() -> dict:
|
||||
return {"key": "value", "number": 123}
|
||||
|
||||
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 == 'CUSTOM:{"key": "value", "number": 123}'
|
||||
|
||||
|
||||
class TestToolSchema:
|
||||
async def test_context_arg_excluded_from_schema(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue