Hmbown/CodeWhale · critical · anyhow::Error

MCP connection task panicked

Error message

MCP connection task panicked

What it means

MCP connection tasks are spawned with catch_unwind; if the underlying connect future panics, the panic is converted into a regular Err so the pool records it as a connection failure instead of tearing down the caller. The message intentionally hides panic details behind a stable label.

Solutions

  1. Inspect logs/stderr for the original panic backtrace (panics usually print before being caught)
  2. Identify the server whose connect panicked and reproduce its handshake in isolation
  3. File/fix the panic in the connector or transport code — this message indicates an internal bug, not a config problem
Defensive patterns

Strategy: try-catch

Try / catch

let result = std::panic::catch_unwind(|| connector.connect())
    .unwrap_or_else(|_| Err(anyhow!("MCP connection task panicked")));
if let Err(e) = result { log::error!("mcp connect panic: {e:#}"); }

Prevention

When it happens

Trigger: A panic inside the per-server connect task (e.g. unwrap/ indexing bug in a connector or transport) while handshaking an MCP server.

Common situations: Bug in an MCP transport/SDK integration triggered by a malformed server response; a custom plugin connector panicking during handshake.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

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

            let network_policy = network_policy.clone();
            joins.spawn(async move {
                let connection = std::panic::AssertUnwindSafe(async {
                    let _permit = permit.acquire_owned().await;
                    McpConnection::connect_with_policy(
                        name.clone(),
                        config,
                        &timeouts,
                        network_policy.as_ref(),
                    )
                    .await
                    .map(|mut connection| {
                        connection.catalog_generation = catalog_generation;
                        connection
                    })
                })
                .catch_unwind()
                .await
                .unwrap_or_else(|_| Err(anyhow::anyhow!("MCP connection task panicked")));
                (name, connection)
            });
        }

        joins
    }

    /// Connect to all enabled servers, returning errors for failed connections.
    ///
    /// Servers connect **concurrently** (bounded by [`Self::CONNECT_CONCURRENCY`]).
    /// This used to be a sequential loop over `get_or_connect`, so every
    /// server paid the slowest server's spawn+handshake from its own budget:
    /// with the default 10s connect timeout, N servers meant a worst case of
    /// N×10s before the pool was usable. Each connection still gets its own
    /// configured connect timeout; one wedged server can no longer serialize
    /// the rest.
    ///
    /// Semantics preserved from the sequential loop: only configured servers

View on GitHub (pinned to 73e0f67d83)