Hmbown/CodeWhale · error

child returned a malformed MCP CallToolResult

Error message

child returned a malformed MCP CallToolResult

What it means

stdio_tool_call_result normalizes the child's response into a standard MCP CallToolResult. If the child claims a CallToolResult shape (looks_like_call_tool_result) but its contents fail valid_call_tool_result validation, the proxy refuses to pass it through. This keeps malformed results from propagating to callers that expect the MCP envelope contract.

Solutions

  1. Fix or update the MCP server so its CallToolResult conforms to the MCP spec (valid content array and required fields).
  2. Pin/downgrade or upgrade the server version to one matching the proxy's expected envelope shape.
  3. Capture the raw child response (the legacy nested 'result' path) and validate it manually to find the offending field before filing/fixing the server bug.
Defensive patterns

Strategy: type-guard

Validate before calling

fn result_is_conformant(v: &serde_json::Value) -> bool {
    v.get("content").and_then(|c| c.as_array()).map(|arr|
        arr.iter().all(|item| item.get("type").and_then(|t| t.as_str()).is_some()))
        .unwrap_or(false)
}

Type guard

fn is_valid_call_tool_result(fields: &serde_json::Map<String, serde_json::Value>) -> bool {
    valid_call_tool_result(fields)
}
// guard the child response before trusting it

Try / catch

match manager.call_tool(server, tool, args) {
    Err(e) if e.to_string().contains("malformed MCP CallToolResult") => {
        tracing::error!("server {server} returned non-conformant result: {e}");
        // capture raw response for a bug report; do not retry blindly
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling a tool over the stdio proxy where the child's JSON response object resembles a CallToolResult (has the recognizable fields) but contains invalid values per valid_call_tool_result (e.g., wrong-typed content entries or missing required fields).

Common situations: MCP server implementation bug emitting non-conformant content arrays; version drift where the child uses an older/newer CallToolResult shape; hand-rolled servers that emit the envelope keys with wrong types.

Understand the failure class

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/f7661e9938f6bea8. Report an issue: GitHub.

Appendix: source

Thrown at crates/mcp/src/lib.rs:1175

        "name": qualified_name.clone(),
        "inputSchema": input_schema,
        // Retain the pre-0.9.11 management fields for compatibility.
        "server_name": server_name,
        "tool_name": tool_name,
        "qualified_name": qualified_name,
    });
    if let Some(description) = description {
        value["description"] = Value::String(description);
    }
    value
}

fn stdio_tool_call_result(result: Value) -> Result<Value> {
    let legacy_result = result.clone();
    match result {
        Value::Object(mut fields) if looks_like_call_tool_result(&fields) => {
            if !valid_call_tool_result(&fields) {
                bail!("child returned a malformed MCP CallToolResult");
            }
            // The child already returned a standard MCP CallToolResult. Expose
            // it directly, while retaining the old nested result for clients
            // that used the proxy before its MCP envelope was corrected.
            fields.insert("result".to_string(), legacy_result);
            Ok(Value::Object(fields))
        }
        value => Ok(json!({
            "content": [{"type": "text", "text": legacy_value_text(&value)}],
            "result": legacy_result
        })),
    }
}

fn stdio_resource_descriptor((resource, metadata): (McpResourceDescriptor, Value)) -> Value {
    let McpResourceDescriptor {
        server_name,
        uri,

View on GitHub (pinned to 73e0f67d83)