{"record":{"id":"40841fa531a9a4af","repo":"Hmbown/CodeWhale","slug":"mcp-connection-name-was-cancelled","errorCode":null,"errorMessage":"MCP connection '{name}' was cancelled","messagePattern":"MCP connection '(.+?)' was cancelled","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/tui/src/mcp.rs","lineNumber":2386,"sourceCode":"\n    /// Get connection state\n    #[allow(dead_code)] // Public API for MCP consumers\n    pub fn state(&self) -> ConnectionState {\n        self.state\n    }\n\n    fn next_id(&self) -> String {\n        self.request_id.fetch_add(1, Ordering::SeqCst).to_string()\n    }\n\n    async fn send(&mut self, msg: serde_json::Value) -> Result<()> {\n        let bytes = serde_json::to_vec(&msg).context(\"Failed to serialize MCP JSON-RPC message\")?;\n        let cancel_token = self.cancel_token.clone();\n        let name = self.name.clone();\n        let result = tokio::select! {\n            biased;\n            _ = cancel_token.cancelled() => {\n                Err(anyhow::anyhow!(\"MCP connection '{name}' was cancelled\"))\n            }\n            result = self.transport.send(bytes) => result,\n        };\n        if result.is_err() {\n            // A dead write side is as fatal as a dead read side: the pool\n            // reuses any connection whose `is_ready()` is true, so leaving\n            // this one in `Ready` would hand the same broken transport back\n            // on every later call instead of rebuilding it.\n            self.state = ConnectionState::Disconnected;\n        }\n        result\n    }\n\n    async fn recv(&mut self, expected_id: String) -> Result<serde_json::Value> {\n        loop {\n            let bytes = match tokio::time::timeout(\n                Duration::from_secs(self.read_timeout_secs),\n                async {","sourceCodeStart":2368,"sourceCodeEnd":2404,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/mcp.rs#L2368-L2404","documentation":"The MCP connection's send path races the connection's cancellation token in a biased tokio::select!. If cancellation fires first, the outgoing JSON-RPC message is not sent and this error is returned instead. The library treats any send failure as fatal for the connection so the pool does not reuse a half-dead transport.","triggerScenarios":"Calling any request method on an McpConnection after its cancel_token was cancelled — typically during pool shutdown, reconnect handling, or a session teardown that cancels in-flight MCP work.","commonSituations":"User aborts a turn while an MCP tool call is being dispatched; pool replaces a stale connection and cancels the old one; application shutdown cancels connections with requests still in flight.","solutions":["Treat this as an expected cancellation, not a server fault: stop using the connection and obtain a fresh one from the pool.","Ensure no tool calls are dispatched after triggering cancellation for the connection.","If it appears spuriously, check for code paths that cancel the token without intending to drop the connection.","Retry the operation on a ready connection if the work still needs to complete."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Check the cancellation token before dispatching\nif cancel_token.is_cancelled() { return Err(anyhow!(\"shutting down; not dispatching MCP call\")); }","typeGuard":null,"tryCatchPattern":"match conn.send_request(msg).await {\n    Err(e) if e.to_string().contains(\"was cancelled\") => {\n        // expected during shutdown; skip retry, drop the work\n    }\n    Err(e) => pool.reconnect_and_retry(e)?,\n    ok => ok?,\n}","preventionTips":["Never issue MCP requests after triggering the connection's cancel token.","Structure shutdown to drain in-flight requests before cancelling.","Retry cancelled work on a fresh pooled connection only if the work is still needed.","Distinguish cancellation errors from transport errors in your error handling."],"tags":["mcp","cancellation","tokio"],"backgroundTag":"request-timeout","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}