ultraworkers/claw-code · error · std::io::Error

MCP stdio stream closed while reading headers

Error message

MCP stdio stream closed while reading headers

What it means

Server-side MCP frame reader `read_frame` (runtime/src/mcp_server.rs:259): the header loop got at least one header line (`first_header == false`), then `read_line` returned 0 bytes — stdin closed in the middle of a Content-Length header block, before the blank line that terminates it. Clean EOF before ANY bytes returns `Ok(None)` instead; only mid-header EOF errors with `UnexpectedEof`. This is claw acting as an MCP server over stdio.

Source

Thrown at rust/crates/runtime/src/mcp_server.rs:259

        }),
    }
}

/// Reads a single LSP-framed JSON-RPC payload from `reader`.
///
/// Returns `Ok(None)` on clean EOF before any header bytes have been read,
/// matching how [`crate::mcp_stdio::McpStdioProcess`] treats stream closure.
async fn read_frame(reader: &mut BufReader<Stdin>) -> io::Result<Option<Vec<u8>>> {
    let mut content_length: Option<usize> = None;
    let mut first_header = true;
    loop {
        let mut line = String::new();
        let bytes_read = reader.read_line(&mut line).await?;
        if bytes_read == 0 {
            if first_header {
                return Ok(None);
            }
            return Err(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                "MCP stdio stream closed while reading headers",
            ));
        }
        first_header = false;
        if line == "\r\n" || line == "\n" {
            break;
        }
        let header = line.trim_end_matches(['\r', '\n']);
        if let Some((name, value)) = header.split_once(':') {
            if name.trim().eq_ignore_ascii_case("Content-Length") {
                let parsed = value
                    .trim()
                    .parse::<usize>()
                    .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
                content_length = Some(parsed);
            }
        }

View on GitHub (pinned to 08106b0c37)

Solutions

  1. Treat this as fatal for the connection: stop the read loop and let the server task shut down (the peer is gone).
  2. On the client side, write frames atomically and flush before exiting so headers are never split by termination.
  3. If you see it on every connect, capture the client's framing code — it is emitting truncated header blocks.

Example fix

// before (client, partial write then exit)
write_all(b"Content-Length: 42\r\n").await;
process::exit(0);              // server: stream closed while reading headers

// after (client writes the full frame, then closes)
write_all(format!("Content-Length: {len}\r\n\r\n").as_bytes()).await;
write_all(&body).await;
flush().await;                   // clean EOF later -> server gets Ok(None)
Defensive patterns

Strategy: fallback

Try / catch

match read_frame(&mut reader).await {
    Ok(None) => break,                                   // clean EOF before headers: session over
    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, // client died mid-header: end session, log at debug
    Err(e) => return Err(e),
    Ok(Some(frame)) => handle(frame).await?,
}

Prevention

When it happens

Trigger: MCP client process crashes or is SIGKILLed after writing partial headers; a client that writes a header line then closes stdin without finishing the frame; abrupt pipe teardown during session shutdown.

Common situations: IDE/editor restart killing the MCP client mid-write; test harnesses closing the pipe to signal shutdown after emitting a partial frame; OOM-killed clients.

Related errors


AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18). Data as JSON: /api/errors/c28eb0b2517de97f. Report an issue: GitHub.