{"record":{"id":"e360511cb918d6d3","repo":"Hmbown/CodeWhale","slug":"mcp-connection-was-cancelled","errorCode":null,"errorMessage":"MCP connection '{}' was cancelled","messagePattern":"MCP connection '(.+?)' was cancelled","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/tui/src/mcp.rs","lineNumber":2174,"sourceCode":"    }\n\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        tokio::select! {\n            biased;\n            _ = self.cancel_token.cancelled() => {\n                self.state = ConnectionState::Disconnected;\n                anyhow::bail!(\"MCP connection '{}' was cancelled\", self.name)\n            }\n            result = self.transport.send(bytes) => result,\n        }\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 {\n                    tokio::select! {\n                        biased;\n                        _ = self.cancel_token.cancelled() => {\n                            anyhow::bail!(\"MCP connection '{}' was cancelled\", self.name)\n                        }\n                        result = self.transport.recv() => result,\n                    }\n                },","sourceCodeStart":2156,"sourceCodeEnd":2192,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L2156-L2192","documentation":"Raised by McpConnection::send when the connection-wide CancellationToken fires while a JSON-RPC request is still being written to the transport. The tokio::select! is biased so cancellation always wins over the pending transport.send, and the connection state is set to Disconnected before the error is returned. It is a cooperative shutdown signal, not a transport failure: some other component (close(), a config reload dropping the connection, a workspace switch, or plugin-authority revocation) decided this connection must stop.","triggerScenarios":"Calling any MCP method (tools/list, initialize, call_tool) on a connection whose cancel_token has been cancelled — e.g. after McpConnection::close(), after the pool drops the connection during reload_from_config_sources, or during switch_workspace_config_source_and_connect_all. Any in-flight request that reaches send() after cancellation gets this error.","commonSituations":"User edits .mcp config while a tool call is in flight (lazy auto-reload drops the connection); a reviewed plugin's catalog changes and the old connection is torn down; a sub-agent finishes and the shared pool is closed; tests cancel a connection to exercise shutdown paths.","solutions":["Treat as a transient, expected shutdown error: drop the stale McpConnection reference and retry the operation through McpPool::get_or_connect, which transparently reconnects.","If it happens mid-turn during normal use, check whether the MCP config file changed on disk (mtime-triggered reload) and let the reconnect path rebuild the connection.","Avoid holding a cached &mut McpConnection across await points that can span a config reload; always re-fetch via the pool.","If you intentionally cancelled (close()), simply propagate or ignore the error — do not retry."],"exampleFix":"// before\nlet conn = pool.get_or_connect(\"github\").await?;\nlet tools = conn.list_tools().await?; // fails with 'was cancelled' after reload\n\n// after\nlet tools = match pool.get_or_connect(\"github\").await?.list_tools().await {\n    Ok(tools) => tools,\n    Err(e) if e.to_string().contains(\"was cancelled\") => {\n        pool.get_or_connect(\"github\").await?.list_tools().await?\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Rust: match on the error, distinguish cancellation from real failures\nmatch conn.send_request(...).await {\n    Err(e) if e.to_string().contains(\"was cancelled\") => {\n        // connection torn down (reload/close/authority): rebuild it\n        let conn = pool.get_or_connect(server).await?;\n        conn.send_request(...).await\n    }\n    other => other,\n}","preventionTips":["Always acquire connections through McpPool::get_or_connect instead of caching McpConnection handles across turns.","Never retry when your own code called close() — cancellation is the intended outcome.","Treat 'was cancelled' as a transient signal distinct from timeouts and protocol errors."],"tags":["mcp","cancellation","concurrency","rust"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}