Hmbown/CodeWhale · warning
MCP session preflight cancelled after plugin authority…
Error message
MCP session preflight cancelled after plugin authority changed
What it means
Before an MCP HTTP session is established, a preflight request resolves auth headers. Because a reviewed plugin's authority (trust/approval state) can change asynchronously, the preflight races it via `tokio::select!` with a cancellation token; if cancellation wins, the library aborts the session setup with this error instead of connecting to a server whose authority was revoked mid-flight.
Solutions
- Retry the connection after the plugin's authority state settles (the new policy will be applied on the next attempt)
- Complete any pending plugin review/approval so the authority stops changing, then reconnect
- Cancel and restart the MCP client cleanly rather than reusing the aborted transport
- If this happens repeatedly, serialize plugin policy changes with connection attempts in the caller
Example fix
// before
let transport = client.connect_with_policy(&policy, cancel).await?; // may abort with cancelled preflight
// after
match client.connect_with_policy(&policy, cancel).await {
Ok(t) => t,
Err(e) if e.to_string().contains("plugin authority changed") => {
wait_for_policy_settled().await;
client.connect_with_policy(&policy, cancel).await?
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: retry
Validate before calling
// Ensure no plugin policy reload is in flight before connecting wait_for_plugin_policy_settled(&policy_watch).await;
Try / catch
match client.connect_with_policy(&policy, &cancel).await {
Ok(t) => t,
Err(e) if e.to_string().contains("plugin authority changed") => {
tokio::time::sleep(Duration::from_millis(250)).await; // let policy settle, then retry
client.connect_with_policy(&policy, &cancel).await?
}
Err(e) => return Err(e),
} Prevention
- Avoid revoking/re-reviewing plugins while connections are being established
- Serialize plugin policy reloads with client connect attempts
- Treat this error as 'state changed, retry' rather than a hard failure
When it happens
Trigger: `try_establish_session` (crates/tui/src/mcp/http.rs:206) is waiting on `resolved_headers()` when the cancellation token fires — i.e. the plugin's reviewed authority changed (revoked/re-approval needed) during session setup, via `connect_with_policy` or a GET preflight.
Common situations: A user revokes or re-reviews a plugin while the client is still connecting; plugin policy is reloaded (config change) concurrently with a connection attempt; rapid connect/disconnect during plugin updates.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- MCP operation on plugin server
- MCP connection ' ' was cancelled
- MCP connection ' ' was cancelled
- MCP SSE connect cancelled before authentication completed
- MCP SSE connect cancelled before the request completed
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/c895ef78a7465f0e.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp/http.rs:206
/// line and move on — the `initialize` POST will proceed without a
/// session ID.
/// * If the server opens an SSE stream in response (the GET from old
/// SSE transport), we read only the headers, then discard the body
/// so the SSE stream is torn down. The actual SSE path uses a
/// dedicated `SseTransport` and is triggered by the incompatible-
/// status fallback in [`HttpTransport::send`].
pub(super) async fn try_establish_session(&mut self) -> Result<()> {
let cancel = self.cancel_token.clone();
let transport = match &mut self.mode {
HttpTransportMode::Streamable(t) => t,
// Already on SSE — session is implicit via the long-lived GET.
HttpTransportMode::Sse(_) => return Ok(()),
};
let headers = tokio::select! {
biased;
_ = cancel.cancelled() => {
anyhow::bail!("MCP session preflight cancelled after plugin authority changed")
}
headers = transport.auth.resolved_headers() => headers?,
};
let request = apply_safe_custom_headers(
with_default_mcp_http_headers(transport.client.get(&transport.url), false),
&headers,
);
let response = tokio::select! {
biased;
_ = cancel.cancelled() => {
anyhow::bail!("MCP session preflight cancelled after plugin authority changed")
}
response = tokio::time::timeout(Duration::from_secs(5), transport.client.send(request)) => {
response
.map_err(|_| anyhow::anyhow!("GET timeout"))?
.map_err(|e| anyhow::anyhow!("GET error: {e}"))?
}
};View on GitHub (pinned to 73e0f67d83)