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

MCP ` ` (server ` `) returned isError

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.

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.

Appendix: 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)