This is the documentation for the v2 beta — looking for the v1 documentation?
Skip to content

Authenticate without a user

Protecting a server you run → Require authorization. Authenticating an end user → OAuth. No user — a job, a backend, a service account → this page.

Authenticate with client credentials

ClientCredentialsProvider runs the OAuth client_credentials grant from a client_id and client_secret. Pass it as the transport's authProvider — every flow on this page plugs into that same option.

ts
import { Client, ClientCredentialsProvider, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';

const authProvider = new ClientCredentialsProvider({
    clientId: 'reporting-job',
    clientSecret: 'reporting-job-secret'
});

const client = new Client({ name: 'reporting-job', version: '1.0.0' });
const transport = new StreamableHTTPClientTransport(new URL('https://api.example.com/mcp'), { authProvider });

await client.connect(transport);

connect discovers the server's authorization server, posts the grant to its token endpoint, and attaches the access token to every request. On a 401 the provider refreshes the token and the transport retries once. No browser, no end user.

TIP

Pass expectedIssuer to pin the credential to the authorization server it was registered with. If discovery resolves a different issuer, the SDK throws AuthorizationServerMismatchError instead of sending the secret.

Bring your own bearer token

When something outside the SDK already owns the token — an API key, a gateway, a platform secret store — implement AuthProvider with only token().

ts
const authProvider: AuthProvider = { token: async () => getStoredToken() };

const transport = new StreamableHTTPClientTransport(new URL('https://api.example.com/mcp'), { authProvider });

The transport calls token() before every request and sets the Authorization header from whatever it returns. Without onUnauthorized, a 401 throws UnauthorizedError. Add onUnauthorized(ctx) to refresh the credential and the transport retries the request once.

Sign with a private key instead of a secret

PrivateKeyJwtProvider runs the same client_credentials grant, but authenticates the token request with a signed JWT assertion (private_key_jwt, RFC 7523) in place of a shared secret.

ts
const authProvider = new PrivateKeyJwtProvider({
    clientId: 'reporting-job',
    privateKey: pemEncodedKey,
    algorithm: 'RS256'
});

const transport = new StreamableHTTPClientTransport(new URL('https://api.example.com/mcp'), { authProvider });

privateKey accepts a PEM string, a Uint8Array, or a JWK object. The provider signs a fresh assertion for every token request; jwtLifetimeSeconds overrides the 300-second default, and claims merges extra claims into the assertion.

Act for an enterprise user with cross-app access

Cross-app access (Enterprise Managed Authorization, SEP-990) lets a service reach an MCP server for a user who already authenticated with the enterprise IdP, with no second consent screen. Two exchanges get it there: the IdP ID Token becomes a JWT Authorization Grant (RFC 8693), and that grant becomes an MCP access token (RFC 7523).

CrossAppAccessProvider runs the second exchange. Your assertion callback supplies the grant — here discoverAndRequestJwtAuthGrant performs the first exchange against the IdP.

ts
const authProvider = new CrossAppAccessProvider({
    assertion: async ctx => {
        const grant = await discoverAndRequestJwtAuthGrant({
            idpUrl: 'https://idp.example.com',
            audience: ctx.authorizationServerUrl,
            resource: ctx.resourceUrl,
            idToken: await getIdToken(),
            clientId: 'idp-exchange-client',
            clientSecret: 'idp-exchange-secret',
            scope: ctx.scope,
            fetchFn: ctx.fetchFn
        });
        return grant.jwtAuthGrant;
    },
    clientId: 'reporting-job',
    clientSecret: 'reporting-job-secret'
});

const transport = new StreamableHTTPClientTransport(new URL('https://api.example.com/mcp'), { authProvider });

The SDK discovers the MCP server's authorization server and resource URL (RFC 9728) before it calls assertion, then hands them in on ctx together with the negotiated scope and the transport's fetchFn. Pass them through so the IdP issues a grant bound to the right audience and resource.

Drop to the token-exchange utilities

Both exchanges behind CrossAppAccessProvider are exported as standalone functions for flows the provider does not cover — caching grants across transports, a non-standard IdP step, your own token store.

  • requestJwtAuthorizationGrant exchanges an ID Token for a JWT Authorization Grant at a known IdP token endpoint (RFC 8693).
  • discoverAndRequestJwtAuthGrant performs the same exchange, discovering the IdP's token endpoint from idpUrl first.
  • exchangeJwtAuthGrant exchanges a JWT Authorization Grant for an access token at the MCP server's authorization server (RFC 7523).

All three live in client/crossAppAccess in the API reference.

Recap

  • Every flow on this page plugs in through the same authProvider option on StreamableHTTPClientTransport.
  • ClientCredentialsProvider runs the client_credentials grant with a shared secret; PrivateKeyJwtProvider runs the same grant with a signed JWT assertion in its place.
  • An AuthProvider with only token() is enough when something outside the SDK owns the token; without onUnauthorized, a 401 throws UnauthorizedError.
  • CrossAppAccessProvider chains an enterprise IdP token through a JWT Authorization Grant to an MCP access token (SEP-990), and both exchanges are exported standalone.
  • Authenticating an end user belongs on OAuth.