astrid-runtime/astrid · warning

MCP gateway is shutting down

Error message

MCP gateway is shutting down

What it means

The MCP gateway is shutting down (its shutdown token is cancelled). client_for checks this before serving a principal's uplink and refuses to create or hand out clients, so in-flight requests fail fast instead of hanging against a dying gateway.

Source

Thrown at crates/astrid-cli/src/commands/mcp/gateway.rs:118

            permits: Mutex::new(HashMap::new()),
            slots: Mutex::new(HashMap::new()),
            admission: Arc::new(Mutex::new(())),
            initialize: Mutex::new(()),
            shutdown: CancellationToken::new(),
            active_connections: AtomicUsize::new(0),
            connections_drained: Notify::new(),
            watchers: Mutex::new(Vec::new()),
            shutdown_result: Mutex::new(None),
            shutdown_finished: Notify::new(),
            shutdown_ack_sent: Notify::new(),
            stop_ack_waiters: AtomicUsize::new(0),
        }
    }

    /// Get or establish the one daemon uplink for `principal`.
    async fn client_for(&self, principal: &astrid_core::PrincipalId) -> Result<Client> {
        if self.shutdown.is_cancelled() {
            anyhow::bail!("MCP gateway is shutting down");
        }
        let key = principal.to_string();
        if let Some(client) = self.clients.lock().await.get(&key).cloned() {
            return Ok(client);
        }

        // Serialize first-use handshakes so two simultaneous attaches for a
        // new principal cannot create duplicate long-lived uplinks/watchers.
        let _initialize = self.initialize.lock().await;
        if self.shutdown.is_cancelled() {
            anyhow::bail!("MCP gateway is shutting down");
        }
        if let Some(client) = self.clients.lock().await.get(&key).cloned() {
            return Ok(client);
        }

        let session = astrid_core::SessionId::from_uuid(Uuid::new_v4());
        let mut client = crate::socket_client::connect_for_workspace(

View on GitHub (pinned to affd8760f4)

Solutions

  1. Retry the operation after the gateway restarts
  2. Restart the MCP gateway and re-run `aos mcp ready` + attach
  3. Treat this as expected during shutdown and stop issuing new requests

Example fix

// before
let client = gateway.client_for(&principal).await?;
// after
match gateway.client_for(&principal).await {
    Ok(c) => ...,
    Err(e) if e.to_string().contains("shutting down") => /* retry or exit gracefully */,
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Poll gateway liveness before issuing requests
while gateway_is_starting_or_restarting() { sleep(BACKOFF).await; }

Type guard

fn gateway_alive(g: &Gateway) -> bool { !g.shutdown.is_cancelled() }

Try / catch

match gateway.client_for(&principal).await {
    Ok(c) => c,
    Err(e) if e.to_string().contains("shutting down") => /* stop work gracefully or wait for restart */,
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling verify_uplink (or any operation needing an uplink) while the gateway is concurrently shutting down; e.g. a request racing a `Ctrl-C`/daemon stop, or the first-use handshake attempted after cancellation.

Common situations: Cancelling the gateway while a client still issues requests; shutdown triggered by timeout or supervisor stop; long-running attach outliving the gateway process.

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


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/4495503b3bad9446. Report an issue: GitHub.