Describe the bug
When using the MCP stdio transport (mcp.client.stdio.stdio_client and mcp.client.session.ClientSession), if a server tool returns a CallToolResult containing TextContent where the text field holds the string representation of a primitive type (string, number, boolean, null), the client-side ClientSession incorrectly populates the resulting TextContent.text attribute. Instead of containing the simple string representation (e.g., "42"), it contains the full JSON string representation of the entireCallToolResult object (e.g., '{"_meta": null, "content": [{"type": "text", "text": "42", "annotations": null}], "isError": false}').
This issue does not seem to affect cases where the server returns complex types (dicts, lists) serialized as JSON strings within TextContent.text.
To Reproduce
Steps to reproduce the behavior:
Create an MCP stdio server (stdio_server_script.py) with a simple tool that returns a primitive type within TextContent:
# stdio_server_script.pyimportasyncioimportjsonimportloggingimportsysfromtypingimportAnyfrommcp.server.fastmcpimportFastMCP, Contextfrommcp.typesimportCallToolResult, TextContentlogging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
server=FastMCP("TestStdioServer", log_level="DEBUG")
@server.tool("echo_primitive", description="Echo back primitive as string")asyncdefecho_primitive(context: Context, message: Any=None) ->CallToolResult:
text_value=str(message) ifmessageisnotNoneelse"null"logging.debug(f"Echo returning primitive text: {text_value}")
# Attempt to return primitive string representation in TextContentreturnCallToolResult(content=[TextContent(type="text", text=text_value)])
asyncdefmain():
awaitserver.run_stdio_async()
if__name__=="__main__":
asyncio.run(main())Create an MCP client that connects via stdio and calls the tool:
# stdio_client_test.pyimportasyncioimportsysimportjsonimportloggingfrommcp.client.sessionimportClientSessionfrommcp.client.stdioimportStdioServerParameters, stdio_clientfrommcp.typesimportTextContentlogging.basicConfig(level=logging.DEBUG)
asyncdefrun_test():
server_script="stdio_server_script.py"params=StdioServerParameters(command=sys.executable, args=[server_script])
asyncwithstdio_client(params) asstreams:
read_stream, write_stream=streamsasyncwithClientSession(read_stream, write_stream) assession:
awaitasyncio.wait_for(session.initialize(), timeout=5.0)
awaitasyncio.sleep(1.0) # Ensure initialization completesinput_value=42expected_text="42"result=awaitsession.call_tool("echo_primitive", {"message": input_value})
logging.info(f"Received result: {result}")
assertresult.contentandlen(result.content) >0text_content=result.content[0]
assertisinstance(text_content, TextContent)
actual_text=text_content.textlogging.info(f"Input value: {input_value}")
logging.info(f"Expected text: '{expected_text}'")
logging.info(f"Actual text in text_content.text: '{actual_text}'")
# This assertion failsassertactual_text==expected_text, f"Assertion Failed: Expected '{expected_text}', got '{actual_text}'"if__name__=="__main__":
asyncio.run(run_test())Run the client script: python stdio_client_test.py
See error: Observe the logged output and the AssertionError. The actual_text will contain the full JSON string, not just "42".
# Example Output Snippet
INFO:root:Received result: CallToolResult(_meta=None, content=[TextContent(type='text', text='{"_meta": null, "content": [{"type": "text", "text": "42", "annotations": null}], "isError": false}', annotations=None)], isError=False)
INFO:root:Input value: 42
INFO:root:Expected text: '42'
INFO:root:Actual text in text_content.text: '{"_meta": null, "content": [{"type": "text", "text": "42", "annotations": null}], "isError": false}'
...
AssertionError: Assertion Failed: Expected '42', got '{"_meta": null, "content": [{"type": "text", "text": "42", "annotations": null}], "isError": false}'
Expected behavior
The text_content.text attribute on the client side should contain the exact string value that the server placed in the TextContent's text field when returning a primitive type. In the example above, actual_text should be equal to "42".
Screenshots
N/A (See console output above).
Desktop (please complete the following information):
- OS: macOS Sonoma 14.4 (Darwin 24.4.0)
- Python Version: 3.10.17
mcp Library Version: 1.6.0 (Assuming based on project instructions)
Smartphone (please complete the following information):
N/A
Additional context
- This bug forces clients using the stdio transport to implement a workaround where they check if
TextContent.text contains a JSON string representing the full response and parse it manually to extract the actual intended text field. - The issue seems specific to the
stdio transport combined with primitive return types in TextContent. Returning complex types (dict/list) as JSON strings works as expected (client receives the JSON string in TextContent.text and can parse it). Returning primitives via SSE transport might behave differently (needs verification).
Describe the bug
When using the MCP stdio transport (
mcp.client.stdio.stdio_clientandmcp.client.session.ClientSession), if a server tool returns aCallToolResultcontainingTextContentwhere thetextfield holds the string representation of a primitive type (string, number, boolean, null), the client-sideClientSessionincorrectly populates the resultingTextContent.textattribute. Instead of containing the simple string representation (e.g.,"42"), it contains the full JSON string representation of the entireCallToolResultobject (e.g.,'{"_meta": null, "content": [{"type": "text", "text": "42", "annotations": null}], "isError": false}').This issue does not seem to affect cases where the server returns complex types (dicts, lists) serialized as JSON strings within
TextContent.text.To Reproduce
Steps to reproduce the behavior:
Create an MCP stdio server (
stdio_server_script.py) with a simple tool that returns a primitive type withinTextContent:Create an MCP client that connects via stdio and calls the tool:
Run the client script:
python stdio_client_test.pySee error: Observe the logged output and the
AssertionError. Theactual_textwill contain the full JSON string, not just"42".Expected behavior
The
text_content.textattribute on the client side should contain the exact string value that the server placed in theTextContent'stextfield when returning a primitive type. In the example above,actual_textshould be equal to"42".Screenshots
N/A (See console output above).
Desktop (please complete the following information):
mcpLibrary Version: 1.6.0 (Assuming based on project instructions)Smartphone (please complete the following information):
N/A
Additional context
TextContent.textcontains a JSON string representing the full response and parse it manually to extract the actual intended text field.stdiotransport combined with primitive return types inTextContent. Returning complex types (dict/list) as JSON strings works as expected (client receives the JSON string inTextContent.textand can parse it). Returning primitives via SSE transport might behave differently (needs verification).