fastmcp icon indicating copy to clipboard operation
fastmcp copied to clipboard

Draft: Custom Serializer for Tools

Open strawgate opened this issue 7 months ago • 1 comments

Example for #207

running the example prints:

Tool Result:
[TextContent(type='text', text='name: Test\nvalue: 123\nstatus: true\n', annotations=None)]

strawgate avatar May 03 '25 11:05 strawgate

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())

jlowin avatar May 03 '25 13:05 jlowin

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.

strawgate avatar May 03 '25 15:05 strawgate

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.

jlowin avatar May 03 '25 15:05 jlowin

addressed feedback

strawgate avatar May 03 '25 15:05 strawgate

LGTM, thanks @strawgate! Made small push to get typing to pass and should be good to go

jlowin avatar May 03 '25 16:05 jlowin