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
- Inspect the stderr tail and exit status embedded in the message.
- Run the configured MCP server command manually in a shell to reproduce the startup failure.
- Fix the command, args, or environment in the MCP server config.
- 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
- Smoke-test the exact configured command line in a shell before adding it to MCP config.
- Check MCP server env requirements (API keys, paths) at startup.
- Pin working versions of MCP server packages to avoid surprise startup breakage.
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
- Stdio transport closed
- Stdio transport closed
- Stdio transport closed
- Stdio transport read error
- child returned a malformed MCP CallToolResult
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)