Hmbown/CodeWhale · error

Stdio transport closed

Error message

Stdio transport closed{exit}\n{stderr}

What it means

The stdio MCP transport detected EOF on the child's stdout and the child process has closed. The error reports the exit status (when retrievable) and appends the captured stderr tail so the operator learns why the child died — critical when the child dies before the MCP handshake completes, since the stderr tail is otherwise not retained.

Solutions

  1. Inspect the stderr tail and exit status embedded in the message.
  2. Run the configured MCP server command manually in a shell to reproduce the startup failure.
  3. Fix the command, args, or environment in the MCP server config.
  4. Check for signal kills (SIGKILL/SIGSEGV in the exit status) and address the underlying cause.

Example fix

// before: wrong command in MCP config
command = "npx"
args = ["mcp-server-filles"]  // typo -> child exits, EOF

// after
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
Defensive patterns

Strategy: try-catch

Try / catch

match transport.recv().await {
    Err(e) if e.to_string().starts_with("Stdio transport closed") => {
        // message carries exit status + stderr tail; fail fast with that context
        return Err(anyhow!("MCP server died before handshake: {e}"));
    }
    other => other?,
}

Prevention

When it happens

Trigger: recv() reads zero bytes (EOF) from the child's stdout and stderr context is available — child exited normally or crashed after startup, e.g. bad arguments, missing dependency, failed MCP initialize handshake.

Common situations: Typo'd command or wrong path in MCP server config so the child exits immediately; server crashes on startup due to bad env or missing API key; child killed by a signal (shown in the exit status).

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/16c89651277f9418. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/mcp/stdio.rs:354

            {
                Ok(b) => b,
                Err(err) => {
                    if let Some(stderr) = format_stderr_context(&self.stderr_tail).await {
                        anyhow::bail!("Stdio transport read error: {err}\n{stderr}");
                    }
                    return Err(err.into());
                }
            };
            if bytes == 0 {
                // Let the stderr drain task catch up before snapshotting, and
                // name the exit status: a reviewed plugin's stderr is never
                // retained, so the status is the only reason the operator
                // gets when the child dies before the handshake (#5916).
                tokio::task::yield_now().await;
                let exit = self.child.lock().await.try_wait().ok().flatten();
                let exit = exit.map_or_else(String::new, |status| format!(" ({status})"));
                if let Some(stderr) = format_stderr_context(&self.stderr_tail).await {
                    anyhow::bail!("Stdio transport closed{exit}\n{stderr}");
                }
                anyhow::bail!("Stdio transport closed{exit}");
            }

            let line_bytes = std::mem::take(&mut self.pending_line);
            let line = String::from_utf8_lossy(&line_bytes);
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }

            return Ok(trimmed.as_bytes().to_vec());
        }
    }

    /// Send SIGTERM and wait up to `STDIO_SHUTDOWN_GRACE` for graceful exit,
    /// then force termination and reap the child as the backstop.
    async fn shutdown(&mut self) {

View on GitHub (pinned to 73e0f67d83)