{"record":{"id":"da0ee7d9fe4d2715","repo":"Hmbown/CodeWhale","slug":"mcp-server-server-method-timed-out-after-t","errorCode":null,"errorMessage":"MCP server '{server}': {method} timed out after {timeout:?}","messagePattern":"MCP server '(.+?)': (.+?) timed out after (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/mcp/src/stdio_client.rs","lineNumber":353,"sourceCode":"            // 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:?}\");\n                }\n                Err(RecvTimeoutError::Disconnected) => {\n                    bail!(\n                        \"MCP server '{server}': process closed stdout before answering {method}{}\",\n                        self.exit_note()\n                    );\n                }\n            };\n\n            // Servers occasionally emit banners or log lines on stdout, and\n            // notifications carry no id. Both are skipped; only the matching\n            // response ends the wait.\n            let Ok(message) = serde_json::from_str::<Value>(&line) else {","sourceCodeStart":335,"sourceCodeEnd":371,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/mcp/src/stdio_client.rs#L335-L371","documentation":"A JSON-RPC request to a stdio MCP server exceeded its deadline: the loop recomputed the remaining time before waiting on the response channel and found it already exhausted. The timeout value in the message is the per-call bound — 30s (HANDSHAKE_TIMEOUT) for the initialize handshake and 120s (REQUEST_TIMEOUT) for regular requests like tools/call. Lines without the matching JSON-RPC id (banners, log lines, notifications) are skipped and do not reset the clock, so a server that never answers with the right id will always time out.","triggerScenarios":"A tools/call that legitimately runs longer than 120s; a server that hangs (deadlock, waiting on a network resource); a server that replies with a different or missing id so its responses are skipped as noise; initialize taking over 30s on a cold npx install that downloads dependencies first.","commonSituations":"Long-running tools (bulk scans, codebase indexing, remote builds) exceeding the fixed 120s budget; first-run npx-based servers compiling/installing during the 30s handshake; servers behind a slow network dependency; buggy servers that echo the wrong id.","solutions":["If the tool is legitimately slow, restructure the work (batch smaller, run async server-side) or use spawn_with_timeouts via the crate's testing seam to raise the budget","Run the server manually and time the same request to distinguish 'slow' from 'hung'","Verify the server copies the request id into its response — responses with wrong/missing ids are silently skipped and guarantee a timeout","For npx-style servers, pre-install the package so the handshake does not race package download"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"null","typeGuard":null,"tryCatchPattern":"let mut attempt = 0;\nloop {\n    attempt += 1;\n    match client_call(&client, tool, args) {\n        Ok(v) => break Ok(v),\n        Err(err) if err.to_string().contains(\"timed out\") && attempt < 2 => {\n            tokio::time::sleep(Duration::from_secs(2)).await; // server may be slow, not dead\n        }\n        Err(err) => break Err(err),\n    }\n}","preventionTips":["Measure real tool latency and keep calls well under the 120s request budget","Pre-install npx/package-based servers so the 30s handshake never races a download","Confirm the server echoes request ids; id-less responses are skipped and guarantee timeouts"],"tags":["mcp","stdio","timeout","jsonrpc","deadline"],"backgroundTag":"request-timeout","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}