{"record":{"id":"e8fb1c36ff1f215d","repo":"Hmbown/CodeWhale","slug":"mcp-server-server-process-closed-stdin-before","errorCode":null,"errorMessage":"MCP server '{server}': process closed stdin before answering {method}{}","messagePattern":"MCP server '(.+?)': process closed stdin before answering (.+?)(.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/mcp/src/stdio_client.rs","lineNumber":340,"sourceCode":"        params: Value,\n        timeout: Duration,\n    ) -> Result<Value> {\n        let id = self.next_id;\n        self.next_id += 1;\n        if let Err(err) = self.send(&json!({\n            \"jsonrpc\": \"2.0\",\n            \"id\": id,\n            \"method\": method,\n            \"params\": params\n        })) {\n            // A child that has already exited leaves us racing two symptoms of\n            // the same fact: either the reader thread sees EOF first, or our\n            // write loses the race and returns EPIPE. Which one wins is\n            // platform- and timing-dependent (macOS reliably reports the write\n            // error where Linux reports the EOF), so both report the death the\n            // same way rather than leaking a bare \"Broken pipe\".\n            if is_broken_pipe(&err) {\n                bail!(\n                    \"MCP server '{server}': process closed stdin before answering {method}{}\",\n                    self.exit_note()\n                );\n            }\n            return Err(err)\n                .with_context(|| format!(\"MCP server '{server}': failed to send {method}\"));\n        }\n\n        let deadline = Instant::now() + timeout;\n        loop {\n            let remaining = deadline.saturating_duration_since(Instant::now());\n            if remaining.is_zero() {\n                bail!(\"MCP server '{server}': {method} timed out after {timeout:?}\");\n            }\n            let line = match self.responses.recv_timeout(remaining) {\n                Ok(line) => line,\n                Err(RecvTimeoutError::Timeout) => {\n                    bail!(\"MCP server '{server}': {method} timed out after {timeout:?}\");","sourceCodeStart":322,"sourceCodeEnd":358,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/mcp/src/stdio_client.rs#L322-L358","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet config = McpServerConfig { name: \"db\".into(), command: \"db-mcp\".into(), ..Default::default() };\nlet client = StdioMcpClient::spawn(&config)?; // stdin closed, status 127\n\n// after: absolute path + required env\nlet config = McpServerConfig {\n    name: \"db\".into(),\n    command: \"/usr/local/bin/db-mcp\".into(),\n    env: [(\"DB_DSN\".into(), dsn)].into(),\n    ..Default::default()\n};\nlet client = StdioMcpClient::spawn(&config)?;","handlingStrategy":"try-catch","validationCode":"// Verify the command resolves before spawning:\nlet resolved = which::which(&config.command)\n    .with_context(|| format!(\"MCP command '{}' not found in PATH\", config.command))?;","typeGuard":null,"tryCatchPattern":"match StdioMcpClient::spawn(&config) {\n    Ok(client) => Ok(client),\n    Err(err) => {\n        let msg = format!(\"{err:#}\");\n        if msg.contains(\"status 127\") || msg.contains(\"No such file\") {\n            Err(err).context(\"MCP server executable missing; check command/PATH\"))\n        } else {\n            Err(err).context(\"MCP server exited during startup; run it manually to see stderr\")\n        }\n    }\n}","preventionTips":["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"],"tags":["mcp","stdio","process-exit","epipe","spawn"],"backgroundTag":"server-process-exited","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}