ultraworkers/claw-code · error · std::io::Error
MCP stdio stream closed while reading line
Error message
MCP stdio stream closed while reading line
What it means
Client-side `McpStdioProcess::read_line` (runtime/src/mcp_stdio.rs:1214): reading a newline-delimited JSON-RPC line from the spawned MCP server's stdout returned 0 bytes — the child closed its stdout, normally because it exited. Unlike the server-side frame reader there is no clean-EOF path here: ANY EOF is `UnexpectedEof`, even after the server finished its work.
Source
Thrown at rust/crates/runtime/src/mcp_stdio.rs:1214
pub async fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {
self.stdin.write_all(bytes).await
}
pub async fn flush(&mut self) -> io::Result<()> {
self.stdin.flush().await
}
pub async fn write_line(&mut self, line: &str) -> io::Result<()> {
self.write_all(line.as_bytes()).await?;
self.write_all(b"\n").await?;
self.flush().await
}
pub async fn read_line(&mut self) -> io::Result<String> {
let mut line = String::new();
let bytes_read = self.stdout.read_line(&mut line).await?;
if bytes_read == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"MCP stdio stream closed while reading line",
));
}
Ok(line)
}
pub async fn read_available(&mut self) -> io::Result<Vec<u8>> {
let mut buffer = vec![0_u8; 4096];
let read = self.stdout.read(&mut buffer).await?;
buffer.truncate(read);
Ok(buffer)
}
pub async fn write_frame(&mut self, payload: &[u8]) -> io::Result<()> {
let encoded = encode_frame(payload);
self.write_all(&encoded).await?;
self.flush().awaitView on GitHub (pinned to 08106b0c37)
Solutions
- Run the server command manually with the same args/env from the MCP config and watch stderr — it almost always exits with a message.
- Fix the config: correct command/args, and add required env entries to the server's env map.
- Treat EOF from read_line as 'server exited', not a retryable IO hiccup — surface stderr instead of looping.
Example fix
# before (config: server exits immediately) # mcp config: npx -y @modelcontextprotocol/server-everything --bogus-flag # after (verify manually, then fix args) npx -y @modelcontextprotocol/server-everything --help # observe the real failure # mcp config: args = ["-y", "@modelcontextprotocol/server-everything"]
Defensive patterns
Strategy: fallback
Validate before calling
// smoke-test the server command exactly as configured before wiring it in // (run it, send nothing, confirm it stays alive and prints framed output on stdout) Command::new(&transport.command).args(&transport.args).envs(&transport.env).spawn()?
Try / catch
match process.read_line().await {
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
let _ = process.child.wait().await; // collect exit status
// report "MCP server <name> exited" with captured stderr; do NOT retry blindly
}
other => other,
} Prevention
- Run the server command manually with its config args/env before adding it to MCP config
- Put required secrets in the server's env map, not in the command line
- Treat stdout EOF as server exit — capture stderr to surface the real crash reason
When it happens
Trigger: The configured MCP server binary crashing on startup (bad args, missing env like an API key, unsupported Node version — npx wrapper exits immediately); the server exiting after an error; calling read_line after the server already shut down.
Common situations: `npx -y @some/mcp-server` where the package fails to resolve; server requiring an env var that was not passed in the MCP config `env` map; server printing a fatal error to stderr and exiting while the client is blocked reading stdout.
Related errors
- MCP stdio stream closed while reading headers
- MCP stdio stream closed while reading headers
- missing Content-Length header
- missing Content-Length header
- MCP response for {method} used unsupported jsonrpc version `
AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18).
Data as JSON: /api/errors/913b134f8219be34.
Report an issue: GitHub.