Hmbown/CodeWhale · critical
MCP server ' ': process closed stdin before answering
Error message
MCP server '{}': process closed stdin before answering {}{} What it means
When writing a JSON-RPC request to the MCP child's stdin fails with a broken-pipe error, the child process has already exited and closed its stdin. The client deliberately maps EPIPE to this message — appending `exit_note()` with the child's exit status — so that platform-dependent write-failure races (macOS reports the write error, Linux usually sees EOF first) surface as one consistent 'server died' error instead of a bare `Broken pipe`. This indicates the MCP server crashed or exited before responding.
Solutions
- Read the exit note appended to the message for the child's exit status and run the server command manually to see its startup error.
- Fix the server's crash: check for missing interpreter/runtime, bad arguments, or missing env vars in `McpServerConfig`.
- Increase any startup resources/limits if the child was OOM-killed.
- Recreate the connection/client after fixing the server — the pipe is dead and the connection is unusable.
Example fix
// before: command exits instantly (no shebang / wrong runtime)
{"name":"my-mcp","command":"./server.py","args":[]}
// after: invoke via the interpreter explicitly
{"name":"my-mcp","command":"python3","args":["/abs/path/server.py"]} Defensive patterns
Strategy: try-catch
Validate before calling
// before connecting, confirm the command starts and stays alive
let probe = Command::new(&config.command).args(&config.args).status()?;
if !probe.success() {
return Err("MCP command exits immediately; fix it before connecting".into());
} Try / catch
match conn.request("tools/list", (), timeout).await {
Err(e) if e.to_string().contains("closed stdin before answering") => {
eprintln!("MCP child crashed: {} — run the command manually to see the error", e);
// reconnect after fixing; the connection is dead
}
other => other,
} Prevention
- Run the server command standalone once before configuring it — catch missing interpreters and bad shebangs.
- Send all server startup diagnostics to stderr so crashes are visible in the exit note/logs.
- Monitor child exit codes and surface them next to connection errors.
- Use absolute paths and explicit runtimes to avoid environment-dependent startup failures.
When it happens
Trigger: `request(method, ...)` is called on a `Connection` whose child process has terminated; the serialized request write returns EPIPE, or the reader thread observed EOF first and this write loses the race.
Common situations: The MCP server binary panics or segfaults on startup; the command path resolves to a script that exits immediately (missing interpreter, bad shebang); the server was killed by OOM or an external signal; the server rejected the handshake and quit.
Related errors
- MCP server ' ': process closed stdin before answering
- child returned a malformed MCP CallToolResult
- child stdout was not valid UTF-8
- connection stdin poisoned by an earlier panic
- failed to read bounded stdio JSON-RPC frame
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/549329be49b17ef9.
Report an issue: GitHub.
Appendix: source
Thrown at crates/mcp/src/stdio_client.rs:875
params: Value,
timeout: Duration,
) -> Result<Value> {
let id = self.next_id;
self.next_id += 1;
if let Err(err) = self.send(&json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params
})) {
// A child that has already exited leaves us racing two symptoms of
// the same fact: either the reader thread sees EOF first, or our
// 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(ChildStdoutMessage::Line(line)) => line,
Ok(ChildStdoutMessage::Invalid(error)) => {
bail!(View on GitHub (pinned to 433685b202)