Hmbown/CodeWhale · error · anyhow::Error

MCP server '{server}': process closed stdin before answering

Error message

MCP server '{server}': process closed stdin before answering {method}{}

What it means

While sending a JSON-RPC request (initialize, tools/list, tools/call, ...) to a stdio MCP server, the write to the child's stdin returned EPIPE: the server process had already exited. The message appends exit_note() — the child's exit status when reaped in time — which distinguishes a crashed server from a missing one ('exited with status 127' means the executable was not found). This is one of two platform-dependent symptoms of the same death; on Linux the EOF path (closed stdout) is the more common twin.

Source

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

        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(line) => line,
                Err(RecvTimeoutError::Timeout) => {
                    bail!("MCP server '{server}': {method} timed out after {timeout:?}");

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Check the exit status in the message: 127 means the command could not be found — fix the command path or PATH
  2. Run the exact server command with its args manually and watch stderr (the crate pipes the child's stderr to its own, so run it directly to see startup errors)
  3. Verify the server actually implements the MCP stdio protocol and stays alive waiting on stdin
  4. Supply required env/args from McpServerConfig (env, args) instead of assuming an inherited environment
  5. For status-1 startups, look for missing credentials or invalid flags in the server's own error output

Example fix

// before
let config = McpServerConfig { name: "db".into(), command: "db-mcp".into(), ..Default::default() };
let client = StdioMcpClient::spawn(&config)?; // stdin closed, status 127

// after: absolute path + required env
let config = McpServerConfig {
    name: "db".into(),
    command: "/usr/local/bin/db-mcp".into(),
    env: [("DB_DSN".into(), dsn)].into(),
    ..Default::default()
};
let client = StdioMcpClient::spawn(&config)?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify the command resolves before spawning:
let resolved = which::which(&config.command)
    .with_context(|| format!("MCP command '{}' not found in PATH", config.command))?;

Try / catch

match StdioMcpClient::spawn(&config) {
    Ok(client) => Ok(client),
    Err(err) => {
        let msg = format!("{err:#}");
        if msg.contains("status 127") || msg.contains("No such file") {
            Err(err).context("MCP server executable missing; check command/PATH"))
        } else {
            Err(err).context("MCP server exited during startup; run it manually to see stderr")
        }
    }
}

Prevention

When it happens

Trigger: Server binary missing from PATH (status 127) or not executable; server crashes at startup (bad args, missing env, port conflict for a wrapper script); server exits mid-handshake because it does not actually speak MCP over stdio; server dies during a long tools/call.

Common situations: npx/node-based MCP servers on machines without node or with a corrupted npm cache; servers requiring env vars or credentials that are absent in the spawning environment; version upgrades where the server CLI changed flags and now exits immediately; scripts that print to stdout and quit instead of running a JSON-RPC loop.

Related errors


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