Hmbown/CodeWhale · error · anyhow::Error
MCP server '{server}': {method} timed out after {timeout:?}
Error message
MCP server '{server}': {method} timed out after {timeout:?} What it means
A JSON-RPC request to a stdio MCP server exceeded its deadline: the loop recomputed the remaining time before waiting on the response channel and found it already exhausted. The timeout value in the message is the per-call bound — 30s (HANDSHAKE_TIMEOUT) for the initialize handshake and 120s (REQUEST_TIMEOUT) for regular requests like tools/call. Lines without the matching JSON-RPC id (banners, log lines, notifications) are skipped and do not reset the clock, so a server that never answers with the right id will always time out.
Source
Thrown at crates/mcp/src/stdio_client.rs:353
// write loses the race and returns EPIPE. Which one wins is
// platform- and timing-dependent (macOS reliably reports the write
// error where Linux reports the EOF), so both report the death the
// same way rather than leaking a bare "Broken pipe".
if is_broken_pipe(&err) {
bail!(
"MCP server '{server}': process closed stdin before answering {method}{}",
self.exit_note()
);
}
return Err(err)
.with_context(|| format!("MCP server '{server}': failed to send {method}"));
}
let deadline = Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
bail!("MCP server '{server}': {method} timed out after {timeout:?}");
}
let line = match self.responses.recv_timeout(remaining) {
Ok(line) => line,
Err(RecvTimeoutError::Timeout) => {
bail!("MCP server '{server}': {method} timed out after {timeout:?}");
}
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 {View on GitHub (pinned to 0c42157ee5)
Solutions
- If the tool is legitimately slow, restructure the work (batch smaller, run async server-side) or use spawn_with_timeouts via the crate's testing seam to raise the budget
- Run the server manually and time the same request to distinguish 'slow' from 'hung'
- Verify the server copies the request id into its response — responses with wrong/missing ids are silently skipped and guarantee a timeout
- For npx-style servers, pre-install the package so the handshake does not race package download
Defensive patterns
Strategy: retry
Validate before calling
null
Try / catch
let mut attempt = 0;
loop {
attempt += 1;
match client_call(&client, tool, args) {
Ok(v) => break Ok(v),
Err(err) if err.to_string().contains("timed out") && attempt < 2 => {
tokio::time::sleep(Duration::from_secs(2)).await; // server may be slow, not dead
}
Err(err) => break Err(err),
}
} Prevention
- Measure real tool latency and keep calls well under the 120s request budget
- Pre-install npx/package-based servers so the 30s handshake never races a download
- Confirm the server echoes request ids; id-less responses are skipped and guarantee timeouts
When it happens
Trigger: A tools/call that legitimately runs longer than 120s; a server that hangs (deadlock, waiting on a network resource); a server that replies with a different or missing id so its responses are skipped as noise; initialize taking over 30s on a cold npx install that downloads dependencies first.
Common situations: Long-running tools (bulk scans, codebase indexing, remote builds) exceeding the fixed 120s budget; first-run npx-based servers compiling/installing during the 30s handshake; servers behind a slow network dependency; buggy servers that echo the wrong id.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- MCP server '{server}': process closed stdout before answerin
- app-server auth token cannot be empty
- MCP server '{server_name}' has no command configured
- MCP server '{server}': process closed stdin before answering
- MCP server '{server}': {method} failed: {error}
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/da0ee7d9fe4d2715.
Report an issue: GitHub.