Repository navigation
Clients: fix what goes into a tool_result (Python, TypeScript, Go, Ruby) #273
Description
Problem
Three defects affect what the model receives after a tool call. All three were in every client. #205 fixed the first two in the Rust client, but the other four clients still have all three. The five clients are meant to be the same program in five languages, and right now only Rust handles these cases.
@DaleSeo raised the first two while reviewing #205 and posted a Rust patch in that thread. Copilot raised the third on #200.
-
A failed
tools/callends the query instead of going back to the model. When the server returns a JSON-RPC error,process_queryaborts. The user seesError: ..., and the model never learns that its call was rejected, so it can't correct itself.mcp-client-go/main.goreturns"", fmt.Errorf(...).- The Python, TypeScript and Ruby clients don't guard the call, so the error is raised into the chat loop.
The fix: send the error back as a
tool_resultwithis_errorset, and keep going. Everytool_usestill needs a matchingtool_result. -
structuredContentwithout a text block produces an emptytool_result. Each client joins only thetextblocks. The spec says a structured result SHOULD also include the serialized JSON in a text block. So a server that sends only structured content is unusual but legal, and these clients claim to work with any stdio server. When there's no text block, they should fall back to serializingstructuredContent. -
Non-text blocks are dropped without any warning. For the same reason, a tool that returns only an image or only a resource produces an empty
tool_result. The clients should:- convert
image(base64 + mimeType) to an Anthropic image block - turn text resources into text
- send an explicit placeholder for anything else, such as
[unsupported result type: audio]
Before Fix the tool-use loop in the Go, Rust, and Ruby clients and add a tool-loop smoke test #200, the TypeScript client forwarded the raw MCP block. That would have made the API call fail outright, rather than silently.
- convert
What the Rust client does now (copy this)
let tool_result = match session.call_tool(params).await {
Ok(result) => result,
// A rejected call is the model's mistake to correct, so it
// goes back as a failed tool_result rather than ending the
// query. Every tool_use still needs a matching result.
Err(error) => {
final_text.push(format!("[{name} failed: {error}]"));
tool_results.push(json!({
"type": "tool_result",
"tool_use_id": id,
"content": format!("Tool call {name} failed: {error}"),
"is_error": true,
}));
continue;
}
};// A structured result SHOULD also carry serialized JSON in a text
// block, but only SHOULD, so fall back to serializing it rather
// than sending the model an empty result.
if payload.is_empty() {
if let Some(structured) = &tool_result.structured_content {
payload = structured.to_string();
}
}Item 3 isn't done in Rust either, so it applies to all five clients.
Proposed mapping (same in all five)
text→ textimage→ base64 image block- text resource → text
- anything else →
[unsupported result type: <type>] - If the result has no MCP text block, append
structuredContentas JSON.
For item 1, Python, TypeScript and Ruby catch only the SDK's server-error class: MCPError, ProtocolError and MCP::Client::ServerError. Go and Rust catch any error from the call, as Rust already does.
Tasks
-
Fix items 1 and 2 in
mcp-client-python/client.py,mcp-client-typescript/index.ts,mcp-client-go/main.goandmcp-client-ruby/client.rb. -
Fix item 3 in all five clients, using the mapping above.
-
Add three scenarios to
tests/helpers/tool-loop-test.ts:- a call the server rejects at the protocol level
- a result with
structuredContentand no text block - a result with no text content
They need new tools on
tests/helpers/mock-mcp-server.ts. One of them must fail at the protocol level; throwing in thetools/callhandler is enough. -
Describe the new scenarios in
tests/README.mdand in the tool-loop comment intests/smoke-test.sh.
Put the fixes and the new test scenarios in the same PR. The tool-loop test runs against every client, so adding the scenarios before the clients are fixed would turn four passing steps red.
Acceptance criteria
- In all five clients, when a server rejects a call, the conversation continues and the model is told about the error.
- In all five clients, a result with only structured content reaches the model as JSON.
- The new tool-loop scenarios pass for all five clients.
Activity