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

  1. 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.
  2. Fix the server's crash: check for missing interpreter/runtime, bad arguments, or missing env vars in `McpServerConfig`.
  3. Increase any startup resources/limits if the child was OOM-killed.
  4. 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

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


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)