Hmbown/CodeWhale · error
Stdio transport read error
Error message
Stdio transport read error: {err}\n{stderr} What it means
The stdio MCP transport failed while reading from the child process's stdout. When a captured stderr tail is available it is appended to the error so the operator sees the child's own diagnostic output alongside the I/O error.
Solutions
- Read the embedded stderr tail — it usually contains the child's panic or error message.
- Verify the MCP server command/args in config point to a working binary (run it manually).
- Check the child isn't being killed by OOM or a resource limit.
- Update or reinstall the MCP server package if the binary is corrupted.
Defensive patterns
Strategy: try-catch
Validate before calling
// before spawning, ensure the command resolves
which::which(&cfg.command).map_err(|_| format!("command not found: {}", cfg.command))?; Try / catch
match transport.recv().await {
Err(e) if e.to_string().starts_with("Stdio transport read error") => {
// stderr tail is embedded — surface it to the operator, then restart the child
log::error!("{e}");
transport = StdioTransport::spawn(&cfg).await?;
}
other => other?,
} Prevention
- Validate the MCP server command/args resolve before configuring them.
- Watch child stderr continuously and alert on panics.
- Set sane memory limits so the child isn't OOM-killed mid-response.
When it happens
Trigger: recv() awaiting the next length-prefixed message from the child's stdout hits an I/O error — child crashed mid-write, pipe broken, descriptor closed, or the process was killed.
Common situations: MCP server binary panics or OOM-killed mid-response; child writes invalid data then dies; OS pipe buffer/EOF issues; spawning with a bad command leaves no reader.
Related errors
- Stdio transport closed
- Stdio transport closed
- Stdio transport read error
- child returned a malformed MCP CallToolResult
- child stdout was not valid UTF-8
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/bc2a5589a3c316f6.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp/stdio.rs:340
Err(_) => false,
}
}
async fn recv(&mut self) -> Result<Vec<u8>> {
loop {
// 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 self.pending_line,
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 {
// 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}");
}
View on GitHub (pinned to 73e0f67d83)