Hmbown/CodeWhale · warning · anyhow::Error

MCP connection ' ' was cancelled

Error message

MCP connection '{name}' was cancelled

What it means

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.

Solutions

  1. Treat this as an expected cancellation, not a server fault: stop using the connection and obtain a fresh one from the pool.
  2. Ensure no tool calls are dispatched after triggering cancellation for the connection.
  3. If it appears spuriously, check for code paths that cancel the token without intending to drop the connection.
  4. Retry the operation on a ready connection if the work still needs to complete.
Defensive patterns

Strategy: try-catch

Validate before calling

// Check the cancellation token before dispatching
if cancel_token.is_cancelled() { return Err(anyhow!("shutting down; not dispatching MCP call")); }

Try / catch

match conn.send_request(msg).await {
    Err(e) if e.to_string().contains("was cancelled") => {
        // expected during shutdown; skip retry, drop the work
    }
    Err(e) => pool.reconnect_and_retry(e)?,
    ok => ok?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/40841fa531a9a4af. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/mcp.rs:2386

    /// Get connection state
    #[allow(dead_code)] // Public API for MCP consumers
    pub fn state(&self) -> ConnectionState {
        self.state
    }

    fn next_id(&self) -> String {
        self.request_id.fetch_add(1, Ordering::SeqCst).to_string()
    }

    async fn send(&mut self, msg: serde_json::Value) -> Result<()> {
        let bytes = serde_json::to_vec(&msg).context("Failed to serialize MCP JSON-RPC message")?;
        let cancel_token = self.cancel_token.clone();
        let name = self.name.clone();
        let result = tokio::select! {
            biased;
            _ = cancel_token.cancelled() => {
                Err(anyhow::anyhow!("MCP connection '{name}' was cancelled"))
            }
            result = self.transport.send(bytes) => result,
        };
        if result.is_err() {
            // A dead write side is as fatal as a dead read side: the pool
            // reuses any connection whose `is_ready()` is true, so leaving
            // this one in `Ready` would hand the same broken transport back
            // on every later call instead of rebuilding it.
            self.state = ConnectionState::Disconnected;
        }
        result
    }

    async fn recv(&mut self, expected_id: String) -> Result<serde_json::Value> {
        loop {
            let bytes = match tokio::time::timeout(
                Duration::from_secs(self.read_timeout_secs),
                async {

View on GitHub (pinned to 73e0f67d83)