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
- 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.
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
- 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.
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
- MCP connection ' ' was cancelled
- MCP import failed
- MCP operation on plugin server
- MCP session preflight cancelled after plugin authority…
- MCP SSE connect cancelled before authentication completed
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)