Hmbown/CodeWhale · error
Stdio transport closed
Error message
Stdio transport closed{exit} What it means
Variant of the stdio transport closed error when no stderr context could be captured: the child's stdout hit EOF and it exited, but its stderr tail was empty or unavailable. Only the exit status (if retrievable) is reported.
Solutions
- Check the exit status in the message for a signal or nonzero code.
- Run the MCP server command manually with stderr visible to see its output.
- Verify the configured command/args and environment.
- If the child was killed, check ulimits, cgroup limits, or supervisor policies.
Defensive patterns
Strategy: try-catch
Try / catch
Err(e) if e.to_string().starts_with("Stdio transport closed") && !e.to_string().contains('\n') => {
// no stderr captured: reproduce manually with stderr visible
return Err(anyhow!("MCP server exited silently: {e}; run the command manually to see stderr"));
} Prevention
- Always capture child stderr so silent exits are diagnosable.
- Run silent-exiting commands manually with stderr attached to find the cause.
- Check for signal kills via the exit status shown in the message.
When it happens
Trigger: Same as the stderr-annotated variant — EOF on child stdout with child exited — but format_stderr_context returned None (empty stderr tail).
Common situations: Child exits silently (e.g. killed by signal before writing anything); server binary writes nothing to stderr; stderr pipe not captured for that spawn mode.
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/6c732e21f2d6cedd.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp/stdio.rs:356
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) {
let mut child = self.child.lock().await;
terminate_child(&mut child).await;View on GitHub (pinned to 73e0f67d83)