Hmbown/CodeWhale · error · anyhow::Error
MCP server '{server}': process closed stdin before answering
Error message
MCP server '{server}': process closed stdin before answering {method}{} What it means
While sending a JSON-RPC request (initialize, tools/list, tools/call, ...) to a stdio MCP server, the write to the child's stdin returned EPIPE: the server process had already exited. The message appends exit_note() — the child's exit status when reaped in time — which distinguishes a crashed server from a missing one ('exited with status 127' means the executable was not found). This is one of two platform-dependent symptoms of the same death; on Linux the EOF path (closed stdout) is the more common twin.
Source
Thrown at crates/mcp/src/stdio_client.rs:340
params: Value,
timeout: Duration,
) -> Result<Value> {
let id = self.next_id;
self.next_id += 1;
if let Err(err) = self.send(&json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params
})) {
// A child that has already exited leaves us racing two symptoms of
// the same fact: either the reader thread sees EOF first, or our
// write loses the race and returns EPIPE. Which one wins is
// platform- and timing-dependent (macOS reliably reports the write
// error where Linux reports the EOF), so both report the death the
// same way rather than leaking a bare "Broken pipe".
if is_broken_pipe(&err) {
bail!(
"MCP server '{server}': process closed stdin before answering {method}{}",
self.exit_note()
);
}
return Err(err)
.with_context(|| format!("MCP server '{server}': failed to send {method}"));
}
let deadline = Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
bail!("MCP server '{server}': {method} timed out after {timeout:?}");
}
let line = match self.responses.recv_timeout(remaining) {
Ok(line) => line,
Err(RecvTimeoutError::Timeout) => {
bail!("MCP server '{server}': {method} timed out after {timeout:?}");View on GitHub (pinned to 0c42157ee5)
Solutions
- Check the exit status in the message: 127 means the command could not be found — fix the command path or PATH
- Run the exact server command with its args manually and watch stderr (the crate pipes the child's stderr to its own, so run it directly to see startup errors)
- Verify the server actually implements the MCP stdio protocol and stays alive waiting on stdin
- Supply required env/args from McpServerConfig (env, args) instead of assuming an inherited environment
- For status-1 startups, look for missing credentials or invalid flags in the server's own error output
Example fix
// before
let config = McpServerConfig { name: "db".into(), command: "db-mcp".into(), ..Default::default() };
let client = StdioMcpClient::spawn(&config)?; // stdin closed, status 127
// after: absolute path + required env
let config = McpServerConfig {
name: "db".into(),
command: "/usr/local/bin/db-mcp".into(),
env: [("DB_DSN".into(), dsn)].into(),
..Default::default()
};
let client = StdioMcpClient::spawn(&config)?; Defensive patterns
Strategy: try-catch
Validate before calling
// Verify the command resolves before spawning:
let resolved = which::which(&config.command)
.with_context(|| format!("MCP command '{}' not found in PATH", config.command))?; Try / catch
match StdioMcpClient::spawn(&config) {
Ok(client) => Ok(client),
Err(err) => {
let msg = format!("{err:#}");
if msg.contains("status 127") || msg.contains("No such file") {
Err(err).context("MCP server executable missing; check command/PATH"))
} else {
Err(err).context("MCP server exited during startup; run it manually to see stderr")
}
}
} Prevention
- Smoke-test the server command with --help or a dry run before wiring it into MCP config
- Use absolute paths for commands in supervised environments
- Pass required env via McpServerConfig.env rather than relying on inherited environment
When it happens
Trigger: Server binary missing from PATH (status 127) or not executable; server crashes at startup (bad args, missing env, port conflict for a wrapper script); server exits mid-handshake because it does not actually speak MCP over stdio; server dies during a long tools/call.
Common situations: npx/node-based MCP servers on machines without node or with a corrupted npm cache; servers requiring env vars or credentials that are absent in the spawning environment; version upgrades where the server CLI changed flags and now exits immediately; scripts that print to stdout and quit instead of running a JSON-RPC loop.
Related errors
- MCP server '{server_name}' has no command configured
- MCP server '{server}': process closed stdout before answerin
- Stdio transport closed {stderr}
- Stdio transport closed
- app-server auth token cannot be empty
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/e8fb1c36ff1f215d.
Report an issue: GitHub.