Allow alternative serialization to str when objects are returned
Right now there's a content formatter that gets called when tools return. Anything other than a string basically.
The content formatter just dumps it to JSON
It would be nice if this was customizable. At the moment I have to wrap all my tool return calls in a formatter call to return something other than compressed json
For example, I'd like to return yaml from tool calls as that tends to be readable for humans and for machines or alternatively maybe I'd like to return prettified json
Perhaps a configurable formatting callable that defaults to dumping json?
For now I'm doing:
# We need to wrap the underlying call in a way that captures the response and then calls the formatter.
def format_response(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(*args, **kwargs):
response = await func(*args, **kwargs)
return self.response_formatter(response)
return wrapper
# Register the tools with the MCP server.
mcp.add_tool(format_response(self.knowledge_base_client.create))
mcp.add_tool(format_response(self.knowledge_base_client.get))
I would really like to switch to using the mixin and the decorators and will try to figure out if I can do this probably by nesting the decorators
@jlowin for many clients it would be nice to dump responses to pretty yaml or pretty json instead of flattened JSON. Especially if this could be configured as an env var (though that could be part of what the user implements)
For example in Roo Code, responses w/ objects come as a single line of json which is probably fine for responses that only an LLM needs to see, but if it's desirable for the response to be usable by both the user and the LLM then the MCP author needs to perform their own serialization and return strings instead of objects.
It would be really nice if there was some convenient way to customize the response serialization if an object is returned.
For example:
vs
I could perhaps offer a contrib module for this, perhaps a decorator which goes before the FastMCP decorator that changes the return type but I think a decorator that changes the return type breaks function docs/type hints in the IDE (in vscode at least)
In this way, we can continue to write methods and functions which take complex inputs and return complex outputs but when used as a tool serialize appropriately for the use-case without requiring custom wrapping or MCP-specific versions of each method/function
I'd be happy to offer a PR to either:
- Allow the decorators to take a serializer parameter which the user provides
- Add a server-level setting for a serializer which effects all tools/resources registered with the server
if you were open to this
Hey @strawgate -- I'm not sure I'm totally following where the serializer should be injected.
In this code, I have a random tool that returns an object (for no other reason than because it doesn't have a nice serialization). If I call my_tool() as a user, I get the object; but if I simulate the call to the MCP, I get the serialized TextContent. Is that the behavior you meant? Or like an alternative serialization for the MCP call?
from fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool()
def my_tool():
return object()
await mcp._mcp_call_tool('my_tool', {})
# [TextContent(type='text', text='<object object at 0x12024f400>', annotations=None)]
my_tool()
# <object at 0x1034f2950>
When using Pydantic models
import asyncio
from fastmcp import FastMCP
from pydantic import BaseModel
mcp = FastMCP()
class CustomReturn(BaseModel):
"""Custom return type for the MCP server."""
message: str
@mcp.tool()
def my_tool():
return CustomReturn(message="test")
async def main():
await mcp._mcp_call_tool('my_tool', {})
# [TextContent(type='text', text='{"message": "test"}, annotations=None)]
my_tool()
# CustomReturn(message='test')
if __name__ == "__main__":
asyncio.run(main())
which is done by
def _convert_to_content(
result: Any,
_process_as_single_item: bool = False,
) -> list[TextContent | ImageContent | EmbeddedResource]:
"""Convert a result to a sequence of content objects."""
...
if not isinstance(result, str):
try:
result = json.dumps(pydantic_core.to_jsonable_python(result))
except Exception:
result = str(result)
return [TextContent(type="text", text=result)]
so it's this json.dumps call that would be nice to make a configurable serializer so that you can return objects from function/method calls and control how they get turned into text separately from modifying the functions themselves
I think this is closed by #308