Skip to content

Comments

fix: handle CallToolResult in _convert_to_content to prevent double-serialization#2136

Closed
maxisbey wants to merge 1 commit intomainfrom
claude/reproduce-issue-592-1DUtM
Closed

fix: handle CallToolResult in _convert_to_content to prevent double-serialization#2136
maxisbey wants to merge 1 commit intomainfrom
claude/reproduce-issue-592-1DUtM

Conversation

@maxisbey
Copy link
Contributor

Problem

When a tool returns a CallToolResult nested inside a list (e.g., return [CallToolResult(...)]), the _convert_to_content function in func_metadata.py would fall through to the pydantic_core.to_json fallback, serializing the entire CallToolResult object as a JSON string and wrapping it in a TextContent. This caused the client to receive:

TextContent(text='{"_meta": null, "content": [{"type": "text", "text": "42", ...}], "isError": false}')

instead of the actual content blocks from the CallToolResult:

TextContent(text='42')

The root cause is that CallToolResult is not a ContentBlock (which is TextContent | ImageContent | AudioContent | ...), so it would miss the isinstance(result, ContentBlock) guard and hit the generic JSON serializer.

Fix

Added an explicit guard in _convert_to_content for CallToolResult objects: when one is encountered, its .content blocks are extracted and flattened into the result sequence rather than being JSON-serialized.

The direct return case (a tool returning CallToolResult at the top level) was already correctly handled by the isinstance(result, CallToolResult) check in convert_result.

Tests

Added regression tests in tests/issues/test_592_call_tool_result_primitive.py covering:

  1. Tool returning CallToolResult directly with -> CallToolResult annotation
  2. Tool returning [CallToolResult(...)] (nested in a list) — this was the broken case

Fixes #592

…erialization

When a tool returns a CallToolResult nested inside a list (or any other
sequence), _convert_to_content would fall through to the pydantic_core.to_json
fallback, serializing the entire CallToolResult object as a JSON string and
wrapping it in a TextContent. This caused the client to receive:

  TextContent(text='{"_meta": null, "content": [...], "isError": false}')

instead of the actual content blocks from the CallToolResult.

The fix adds an explicit guard in _convert_to_content for CallToolResult objects:
when one is encountered, its content blocks are extracted and flattened into
the result sequence rather than being JSON-serialized.

The direct return case (tool returning CallToolResult at the top level) was
already correctly handled by the isinstance check in convert_result.

Reported-by: wilson-urdaneta
Github-Issue: #592
Reported-by: wilson-urdaneta
Github-Issue: #592
@maxisbey maxisbey marked this pull request as draft February 24, 2026 14:18
@maxisbey maxisbey closed this Feb 24, 2026
@maxisbey
Copy link
Contributor Author

just wanted to get claude to reproduce, didn't mean for it to actually post a pr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP stdio transport inconsistent CallTooResult return

2 participants