zeroclaw-labs/zeroclaw · error · anyhow::Error

MCP `{op}` (server `{server_name}`) returned isError: {detai

Error message

MCP `{op}` (server `{server_name}`) returned isError: {detail}

What it means

MCP servers report tool-execution failures as a successful JSON-RPC response whose result object carries isError: true; check_result_is_error converts that envelope into an anyhow error carrying the server-supplied detail. This is how 'the tool ran but failed' (bad arguments, missing resource, internal tool error) reaches the caller, as opposed to protocol errors.

Source

Thrown at crates/zeroclaw-tools/src/mcp_client.rs:153

                .filter_map(|item| item.get("text").and_then(|t| t.as_str()))
                .collect::<Vec<_>>()
                .join("\n")
        })
        .filter(|s: &String| !s.is_empty())
        .unwrap_or_else(|| "(no error detail returned by server)".to_string());
    let detail = zeroclaw_providers::sanitize_api_error(&detail);
    ::zeroclaw_log::record!(
        WARN,
        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
            .with_outcome(::zeroclaw_log::EventOutcome::Failure)
            .with_attrs(::serde_json::json!({
                "mcp_server": server_name,
                "op": op,
                "detail": &detail,
            })),
        "mcp_client: MCP result returned isError:true"
    );
    bail!("MCP `{op}` (server `{server_name}`) returned isError: {detail}");
}

// ── Internal server state ──────────────────────────────────────────────────

struct McpServerInner {
    config: McpServerConfig,
    #[cfg(target_has_atomic = "64")]
    next_id: AtomicU64,
    #[cfg(not(target_has_atomic = "64"))]
    next_id: AtomicU32,
    tools: Vec<McpToolDef>,
    capabilities: McpServerCapabilities,
}

// ── Recovery barrier ────────────────────────────────────────────────────────

/// Synchronously-published gate that blocks new writes while a post-write
/// outcome-unknown request is being recovered.

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read the detail text — the server usually names the offending argument or resource
  2. Validate arguments against the tool's inputSchema from tools/list before calling
  3. If the server was upgraded, refresh the cached tool list; field names change between versions
  4. Retry only when the detail clearly indicates a transient downstream failure

Example fix

// before
let res = server.call_tool("read_file", json!({"path": p})).await?;

// after: match the tool's declared schema first
let schema = tool_input_schema(&server, "read_file").await?;
validate_args(&schema, &args)?;
let res = server.call_tool("read_file", args).await?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate args against the tool's declared input schema before calling
let tools = list_tools(&server).await?;
let tool = tools.iter().find(|t| t.name == name).context("unknown tool")?;
validate_against_schema(&args, &tool.input_schema)?;
let result = server.call_tool(name, args).await?;

Type guard

fn is_tool_is_error(err: &anyhow::Error) -> bool {
    err.to_string().contains("returned isError")
}

Try / catch

Catch, read the server-supplied detail, and branch: argument problems -> fix args and retry; missing resource -> skip or create it; transient downstream failure -> retry with backoff.

Prevention

When it happens

Trigger: tools/call with arguments violating the tool's input schema; referencing a file/URI the server cannot resolve; the tool's own downstream dependency failing (API key, missing binary); server-side exceptions surfaced as error text.

Common situations: Schema drift after a server upgrade renames a required field; passing strings where the tool expects numbers; paths that exist on the client but not on the server host.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/d5647727decf4f7f. Report an issue: GitHub.