Skip to main content
This page is the canonical reference for connection mode (sticky vs. per-call) and connection state across every MCP auth type. For the auth-type decision itself (which credential shape to use), see MCP Authentication.

Two independent axes

Every MCP client sits somewhere on two independent axes:
  1. Who authenticates — server-level (none, headers, oauth) vs. per-user (per_user_headers, per_user_oauth, token_exchange). Covered in MCP Authentication.
  2. How the connection is heldsticky (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.
The two axes aren’t fully independent — which connection modes are even available depends on auth type and connection type: 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.

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.
  • unstable is 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_reauth is 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 is POST /api/mcp/client/{id}/reauthorize (also covers oauth; headers/none credentials 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 /reconnect returns 400 for this mode), and needs_reauth doesn’t apply here (there’s no single connection credential to die — headers/none/oauth per-call clients cycle through a fresh credential resolution on every call).
  • AddClient runs 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 at healthy/0-tools until the periodic checker’s slow first tick, which can be minutes away for an already-healthy client.
  • 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:
  1. Bootstrap. An admin submits a sample credential once (POST /verify-headers, POST /verify-exchange, or the complete-oauth callback for per_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.
  2. 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.
  3. Repair. If the retained credential itself dies (OAuth refresh fails, or a header schema change flips it to stale), it projects as needs_reauth in the client list — but only when the underlying runtime reading is healthy or unstable; a client sitting in pending_verification or disabled keeps that state instead. Repair is auth-type-specific: POST /verify-headers (headers), POST /verify-exchange (token exchange), or reauthorizecomplete-oauth (per_user_oauth).
End-user credential — resolved per caller, on the actual tool-call path, with no relationship to the admin credential above:
  1. A caller’s request carries an identity (VK, signed-in user, or session ID — see Identity modes).
  2. 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_required payload with an auth URL is returned instead of executing the tool.
  3. The caller completes their own auth (OAuth consent, or submitting header values) once; every later call resolves transparently from then on.
Because these two paths are independent, a per-user client’s tool list can go stale (admin credential dead) while every end-user’s actual tool calls keep working perfectly, or vice versa — a specific user can be locked out (their own credential expired) while the tool list stays perfectly current for everyone else. Don’t assume needs_reauth on a per-user client means anyone is blocked; check which credential it actually refers to.

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