Hmbown/CodeWhale · error

Stdio transport closed

Error message

Stdio transport closed

What it means

Stdout EOF with no stderr captured: the server process ended without writing anything to stderr (the ring-buffered tail was empty). Silent exits come from clean status-0 exits (e.g. --help left in args), SIGKILL/OOM kills that never get to print, or wrappers that daemonize and close stdout.

Source

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

            // Bounded read: a server emitting a newline-free multi-GB "line"
            // must not OOM us (read_line is unbounded).
            let bytes =
                match read_line_capped(&mut self.reader, &mut line_bytes, MAX_MCP_RESPONSE_BYTES)
                    .await
                {
                    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 {
                if let Some(stderr) = format_stderr_context(&self.stderr_tail).await {
                    anyhow::bail!("Stdio transport closed\n{stderr}");
                }
                anyhow::bail!("Stdio transport closed");
            }

            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 8880682c63)

Solutions

  1. Run the command manually and observe its exit code and output
  2. Check system logs (dmesg, journalctl) for OOM or signal kills of the child
  3. Remove flags like --help/--version from the server args
  4. Confirm the server is a foreground stdio MCP server, not a daemonizing wrapper
Defensive patterns

Strategy: try-catch

Type guard

```rust
fn is_silent_exit(msg: &str) -> bool {
    msg.trim() == "Stdio transport closed"
}
```

Try / catch

```rust
match stdio_transport.recv().await {
    Err(e) if is_silent_exit(&format!("{e:#}")) => {
        check_oom_and_signal_logs(); // silent exits: OOM kill, SIGKILL, or clean exit(0)
        Err(e)
    }
    other => other?,
}
```

Prevention

When it happens

Trigger: The child exits immediately with code 0 (argument parsing that prints to stdout only), is killed by SIGKILL/OOM before writing stderr, or forks to the background and closes its stdio.

Common situations: A --help/--version flag accidentally left in server args; OOM killer silently terminating the child; wrapper scripts that daemonize instead of staying in the foreground.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/3dbe67f9304f390f. Report an issue: GitHub.