Repository navigation
Conversation
Three defects decided what the model received after a tool call: 1. A tools/call the server rejected with a JSON-RPC error ended the query in the Python, TypeScript, Go and Ruby clients, so the model never learned its call failed. It now goes back as a tool_result with is_error set, as the Rust client already did. 2. A result with structuredContent and no text block reached the model as an empty tool_result in the same four clients. Its JSON is now sent. 3. Non-text blocks were dropped in all five clients. Images in a type the Messages API accepts become base64 image blocks, text resources become their text, and anything else becomes a placeholder such as "[unsupported result type: audio]". All five clients use the same mapping. Python, TypeScript and Ruby catch only the SDK's server-error class (MCPError, ProtocolError, MCP::Client::ServerError); Go and Rust catch any error from the call. The tool-loop test gains a fourth query, "tool result contents", backed by three new mock-server tools (rejected_call, structured_only, non_text). The unfixed Python client fails it; all five fixed clients pass. Closes #273 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkL34oWhufSSqj9FbPx5dr
There was a problem hiding this comment.
🟡 Changes recommended
All five clients forward empty MCP text blocks, including those from empty text resources, which the Messages API rejects and causes follow-up requests to fail.
5 open issues
Skip empty strings when converting MCP text content · New Filter empty MCP text blocks before Messages API requests · New Filter empty MCP text blocks before Messages API requests · New Filter empty MCP text blocks before Messages API requests · New Filter empty MCP text blocks before Messages API requests · New
What changed in this PR
This PR addresses #273 by aligning how all five quickstart clients translate MCP tool results for the Anthropic Messages API.
Changes:
- Returns rejected tool calls to the model as failed tool results instead of ending the query.
- Forwards structured-only results as JSON and converts non-text content into supported blocks or explicit placeholders.
- Adds a fourth tool-loop scenario, shared fixtures, and documentation for the new behavior.
| File | Description |
|---|---|
tests/smoke-test.sh |
Updates comments to describe the fourth tool-loop query. |
tests/README.md |
Documents the new scenarios and mock tools. |
tests/helpers/tool-loop-test.ts |
Checks rejected calls and translated result content. |
tests/helpers/mock-values.ts |
Defines shared expected result values. |
tests/helpers/mock-mcp-server.ts |
Adds three tools exercising result translation. |
mcp-client-typescript/README.md |
Documents result mapping and rejected-call handling. |
mcp-client-typescript/index.ts |
Adds content conversion and protocol-error forwarding. |
mcp-client-rust/src/main.rs |
Extends result conversion to non-text blocks. |
mcp-client-rust/README.md |
Documents the unified result mapping. |
mcp-client-ruby/README.md |
Documents result mapping and rejected-call handling. |
mcp-client-ruby/client.rb |
Adds content conversion and server-error forwarding. |
mcp-client-python/README.md |
Documents result mapping and rejected-call handling. |
mcp-client-python/client.py |
Adds content conversion and MCP-error forwarding. |
mcp-client-go/README.md |
Documents result mapping and rejected-call handling. |
mcp-client-go/main.go |
Adds content conversion and failed-call forwarding. |
Review effort: Balanced
| func toolResultContent(result *mcp.CallToolResult) []anthropic.ToolResultBlockParamContentUnion { | ||
| var content []anthropic.ToolResultBlockParamContentUnion | ||
| text := func(s string) { | ||
| content = append(content, anthropic.ToolResultBlockParamContentUnion{OfText: &anthropic.TextBlockParam{Text: s}}) |
| if result.structured_content is not None and not any(isinstance(block, TextContent) for block in result.content): | ||
| content.append({"type": "text", "text": json.dumps(result.structured_content)}) | ||
|
|
||
| return content |
| content << { type: "text", text: structured.to_json } | ||
| end | ||
|
|
||
| content |
| content.push(json!({"type": "text", "text": structured.to_string()})); | ||
| } | ||
|
|
||
| content |
| content.push({ type: "text", text: JSON.stringify(result.structuredContent) }); | ||
| } | ||
|
|
||
| return content; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Motivation and Context
Closes #273.
Three defects decided what the model received after a tool call:
tools/callthe server rejected with a JSON-RPC error ended the query in the Python, TypeScript, Go and Ruby clients, so the model never learned its call failed. It now goes back as atool_resultwithis_errorset, as the Rust client already did.structuredContentand no text block reached the model as an emptytool_resultin the same four clients. Its JSON is now sent.All five clients use the same mapping. Python, TypeScript and Ruby catch only the SDK's server-error class (
MCPError,ProtocolError,MCP::Client::ServerError); Go and Rust catch any error from the call.The tool-loop test gains a fourth query, "tool result contents", backed by three new mock-server tools (
rejected_call,structured_only,non_text). The unfixed Python client fails it; all five fixed clients pass.How Has This Been Tested?
Locally.
Breaking Changes
N/a.
Types of changes
Checklist
Additional context
Assisted by Claude Code, reviewed by me.