Hmbown/CodeWhale · error · anyhow::Error
MCP server '{server}': process closed stdout before answerin
Error message
MCP server '{server}': process closed stdout before answering {method}{} What it means
The reader thread that drains the MCP server's stdout hit EOF, disconnecting the response channel while a request was outstanding: the child process closed stdout (died) before answering. exit_note() appends the reaped exit status where available. On Linux this is the usual symptom of a server that dies mid-request; on macOS the EPIPE/stdin variant tends to win the race — both describe the same death.
Source
Thrown at crates/mcp/src/stdio_client.rs:361
);
}
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 {
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}");
}View on GitHub (pinned to 0c42157ee5)
Solutions
- Read the exit status appended to the message, then the server's stderr (inherited) for its crash reason
- Fix the server-side crash: capture its panic/exit cause by running the same command and request manually
- If the server is killed for memory, raise the limit or make the tool stream/paginate instead of buffering
- Restart the server (re-register via register_server) and retry idempotent requests; never blindly retry a tools/call that may have had side effects
Defensive patterns
Strategy: try-catch
Validate before calling
null
Try / catch
match client_call(&client, tool, args) {
Ok(v) => Ok(v),
Err(err) if err.to_string().contains("closed stdout") => {
// server died mid-request: do NOT auto-retry non-idempotent tools
Err(err).context("MCP server died mid-request; check its stderr/exit status before retrying"))
}
Err(err) => Err(err),
} Prevention
- Only retry after a mid-request death when the tool is idempotent — the first call may have had side effects
- Monitor the server process (memory, OOM events) for recurring mid-flight deaths
- Re-register a fresh client (register_server) after any death before retrying
When it happens
Trigger: Server process crashes while executing a tools/call (panic, OOM kill, segfault); server calls exit() after an internal error; wrapper scripts (npx, docker) exiting and closing stdout; server killed externally (OOM reaper, container stop) during a long request.
Common situations: Memory-hungry MCP servers killed by the OOM killer mid-task; node servers dying on unhandled promise rejections; containers wrapped as MCP servers being stopped; servers with a fixed idle timeout that exit during long operations.
Related errors
- Stdio transport closed
- MCP server '{server}': process closed stdin before answering
- MCP server '{server}': {method} timed out after {timeout:?}
- Stdio transport closed {stderr}
- app-server auth token cannot be empty
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/8fa53522f91b130b.
Report an issue: GitHub.