{"record":{"id":"549329be49b17ef9","repo":"Hmbown/CodeWhale","slug":"mcp-server-process-closed-stdin-before-answering","errorCode":null,"errorMessage":"MCP server '{}': process closed stdin before answering {}{}","messagePattern":"MCP server '(.+?)': process closed stdin before answering (.+?)(.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/mcp/src/stdio_client.rs","lineNumber":875,"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(ChildStdoutMessage::Line(line)) => line,\n                Ok(ChildStdoutMessage::Invalid(error)) => {\n                    bail!(","sourceCodeStart":857,"sourceCodeEnd":893,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/mcp/src/stdio_client.rs#L857-L893","documentation":"When writing a JSON-RPC request to the MCP child's stdin fails with a broken-pipe error, the child process has already exited and closed its stdin. The client deliberately maps EPIPE to this message — appending `exit_note()` with the child's exit status — so that platform-dependent write-failure races (macOS reports the write error, Linux usually sees EOF first) surface as one consistent 'server died' error instead of a bare `Broken pipe`. This indicates the MCP server crashed or exited before responding.","triggerScenarios":"`request(method, ...)` is called on a `Connection` whose child process has terminated; the serialized request write returns EPIPE, or the reader thread observed EOF first and this write loses the race.","commonSituations":"The MCP server binary panics or segfaults on startup; the command path resolves to a script that exits immediately (missing interpreter, bad shebang); the server was killed by OOM or an external signal; the server rejected the handshake and quit.","solutions":["Read the exit note appended to the message for the child's exit status and run the server command manually to see its startup error.","Fix the server's crash: check for missing interpreter/runtime, bad arguments, or missing env vars in `McpServerConfig`.","Increase any startup resources/limits if the child was OOM-killed.","Recreate the connection/client after fixing the server — the pipe is dead and the connection is unusable."],"exampleFix":"// before: command exits instantly (no shebang / wrong runtime)\n{\"name\":\"my-mcp\",\"command\":\"./server.py\",\"args\":[]}\n// after: invoke via the interpreter explicitly\n{\"name\":\"my-mcp\",\"command\":\"python3\",\"args\":[\"/abs/path/server.py\"]}","handlingStrategy":"try-catch","validationCode":"// before connecting, confirm the command starts and stays alive\nlet probe = Command::new(&config.command).args(&config.args).status()?;\nif !probe.success() {\n    return Err(\"MCP command exits immediately; fix it before connecting\".into());\n}","typeGuard":null,"tryCatchPattern":"match conn.request(\"tools/list\", (), timeout).await {\n    Err(e) if e.to_string().contains(\"closed stdin before answering\") => {\n        eprintln!(\"MCP child crashed: {} — run the command manually to see the error\", e);\n        // reconnect after fixing; the connection is dead\n    }\n    other => other,\n}","preventionTips":["Run the server command standalone once before configuring it — catch missing interpreters and bad shebangs.","Send all server startup diagnostics to stderr so crashes are visible in the exit note/logs.","Monitor child exit codes and surface them next to connection errors.","Use absolute paths and explicit runtimes to avoid environment-dependent startup failures."],"tags":["mcp","broken-pipe","process-crashed","stdio"],"backgroundTag":"broken-pipe","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}