Configurable connection mode via
needs_session_stickiness, and the token_exchange auth type, are available in Bifrost v2.0.0 and above.Two independent axes
Every MCP client sits somewhere on two independent axes:- Who authenticates — server-level (
none,headers,oauth) vs. per-user (per_user_headers,per_user_oauth,token_exchange). Covered in MCP Authentication. - How the connection is held — sticky (one persistent upstream connection, reused for every tool call) vs. per-call (a fresh connection dialed per tool call, closed immediately after). This page’s focus.
So
needs_session_stickiness only ever has an effect on http connections using a server-level auth type. Everything else is fixed by construction.
Two entirely separate credential-resolution paths exist for per-user auth types, and they’re easy to conflate: the admin discovery credential (retained once, used only for periodically refreshing the server’s tool list) and the end-user’s own credential (resolved per request, used only for the actual tool call). See Per-user lifecycle below.
Connection states
pending_tools and a bare connected/disconnected naming existed in older versions of this doc set — the current state names are exactly the seven above. If you see connected, disconnected, connecting, or pending_tools referenced anywhere else in these docs, that’s stale and maps to healthy, unstable, (nothing — was never a real state), and (removed — an empty healthy tool map already communicates the same thing) respectively.Why a server is in a state
When Bifrost’s own connection handling has failed for a server, the state carries its explanation.GET /api/mcp/clients returns it as last_failure, and the UI shows the same record behind the state badge (click it) and at the top of the server sheet. The field is present only after the serving instance has attempted a connect or check that failed; it is absent while healthy, and absent when a state changed without such an attempt.
A few properties worth knowing:
- It is the serving instance’s own record. It describes what this Bifrost instance last ran into, never the outcome of real tool calls made through the server.
- It clears on the first passing check. A
healthyserver never carries a stale reason; the record is gone the moment the state flips back. - It can lag a credential-driven state. A
needs_reauththat comes from a credential row dying in the store (a rejected refresh, a rotation) shows no reason until the server’s next scheduled check actually hits the dead credential, at which point the provider’s rejection is recorded. Shared-OAuth servers hit it on their next reconnect; per-user servers on their next tool-discovery check. - A single failed check is enough. There is no consecutive-failure counter across checks. Each check already retries with backoff internally (three retries per operation), and that budget is what absorbs an ordinary blip. A check that exhausts it marks the server
unstableand records why. - Reasons are logged once per transition. The state-change log line at
INFOcarries the stage and message. Individual failed checks during an outage log atDEBUGonly, so a long outage does not flood the log.
last_failure through the shared node-state heartbeat. When instances disagree, state is degraded and node_states maps each instance ID to its own { "state", "last_failure" }. The same map is also attached when every instance agrees on unstable, because their reasons can still differ (one pod missing a binary, another timing out). The UI folds that map by state and reason, so three instances failing the same way read as one line with a count; the per-instance detail stays available in the API.
Lifecycle: sticky server-level (needs_session_stickiness: true, or sse/stdio)
- Connect happens once at
AddClient(boot, or client creation) and again on every reconnect. Reconnects are make-before-break: the old connection keeps serving until the new one is ready, so token rotation never causes downtime. unstableis purely informational — a network blip doesn’t stop tool calls, and the client self-heals on the next successful check with no human involvement.needs_reauthis a hard gate specifically for server-level clients: the periodic checker stops retrying entirely (retrying against a known-dead credential is pointless), and tool calls are refused outright rather than attempted. Recovery isPOST /api/mcp/client/{id}/reauthorize(also coversoauth;headers/nonecredentials don’t expire the same way, so this state is effectively OAuth-only in practice for server-level clients).- Every discovered tool list — from the initial connect and from every periodic refresh — persists to the DB, gated on the tool list actually having changed (no write on an unchanged tick), so a restart doesn’t lose anything a running instance had already discovered.
Lifecycle: per-call server-level (needs_session_stickiness: false/omitted, http only)
- No persistent connection ever exists — every tool call dials fresh and closes immediately after. There’s nothing to reconnect (
POST /reconnectreturns400for this mode), andneeds_reauthdoesn’t apply here (there’s no single connection credential to die —headers/none/oauthper-call clients cycle through a fresh credential resolution on every call). AddClientruns a synchronous first discovery pass if no tools are already known (e.g. from a prior successful discovery persisted to the DB) — without this, a client would sit athealthy/0-tools until the periodic checker’s slow first tick, which can be minutes away for an already-healthyclient.- The periodic checker’s ongoing ticks are the only thing revisiting this client afterward — same content-hash gating as the sticky case, so an unchanged tool list doesn’t cause a write.
Lifecycle: per-user (per_user_oauth, per_user_headers, token_exchange)
Per-user clients have two entirely separate credential paths, which is the single most important thing to understand about them:
Admin discovery credential — exists purely to keep the advertised tool list current, independent of any individual user:
- Bootstrap. An admin submits a sample credential once (
POST /verify-headers,POST /verify-exchange, or thecomplete-oauthcallback forper_user_oauth). This is the one and only time a live admin session is required. As a side effect, that credential is retained server-side. - Ongoing refresh.
AddClient’s own synchronous first-discovery pass and the periodic checker’s ongoing ticks both reuse the retained credential (never a fresh admin login) to keep the tool list current — same content-hash-gated persistence as server-level clients. - Repair. If the retained credential itself dies (OAuth refresh fails, or a header schema change flips it to stale), it projects as
needs_reauthin the client list — but only when the underlying runtime reading ishealthyorunstable; a client sitting inpending_verificationordisabledkeeps that state instead. Repair is auth-type-specific:POST /verify-headers(headers),POST /verify-exchange(token exchange), orreauthorize→complete-oauth(per_user_oauth).
- A caller’s request carries an identity (VK, signed-in user, or session ID — see Identity modes).
- Bifrost looks up that identity’s own stored credential for this MCP client. Found and active → the call proceeds transparently. Missing or stale → an
mcp_auth_requiredpayload with an auth URL is returned instead of executing the tool. - The caller completes their own auth (OAuth consent, or submitting header values) once; every later call resolves transparently from then on.
Recovery endpoints by combination
A
reconnect/reauthorize/close call that doesn’t apply to a client’s current connection mode returns a consistent error (client uses per-call connections; there is no persistent connection to reconnect/close) rather than one that implies something is specifically wrong with per-user auth — the same message applies to any per-call client, shared or per-user.
Next Steps
- Session Stickiness — how to set
needs_session_stickiness - MCP Authentication — which auth type to pick
- MCP Gateway — reconnection behavior, dynamic tool discovery, health monitoring

