Description
Summary
When an MCP server returns a CallToolResult
object, it gets incorrectly serialized during stdio transport, resulting in a tuple format that causes Pydantic validation errors on the client side.
Environment
- Python Version: 3.11
- MCP SDK Version: 1.1.2
- Operating System: macOS Darwin 24.5.0
- Affected Component: mcp.server, mcp.client stdio transport
Description
MCP servers that return CallToolResult(content=[TextContent(type="text", text="...")])]
experience serialization issues where the object is converted to a tuple format during stdio communication between server and client.
Steps to Reproduce
- Create an MCP server with a tool that returns a CallToolResult:
@server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
result = await some_async_operation()
return CallToolResult(content=[TextContent(type="text", text=json.dumps(result))])
- Call the tool from an MCP client:
result = await session.call_tool("tool_name", {})
- Observe the validation error
Expected Behavior
The client should receive a properly deserialized CallToolResult
object with accessible content
attribute containing TextContent
objects.
Actual Behavior
The client receives a tuple in the format ('meta', None), ('content', [...]), ('isError', False)
which fails Pydantic validation with:
12 validation errors for CallToolResult
content.0.TextContent
Input should be a valid dictionary or instance of TextContent [type=model_type, input_value=('meta', None), input_type=tuple]
Error Details
Full error output shows multiple validation failures for different content types:
- TextContent
- ImageContent
- AudioContent
- EmbeddedResource
All fail with the same pattern of receiving tuples instead of proper objects.
Minimal Reproduction Example
Server (minimal_server.py):
#!/usr/bin/env python3
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent, CallToolResult
server = Server("test-server")
@server.list_tools()
async def list_tools():
return [Tool(name="test_tool", description="Test tool")]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> CallToolResult:
# This return statement causes the serialization issue
return CallToolResult(content=[TextContent(type="text", text="Hello from server")])
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
Client (minimal_client.py):
#!/usr/bin/env python3
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
server_params = StdioServerParameters(
command="python",
args=["minimal_server.py"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# This call triggers the serialization error
result = await session.call_tool("test_tool", {})
print(f"Result: {result}")
if __name__ == "__main__":
asyncio.run(main())
Workaround
Currently, we bypass the MCP protocol by directly importing and calling the server functions, which avoids the stdio serialization:
# Direct function call (works)
from server_module import handle_tool_directly
result = await handle_tool_directly("tool_name", {})
# Instead of MCP protocol call (fails)
result = await session.call_tool("tool_name", {})
Additional Context
- The issue appears to be in the stdio transport serialization/deserialization layer
- The server-side code creates proper CallToolResult objects
- The client receives tuples instead of the expected object format
- This affects any MCP server returning CallToolResult objects
Proposed Solution
The serialization/deserialization mechanism in the stdio transport should properly handle CallToolResult objects, maintaining their structure through the communication channel.
Impact
This bug prevents proper use of MCP servers that return structured results, requiring workarounds that bypass the MCP protocol entirely.