Hmbown/CodeWhale · error · anyhow::Error

MCP server '{server}': {method} failed: {error}

Error message

MCP server '{server}': {method} failed: {error}

What it means

The MCP server answered the request with a JSON-RPC error object, and the client surfaces it verbatim: 'MCP server '<name>': <method> failed: <error>'. This is a server-side rejection, not a transport failure — the protocol worked. The embedded error is the server's own message/code (e.g. 'Tool not found', 'Invalid params', 'Internal error').

Source

Thrown at crates/mcp/src/stdio_client.rs:378

                Err(RecvTimeoutError::Disconnected) => {
                    bail!(
                        "MCP server '{server}': process closed stdout before answering {method}{}",
                        self.exit_note()
                    );
                }
            };

            // Servers occasionally emit banners or log lines on stdout, and
            // notifications carry no id. Both are skipped; only the matching
            // response ends the wait.
            let Ok(message) = serde_json::from_str::<Value>(&line) else {
                continue;
            };
            if message.get("id").and_then(Value::as_u64) != Some(id) {
                continue;
            }
            if let Some(error) = message.get("error") {
                bail!("MCP server '{server}': {method} failed: {error}");
            }
            return Ok(message.get("result").cloned().unwrap_or(Value::Null));
        }
    }

    /// The child's exit status, when it has one, for appending to a failure
    /// message. Polls briefly because both callers run at the moment the child
    /// is dying: the write can return EPIPE, or stdout can hit EOF, before the
    /// kernel has finished reaping the process. Bounded and error-path-only,
    /// so the cost buys a real diagnostic ("exited with status 127" is the
    /// difference between a crashed server and a missing one).
    fn exit_note(&mut self) -> String {
        let deadline = Instant::now() + EXIT_STATUS_GRACE;
        loop {
            match self.child.try_wait() {
                Ok(Some(status)) => return format!(" (process exited with {status})"),
                Ok(None) if Instant::now() < deadline => {
                    thread::sleep(Duration::from_millis(5));

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Read the embedded error object — its code and message come from the server and name the actual problem
  2. For unknown-tool errors, list current tools (list_tools) and use the exact advertised name
  3. For invalid-params errors, match your arguments to the tool's inputSchema from the descriptor
  4. For credential/quota errors, fix them on the server's configuration side, then retry

Example fix

// before
let v = registry.call_tool("github", "search_code", json!({"q": "repo:x y"}))?;

// after: inspect the server's error and correct the call
match registry.call_tool("github", "search_code", json!({"q": "repo:x y"})) {
    Err(e) if e.to_string().contains("failed") => {
        let tools = registry.list_tools()?; // verify the tool name and its inputSchema
        tracing::warn!("server rejected call: {e:#}");
    }
    other => other?,
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify the tool exists with the exact advertised name before calling:
let names: Vec<String> = registry.list_tools()?
    .into_iter().map(|t| t.tool_name).collect();
if !names.contains(&tool.to_string()) { bail!("unknown tool {tool}"); }

Try / catch

match registry.call_tool(server, tool, args) {
    Ok(v) => Ok(v),
    Err(err) if err.to_string().contains("failed:") => {
        // server-side JSON-RPC error: surface its message; fix args/name based on it
        Err(err)
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: Calling a tool name the server does not implement; sending arguments that fail the server's schema validation; the tool's internal operation failing (auth expired on the server's side, upstream API error); the server rejecting the initialize params.

Common situations: Tool renamed or removed in a new server version while callers still use the old name; argument schemas drifted between client and server; expired credentials inside the server (GitHub token, DB password); rate limits or quota errors surfaced by the underlying API the tool wraps.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/cd496bfb9aaba2b3. Report an issue: GitHub.