Skip to main content
A server verifies the caller’s identity before it runs any handler. The client’s job is the other side of that exchange: present a credential the server will accept. Authentication is orthogonal to the transport — you choose where the server lives and, separately, how you prove who you are — so you supply a credential through the auth option regardless of whether you connect over HTTP or stdio. This page covers three credential families. A static bearer token suits the simplest setup. Interactive OAuth suits user-delegated access with refresh; it also covers step-up, where the client re-authorizes mid-session if a server later demands a broader scope. When the client runs in a browser, a browser variant adapts the same OAuth flow — a popup or redirect rather than the system browser — but the credential model is unchanged. A third family covers non-interactive, machine credentials for machine-to-machine calls: client credentials (with three token-endpoint authentication methods), the JWT-bearer grant for workload identity, and enterprise-managed token exchange for federated deployments (SEP-990).

Bearer tokens

The simplest credential is a token you already hold. BearerAuth attaches it as an Authorization: Bearer <token> header on every request, unchanged for the life of the client. This is the right choice whenever you have a long-lived token from an environment variable or secret store and the server verifies it directly — a JWT it validates against a JWKS, an opaque token it introspects, or a static token in a development setup.
Because the header is static, BearerAuth does no token management. When the token can expire and needs renewing, reach for OAuth instead.

OAuth

When access is delegated through an identity provider — the OAuth flow a server fronts or proxies — tokens are short-lived and must be refreshed. OAuth wraps the transport’s fetch so that every request resolves the current access token, and when that token is within its refresh buffer of expiring, it transparently exchanges the refresh token for a new one before the request goes out. You never see an expired-token failure on the happy path. Refreshes are coalesced: if several requests discover an expiring token at once, they share a single refresh call rather than each firing their own, so a burst of concurrent activity never stampedes the token endpoint. Some servers withhold a broader scope until you ask for it: a request succeeds against one operation but comes back 401, or 403 with an insufficient_scope challenge, on another. When that happens after the client has connected, OAuth runs a step-up re-authorization — the same authorization round connect() performs, this time honoring the scope the server challenged for — and retries the request. The round is single-flight, so concurrent challenges share it rather than each opening a browser. Re-authorization is bounded per request: one round covers a scope step-up; a second covers an authorization-server migration (SEP-2352), where the first authenticated request moves the server to a new authorization server and the next request must re-register and re-authorize there. Beyond the bound, the failure propagates rather than looping. Step-up is interactive-only. A failure from a non-interactive provider (BearerAuth, ClientCredentials, JwtBearerAuth, or EnterpriseManagedAuth) propagates unchanged, because a machine-to-machine credential has no user to send through a browser. Tokens survive only as long as the client unless you persist them. OAuth stores tokens through a pluggable store — the default InMemoryStore keeps them for the process lifetime, while FileTokenStorage writes them to disk so a restarted client resumes without sending the user back through the authorization flow.
Two hardening measures run inside that flow with no extra configuration. The client validates the RFC 9207 iss parameter the authorization server returns on the callback, so a response minted by the wrong issuer is rejected rather than trusted. It also keys stored credentials by issuer (SEP-2352), so a client that reaches servers behind different authorization servers never crosses a token from one issuer into another. If a server later migrates to a different authorization server, the client re-discovers it, registers fresh at the new server, and never presents the old server’s client credentials there. When your client publishes a metadata document, clientMetadataUrl (SEP-991) skips Dynamic Client Registration entirely. Point it at an HTTPS URL that serves the client’s metadata; when the authorization server advertises support, that URL is used directly as the client_id. The value is checked when you construct OAuth, so a malformed URL fails at construction rather than deep in the flow.
Prefer an external identity provider when you can. A FastMCP server’s built-in authorization server runs on a frozen, supported-interim code path, and Dynamic Client Registration is deprecated in revision 2026-07-28 — clientMetadataUrl is how a client sidesteps DCR against a server that still fronts one.

Client credentials

Machine-to-machine integrations have no user to send through a browser. The client credentials grant trades a client ID and secret directly for an access token, with no interactive step, which makes ClientCredentials the strategy for background jobs, services, and CI talking to a protected server. Like OAuth, it acquires and refreshes tokens for you; unlike OAuth, the whole exchange is unattended.
By default the provider sends the client ID and secret in the request body (client_secret_post). Two other token-endpoint authentication methods are available:
  • Set authMethod: 'client_secret_basic' to send the credentials in an HTTP Basic Authorization header (RFC 6749 §2.3.1). Nothing secret goes in the body.
  • Supply a privateKey (PEM) and its algorithm in place of clientSecret to authenticate with a signed JWT assertion (private_key_jwt, RFC 7523). The client signs one assertion per token request and sends no secret. You must also set audience — the assertion’s aud claim. RFC 7523 §3 recommends the authorization server’s issuer identity; some servers verify against the token endpoint URL instead. Set whichever your server checks.
Give either a clientSecret or a privateKey — never both. client_secret_post stays the default.
When you connect to several servers at once, each can carry its own credential rather than sharing one. A multi-server config accepts an auth field per entry, so one client can present a bearer token to one server and run OAuth against another.

Workload identity (JWT-bearer)

A workload — a Kubernetes pod, a cloud VM, a CI job — often already holds a signed identity token its runtime minted. JwtBearerAuth presents that token directly to the authorization server under the JWT-bearer grant (RFC 7523 §2.1) and receives an access token in return. There is no shared secret and no interactive step: the assertion itself is the credential.
The client is a public client by default (authMethod: 'none') — the assertion carries the identity, so no secret is sent. Pass a function for assertion when the workload token is short-lived; the provider calls it each time it fetches a new access token. Set scope to request specific scopes.

Enterprise-managed authorization (SEP-990)

Enterprise deployments federate identity through a central provider: the user signs in at the enterprise IdP, and the MCP server’s authorization server trusts that IdP rather than authenticating the user itself. EnterpriseManagedAuth runs the two-hop flow. First it exchanges the user’s OpenID Connect ID token for an identity-assertion grant (an ID-JAG) scoped to the target server (token exchange, RFC 8693). Then it presents that grant to the server’s authorization server under the JWT-bearer grant (RFC 7523) to obtain the access token.
Both hops are unattended once you supply the ID token. Set audience to the authorization server the grant is minted for, and resource to the MCP server the token is used against — the authorization server verifies the grant against both. The access token is cached and refreshed through the same store as the other providers.

Browser apps

The OAuth strategy assumes a desktop environment: it opens the system browser, runs a throwaway HTTP server on localhost to catch the redirect, and writes tokens to a file. None of that exists inside a browser tab. BrowserOAuth runs the same authorization-code-with-PKCE flow and the same transparent refresh, but drives the redirect the way a web app can and reads tokens from web storage instead of disk. It is an OAuth provider underneath, so the connection treats it identically — only the construction differs. The authorization step opens the provider in a popup by default. You give BrowserOAuth a redirectUri on your own origin; the page served there hands the authorization code back to the opener by calling handleOAuthCallback. The popup closes itself, the original page receives the code over postMessage, and the connection completes without the user ever leaving your app.
The page at that redirectUri only needs to forward the result back to the opener:
Browsers only open popups in response to a user gesture, so trigger connect() from a click rather than on page load, or the window is blocked. Where popups are impractical — stricter mobile browsers, for instance — construct BrowserOAuth with mode: 'redirect' to navigate the whole tab to the provider instead. The tab returns to your redirectUri with the code in the query string, and resumeFromRedirect() reads it so the page that loads can finish connecting. Tokens persist through the same pluggable store the Node provider uses, with browser-backed implementations standing in for the filesystem. LocalStorageStore keeps tokens in localStorage for a small, synchronous store; IndexedDBStore uses IndexedDB when you want larger or more durable storage. Either lets a returning user skip the authorization flow, exactly as FileTokenStorage does on a server. One constraint shapes every browser deployment: the client reaches the server’s OAuth and message endpoints with fetch, so they must be reachable cross-origin. A FastMCP server’s OAuth mode does not emit CORS headers, so a browser on a different origin is stopped by the same-origin policy before a request is even sent. Serve your app from the same origin as the server, or place a CORS-aware proxy in front of it.