8000 Bug Report: CallToolResult serialization fails with "Input should be a valid dictionary" error · Issue #987 · modelcontextprotocol/python-sdk · GitHub
[go: up one dir, main page]

Skip to content
Bug Report: CallToolResult serialization fails with "Input should be a valid dictionary" error #987
Open
@kadgodda

Description

@kadgodda

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

  1. 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))])
  1. Call the tool from an MCP client:
result = await session.call_tool("tool_name", {})
  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0