Skip to content

Fix what the clients put in a tool_result - #279

Open
a-akimov wants to merge 2 commits into
mainfrom
fix/tool-result-contents
Open

a-akimov wants to merge 2 commits into
mainfrom
fix/tool-result-contents

Conversation

@a-akimov

@a-akimov a-akimov commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

Closes #273.

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.

How Has This Been Tested?

Locally.

Breaking Changes

N/a.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Assisted by Claude Code, reviewed by me.

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
Copilot AI balanced review requested due to automatic review settings November 2, 2026 07:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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
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

Comment thread mcp-client-go/main.go
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
Comment thread mcp-client-ruby/client.rb
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;
Copilot AI balanced review requested due to automatic review settings November 2, 2026 21:16
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.

Clients: fix what goes into a tool_result (Python, TypeScript, Go, Ruby)

2 participants