Draft: Custom Serializer for Tools
Example for #207
running the example prints:
Tool Result:
[TextContent(type='text', text='name: Test\nvalue: 123\nstatus: true\n', annotations=None)]
I think this is all possible (and considerably more customizable, flexible, and supported) by using Pydantic's existing functionality, so I'd prefer to lean on that. For example, this script permits the same outcome with no modification to FastMCP:
import asyncio
import yaml
import pydantic
from fastmcp import FastMCP, Client
mcp = FastMCP()
# new base class with serializer
class PrettyPrintModel(pydantic.BaseModel):
@pydantic.model_serializer
def serialize(self) -> str:
return yaml.dump(dict(self), width=100, sort_keys=False)
# pretty-printed custom model
class ReturnModel(PrettyPrintModel):
x: int
y: str
# model with custom serializer
class ReturnModel2(pydantic.BaseModel):
x: int
y: str
@pydantic.model_serializer
def serialize(self) -> str:
return f"A return model with {self.x} and {self.y}"
@mcp.tool()
async def demo_pretty(x: int, y: str) -> ReturnModel:
"""Demonstrate a pretty-printed model."""
return ReturnModel(x=x, y=y)
@mcp.tool()
async def demo_custom(x: int, y: str) -> ReturnModel2:
"""Demonstrate a custom serializer."""
return ReturnModel2(x=x, y=y)
async def main():
async with Client(mcp) as client:
result1 = await client.call_tool("demo_pretty", dict(x=1, y="hello"))
print(f"Pretty result: {result1}")
result2 = await client.call_tool("demo_custom", dict(x=1, y="hello"))
print(f"Custom result: {result2}")
if __name__ == "__main__":
asyncio.run(main())
That's certainly true but it comes with some challenges -- I believe that in pydantic, model serializers returning strings is a pretty blunt tool and doing this would break many other use cases for model dump as generally you expect a model to serialize to a dictionary (which then can be separately serialized to a string).
My overall goal is to be able to build a server that I wrap with FastMCP and so customizing models in a way that I think breaks other pydantic functionality is less than ideal.
I totally understand if this is not functionality you're interested in making configurable but I think the overall argument is that if the code is going to call a serializer with specific settings like indent, etc that serializer should be customizable.
Ah, ok ok I understand better now. I am a little nervous about the "blunt instrument" of making serialization globally configurable but I agree with you now it is the best solution, and preserves the "scalpel" of Pydantic serializers for users who want it.
addressed feedback
LGTM, thanks @strawgate! Made small push to get typing to pass and should be good to go