OAuth 2.0 client_credentials grant — machine-to-machine MCP auth, fully self-verifying with no browser.
client_credentials is the grant a backend service uses to authenticate as itself (not on behalf of a user): it presents a pre-registered client_id/client_secret directly to the Authorization Server's token endpoint and receives a Bearer access token. There is no
redirect, no authorization code, no user consent screen.
The interactive authorization-code flow (the one that opens a browser and asks a human to sign in) lives under ../oauth/; the runner drives it headlessly via the demo AS's OAUTH_DEMO_AUTO_CONSENT=1 auto-approve mode.
server.tsstarts two listeners in one process:- the MCP resource server on
--port—createMcpHandlerbehindrequireBearerAuthfrom@modelcontextprotocol/express, advertising the AS viamcpAuthMetadataRouter(RFC 9728 + RFC 8414). - a minimal
client_credentials-only Authorization Server on--port + 1(createClientCredentialsAuthServerfrom@mcp-examples/shared). The repo's full better-auth/OIDC demo AS only implementsauthorization_code, so this story ships its own purpose-built AS.
- the MCP resource server on
client.tsfirst asserts a bare request is401with aWWW-Authenticatechallenge, then connects with aClientCredentialsProvideron the transport. The SDK auth driver discovers the AS from the challenge, postsgrant_type=client_credentials(HTTP Basic auth) to/token, attaches the returned Bearer token, and thewhoamitool'sctx.authInfocarries the grantedclientIdandscopesend to end.
pnpm --filter @mcp-examples/oauth-client-credentials server -- --http --port 3000
pnpm --filter @mcp-examples/oauth-client-credentials client -- --http http://127.0.0.1:3000/mcpHTTP-only; runs on both protocol eras (the client honours --legacy via parseExampleArgs().era).
To authenticate the client_credentials grant with a signed JWT assertion (RFC 7523 §2.2) instead of a shared secret, swap ClientCredentialsProvider for PrivateKeyJwtProvider:
import { PrivateKeyJwtProvider } from '@modelcontextprotocol/client';
const authProvider = new PrivateKeyJwtProvider({
clientId: 'my-service',
privateKey: pemEncodedKey,
algorithm: 'RS256'
});The full snippet lives in Machine auth › Sign with a private key instead of a secret (guides/clients/machine-auth.examples.ts → privateKeyJwt_provider). There is no runnable leg for it in this story — the in-repo client_credentials AS only implements client_secret_basic/client_secret_post.