Skip to content

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

Open

Description

@a-akimov

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.

  1. A failed tools/call ends the query instead of going back to the model. When the server returns a JSON-RPC error, process_query aborts. The user sees Error: ..., and the model never learns that its call was rejected, so it can't correct itself.

    • mcp-client-go/main.go returns "", 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_result with is_error set, and keep going. Every tool_use still needs a matching tool_result.

  2. structuredContent without a text block produces an empty tool_result. Each client joins only the text blocks. 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 serializing structuredContent.

  3. 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.

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 → text
  • image → base64 image block
  • text resource → text
  • anything else → [unsupported result type: <type>]
  • If the result has no MCP text block, append structuredContent as 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.go and mcp-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 structuredContent and 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 the tools/call handler is enough.

  • Describe the new scenarios in tests/README.md and in the tool-loop comment in tests/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

  1. added
    bugSomething isn't working
    javascriptPull requests that update javascript code
    rustPull requests that update rust code
    python:uvPull requests that update python:uv code
    goPull requests that update go code
    rubyPull requests that update ruby code
    on Nov 1, 2026
  2. self-assigned this
    on Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workinggoPull requests that update go codejavascriptPull requests that update javascript codepython:uvPull requests that update python:uv coderubyPull requests that update ruby coderustPull requests that update rust code

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions