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
- Retry the operation after the gateway restarts
- Restart the MCP gateway and re-run `aos mcp ready` + attach
- 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
- Drain in-flight requests before signalling shutdown
- Suppress new attaches once shutdown begins
- Use supervisor health checks to wait for a restarted gateway
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
- MCP gateway attach limit reached for principal '{principal}'
- MCP gateway startup generation changed before cleanup
- MCP gateway readiness changed before cleanup at {}
- shutdown stage gateway.listener_absence: a gateway is still
- shutdown stage daemon.singleton_lock: lock remains held at {
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/4495503b3bad9446.
Report an issue: GitHub.