Implement MCP 2026-07-28 (#2286)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Felix Weinberger 2026-06-26 14:01:10 +01:00 • committed by GitHub
commit 1823aae891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,41 @@
# oauth-client-credentials
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/`](../oauth/README.md); the runner drives it headlessly via the demo AS's `OAUTH_DEMO_AUTO_CONSENT=1` auto-approve mode.
## What runs
- `server.ts` starts two listeners in one process:
- the MCP **resource server** on `--port` — `createMcpHandler` behind `requireBearerAuth` from `@modelcontextprotocol/express`, advertising the AS via `mcpAuthMetadataRouter` (RFC 9728 + RFC 8414).
- a minimal **`client_credentials`-only Authorization Server** on `--port + 1` (`createClientCredentialsAuthServer` from `@mcp-examples/shared`). The repo's full better-auth/OIDC demo AS only implements `authorization_code`, so this story ships its own purpose-built AS.
- `client.ts` first asserts a bare request is `401` with a `WWW-Authenticate` challenge, then connects with a `ClientCredentialsProvider` on the transport. The SDK auth driver discovers the AS from the challenge, posts `grant_type=client_credentials` (HTTP Basic auth) to
`/token`, attaches the returned Bearer token, and the `whoami` tool's `ctx.authInfo` carries the granted `clientId` and `scopes` end to end.
## Run it
```bash
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/mcp
```
HTTP-only; runs on both protocol eras (the client honours `--legacy` via `parseExampleArgs().era`).
## `private_key_jwt` client authentication
To authenticate the `client_credentials` grant with a signed JWT assertion (RFC 7523 §2.2) instead of a shared secret, swap `ClientCredentialsProvider` for `PrivateKeyJwtProvider`:
```ts
import { PrivateKeyJwtProvider } from '@modelcontextprotocol/client';
const authProvider = new PrivateKeyJwtProvider({
clientId: 'my-service',
privateKey: pemEncodedKey,
algorithm: 'RS256'
});
```
The full snippet lives in the client guide (`docs/client.md` → `auth_privateKeyJwt`). There is no runnable leg for it in this story — the in-repo `client_credentials` AS only implements `client_secret_basic`/`client_secret_post`.