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

  1. Read the stderr excerpt and exit status in the message — it names the child's startup or runtime failure.
  2. Verify the configured command/args run standalone in a terminal and exit 0 on a trivial request.
  3. Fix missing dependencies, wrong binary paths, or missing env vars the child needs.
  4. 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

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


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)