Hmbown/CodeWhale · error
Stdio transport closed
Error message
Stdio transport closed{exit}
{stderr} What it means
Thrown by the stdio MCP transport's `recv` when the child process's stdout reaches EOF (0 bytes read) — the server closed its output — and captured stderr is available. The message includes the child's exit status (via `try_wait`) and the stderr tail, which per issue #5916 is the only diagnostic when the child dies before the MCP handshake completes.
Solutions
- Read the stderr excerpt and exit status in the message — it names the child's startup or runtime failure.
- Verify the configured command/args run standalone in a terminal and exit 0 on a trivial request.
- Fix missing dependencies, wrong binary paths, or missing env vars the child needs.
- Restart the MCP server / reopen the transport once the underlying cause is fixed.
Example fix
// before: child exits immediately "command": "python3", "args": ["mcp_server.py"] // stderr: No module named 'mcp' // after "command": "python3", "args": ["-m", "pip", "install", "mcp"], // then relaunch "command": "python3", "args": ["mcp_server.py"]
Defensive patterns
Strategy: validation
Validate before calling
// Verify the command exists and starts before opening the transport which::which(&config.command)?; let probe = std::process::Command::new(&config.command).args(&config.args).stdin(std::process::Stdio::piped()).stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped()).spawn()?;
Prevention
- Run the MCP server command manually first; confirm it survives a trivial initialize handshake.
- Ensure all env vars, paths, and dependencies the child needs are set in the transport config.
- Read the stderr tail in the error before guessing at fixes.
When it happens
Trigger: `recv` reads 0 bytes from the child's stdout, yields once to let the stderr drain task catch up, snapshots the child's exit status, and finds non-empty stderr in `stderr_tail`.
Common situations: The MCP server command doesn't exist or fails to launch (module not found, bad interpreter); the child crashes on startup due to a config or env problem; the server exits after an unhandled error during a session.
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
- failed to read bounded stdio JSON-RPC frame
- Stdio transport read error
- child returned a malformed MCP CallToolResult
- child stdout was not valid UTF-8
- connection stdin poisoned by an earlier panic
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/953d9fe36c437e5b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp/stdio.rs:343
{
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 433685b202)