MCP Authentication

How a client connects to the MCP server, by signing in with OAuth 2.1 or with a personal access token, and how every tool enforces its ability.

The MCP server, written as https://mcp.example.test on this page, accepts two credentials on the same endpoint:

CredentialHow you get itActs asBest for
OAuth 2.1 access tokenThe client asks; you sign in to Zuse Bifrost and press Authorize. No token to copy.You, on the team you are working in, with every ability you hold thereClaude, Claude Code, Cursor, VS Code, ChatGPT, any OAuth-aware client
Personal access token (zg_…)Account Settings → API Tokens, choosing the abilities and the team scope by handThe abilities and scope:team: / scope:project: you mintedScripts, CI, clients you configure with a header, narrow grants

Both run through the same tenant resolution and the same per-tool ability gate. Nothing about a tool changes with the credential; what changes is how broad the credential is and how it was granted.

Connecting with OAuth

Add the server URL to a client that supports remote MCP servers and choose to connect. The client discovers the authorization server on its own, registers itself, and sends you to Zuse Bifrost:

  1. If you are not signed in to the application, you sign in first (two-factor authentication and passkeys apply as usual).
  2. Zuse Bifrost shows a consent screen naming the client, the account you are signed in as, and what the connection will be able to do.
  3. Authorize returns you to the client, which now holds a short-lived access token and a refresh token.

An access token lasts one hour. The client renews it silently with the refresh token, which lasts 30 days from its last use and is rotated on every renewal; a connection you stop using expires by itself. Disconnecting the server inside the client discards both tokens.

What an OAuth connection can do

The connection acts as you, on the team you had selected when you authorized it, with every ability your role there grants: the same reach you have in the application, and the same tenant boundary. Tools still enforce their own abilities, and tools that change data still ask for confirmation inside the client before they run. If you want a connection that can only read, or only reach one project, mint a personal access token with those abilities instead and configure the client with it.

Under the hood

For anyone wiring up a client by hand, the server follows the MCP authorization specification:

  • An unauthenticated call is answered with 401 and WWW-Authenticate: Bearer realm="Zuse Bifrost MCP", resource_metadata="https://mcp.example.test/.well-known/oauth-protected-resource", scope="mcp:use". When the server lives under the application's own address as /mcp, the metadata sits at /.well-known/oauth-protected-resource/mcp there instead.
  • The protected resource metadata names the endpoint as its resource, the application's address as its one authorization server, mcp:use as its one scope and the Authorization header as the only place a bearer token is read from.
  • /.well-known/oauth-authorization-server on the application's address (mirrored at /.well-known/openid-configuration) lists the authorization, token and dynamic registration endpoints; the grants are authorization_code and refresh_token, PKCE is S256 only, and the one scope is mcp:use.
  • Clients register dynamically (RFC 7591) as public clients; the token endpoint accepts application/x-www-form-urlencoded; a spent or rotated refresh token is refused with invalid_grant.
  • Native clients may name a custom redirect scheme: cursor, vscode, vscode-insiders and windsurf are accepted beside https callbacks.

Connecting with a personal access token

Mint a token under Account Settings → API Tokens and send it on every request:

Authorization: Bearer zg_live_<id>_<secret>

Configure the client with the header (each client page shows where). The token carries only the abilities you ticked, frozen to one team and optionally to a list of projects, exactly as on the REST API. API tokens covers the page in full.

Per-tool abilities

Each tool enforces one concrete ability from the catalog. A personal access token carrying project:view-any can call list_projects but receives TOKEN_MISSING_ABILITY on get_activity_log; an OAuth connection satisfies every ability because you consented as yourself. Grant personal access tokens narrowly.

An ability means the same thing on both transports: a tool requires exactly what its REST equivalent does, so mint for the operations you intend to call rather than for the channel you intend to call them over. There is no MCP-specific ability to tick: mcp:use is the name of the OAuth scope a signed-in connection carries, and a personal access token whose abilities cover the tools it calls may use MCP exactly as it uses REST.

ToolRequired ability
get_meaccount:read
list_projectsproject:view-any
get_projectproject:view
list_team_membersteam-member:view-any
list_tokenstoken:view-any
list_webhook_endpointswebhook-endpoint:view-any
retry_webhook_deliverywebhook-delivery:retry
get_activity_logactivity:read

The tools reference carries the ability mapping for every tool.

Team and project scope

A personal access token carries scope:team:<uuid> and optional scope:project:<uuid> entries, the same ones the REST API uses. An OAuth connection is scoped to the team you were working in when you authorized it and to every project you can reach on it. The MCP server runs the same tenant-resolution step either way, so every tool call is team-isolated exactly as a REST request is, and anything outside the scope answers RESOURCE_NOT_FOUND rather than a permission error.

Revocation

  • Personal access tokens: revoke from Account Settings → API Tokens, or with the revoke_token tool from another token. Revocation takes effect on the next call; any client still holding the token receives AUTHENTICATION_REQUIRED.
  • OAuth connections: disconnect the server inside the client, which discards its tokens. The access token it held expires within the hour and the refresh token 30 days after its last use.

Rate limiting

Authenticated MCP calls are bucketed at 120 a minute per token, per personal access token or per OAuth access token. Hitting the limit returns RATE_LIMITED with Retry-After. Claude, Cursor, ChatGPT Desktop and VS Code all pace their tool calls internally; exhausting this bucket in practice usually means a tight loop on the client side.

Unauthenticated traffic to the MCP endpoint (probes, port scanners, misconfigured clients with no credential attached) is bucketed separately at 5 a minute per address. The OAuth discovery documents and the registration endpoint have their own bucket of 60 a minute per address, so a client's first handshake is never throttled by someone else's probing. See Rate limiting for the REST buckets.

CORS

Browser-based MCP clients (the hosted MCP Inspector, in-browser playgrounds, web extensions) issue cross-origin requests against the MCP endpoint. The server returns a permissive Access-Control-Allow-Origin: * so any browser origin can complete the handshake; Access-Control-Allow-Credentials is not set because the server authenticates exclusively through Authorization: Bearer … and never accepts cookies on this surface.

The CORS layer exposes the request and response headers a browser client needs to read:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, Accept, MCP-Session-Id, MCP-Protocol-Version, Last-Event-ID
Access-Control-Expose-Headers: MCP-Session-Id, X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After, WWW-Authenticate
Access-Control-Max-Age: 86400

Preflight OPTIONS requests short-circuit before authentication so a browser can complete its handshake without first owning a credential. Native MCP clients ignore CORS entirely; these headers only matter when the caller is a browser.

Logging and auditing

Every change a tool makes is recorded in the activity log with you as the causer and mcp as the surface it came through, beside the changes made in the application and over REST; reads leave no entry. The log is readable through the activity:read ability, over the REST GET /v1/activity endpoint or the get_activity_log tool, and on the activity log page, so you can audit what an agent did on your behalf.

How is this guide?

Last updated on

On this page

What changed recentlyEvery release of Zuse Bifrost, with the pages it touched.

For AI agents: llms.txt · llms-full.txt