The tool result conversion pipeline in base.py and function_tool.py mishandles several return value patterns, causing crashes or silent data loss.
1. Async generator tools return repr string instead of data
When a tool is an async generator (async def tool() -> AsyncIterator[str]: yield ...), FunctionTool.run() never consumes the generator. The async generator object itself is passed to _convert_to_content, which calls str() on it, producing "<async_generator object ... at 0x...>" as the tool output. No error is raised.
@mcp.tool
async def stream_data(count: int) -> AsyncIterator[str]:
for i in range(count):
yield f"item {i}"
result = await client.call_tool("stream_data", {"count": 3})
print(result.data) # None
print(result.content) # "<async_generator object ...>"
The fix: detect inspect.isasyncgen(result) after calling the function, and consume into a list.
2. Sync generator text/structured content inconsistency
Sync generators are consumed correctly for text content (Pydantic iterates them) but the resulting list produces inconsistent structured content — result.data is None while text content has the values.
@mcp.tool
def gen_tool() -> list:
return (x for x in [1, 2, 3])
r = await client.call_tool_mcp("gen_tool", {})
print(r.content[0].text) # "[1,2,3]"
print(r.structuredContent) # {"result": []} — empty!
The generator is consumed during text serialization and exhausted by the time structured content is built. Fix: materialize generators before the dual processing pipeline.
3. Non-UTF8 bytes crash serialization
Tools returning bytes with non-UTF8 content crash with PydanticSerializationError: invalid utf-8 sequence. The tool returns an error response instead of encoding the binary data.
@mcp.tool()
def get_binary() -> bytes:
return bytes(range(256))
# PydanticSerializationError: invalid utf-8 sequence of 1 bytes from index 128
Fix: catch PydanticSerializationError in default_serializer for bytes and fall back to base64 encoding, or handle bytes specially in _convert_to_content.
4. None return with typed annotation crashes client
When a tool declares -> str but returns None at runtime, the server wraps None into structuredContent as {"result": null}. The MCP SDK client rejects this because null doesn't match the string output schema.
@mcp.tool()
def lookup(key: str) -> str:
return {"name": "Alice"}.get(key) # None for missing keys
# Client: RuntimeError: Invalid structured content returned by tool
5. Custom output_schema with non-dict return crashes
When a tool specifies a custom output_schema dict but returns a non-dict value (string, int, list), convert_result raises ValueError: structured_content must be a dict or None at runtime. The auto-generated output_schema auto-wraps via x-fastmcp-wrap-result, but custom schemas don't.
@mcp.tool(output_schema={"type": "object", "properties": {"msg": {"type": "string"}}})
def broken() -> str:
return "hello"
# ValueError: structured_content must be a dict or None
🤖 Generated with Claude Code
The tool result conversion pipeline in
base.pyandfunction_tool.pymishandles several return value patterns, causing crashes or silent data loss.1. Async generator tools return repr string instead of data
When a tool is an async generator (
async def tool() -> AsyncIterator[str]: yield ...),FunctionTool.run()never consumes the generator. The async generator object itself is passed to_convert_to_content, which callsstr()on it, producing"<async_generator object ... at 0x...>"as the tool output. No error is raised.The fix: detect
inspect.isasyncgen(result)after calling the function, and consume into a list.2. Sync generator text/structured content inconsistency
Sync generators are consumed correctly for text content (Pydantic iterates them) but the resulting list produces inconsistent structured content —
result.dataisNonewhile text content has the values.The generator is consumed during text serialization and exhausted by the time structured content is built. Fix: materialize generators before the dual processing pipeline.
3. Non-UTF8 bytes crash serialization
Tools returning
byteswith non-UTF8 content crash withPydanticSerializationError: invalid utf-8 sequence. The tool returns an error response instead of encoding the binary data.Fix: catch
PydanticSerializationErrorindefault_serializerfor bytes and fall back to base64 encoding, or handle bytes specially in_convert_to_content.4. None return with typed annotation crashes client
When a tool declares
-> strbut returnsNoneat runtime, the server wrapsNoneintostructuredContentas{"result": null}. The MCP SDK client rejects this becausenulldoesn't match thestringoutput schema.5. Custom output_schema with non-dict return crashes
When a tool specifies a custom
output_schemadict but returns a non-dict value (string, int, list),convert_resultraisesValueError: structured_content must be a dict or Noneat runtime. The auto-generated output_schema auto-wraps viax-fastmcp-wrap-result, but custom schemas don't.🤖 Generated with Claude Code