feat(server): runtime-neutral requireBearerAuth for web-standard hosts (#2420)

This commit is contained in:
Felix Weinberger 2026-07-02 19:35:21 +01:00 • committed by GitHub
commit 7635115d01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 595 additions and 129 deletions

View file

@ -64,8 +64,8 @@ The codemod ([`@modelcontextprotocol/codemod`](https://github.com/modelcontextpr
mechanically applies every rename whose mapping is fixed. The mappings are the
**source of truth** — they live in the codemod package and are not reproduced here:
| Mapping | Source file |
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Mapping | Source file |
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@modelcontextprotocol/sdk/...` import paths → v2 packages | [`mappings/importMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts) |
| Symbol renames (`McpError` → `ProtocolError`, `JSONRPCError` → `JSONRPCErrorResponse`, …) | [`mappings/symbolMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts) |
| `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)` | [`mappings/schemaToMethodMap.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts) |
@ -395,7 +395,9 @@ A few transports need a decision the codemod can't make:
import path changes.
- **Server auth split.** Resource Server helpers (`requireBearerAuth`,
`mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`)
→ `@modelcontextprotocol/express`. Authorization Server helpers (`mcpAuthRouter`,
→ `@modelcontextprotocol/express`; the runtime-neutral core (`requireBearerAuth`
for web-standard `fetch` hosts, `verifyBearerToken`, `bearerAuthChallengeResponse`,
`OAuthTokenVerifier`) is also exported from `@modelcontextprotocol/server`. Authorization Server helpers (`mcpAuthRouter`,
`OAuthServerProvider`, `ProxyOAuthServerProvider`, `allowedMethods`,
`authenticateClient`, `metadataHandler`, `createOAuthMetadata`,
`authorizationHandler` / `tokenHandler` / `revocationHandler` /
@ -1036,7 +1038,8 @@ if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient)
```
⚠ **Token verifiers must throw the v2 `OAuthError`.** `requireBearerAuth` (from
`@modelcontextprotocol/express`) classifies the error your
`@modelcontextprotocol/express`, or from `@modelcontextprotocol/server` on
web-standard hosts) classifies the error your
`OAuthTokenVerifier.verifyAccessToken()` throws: a v2
`OAuthError(OAuthErrorCode.InvalidToken)` produces the proper `401` +
`WWW-Authenticate` challenge, while the legacy `InvalidTokenError` (from

View file

@ -2,6 +2,7 @@
shape: how-to
description: 'Require a bearer token on a server you run: verification, protected-resource metadata, and per-tool scopes.'
---
# Require authorization
Protecting a server you run → this page. Signing a user in from a client you build → [Authenticate a user with OAuth](../clients/oauth.md). No user present → [Authenticate without a user](../clients/machine-auth.md).
@ -42,6 +43,25 @@ A request with a missing, malformed, or expired token gets `401` with the OAuth
The Authorization Server helpers (`mcpAuthRouter`, `ProxyOAuthServerProvider`, …) are frozen in `@modelcontextprotocol/server-legacy/auth`. Use a dedicated identity provider for new servers; this page only covers the resource-server half.
:::
## Require a bearer token on a web-standard host
On hosts whose HTTP surface is a `fetch(request)` handler — Cloudflare Workers, Deno, Bun, Hono — the gate is `requireBearerAuth` from `@modelcontextprotocol/server`: no framework, only web-standard `Request` and `Response`.
```ts source="../../examples/guides/serving/authorization.web.examples.ts#requireBearerAuth_webStandard"
const gate = requireBearerAuth({ verifier, requiredScopes: ['mcp'] });
const handler = createMcpHandler(buildServer);
export default {
async fetch(request: Request): Promise<Response> {
const auth = await gate(request);
if (auth instanceof Response) return auth;
return handler.fetch(request, { authInfo: auth });
}
};
```
The gate resolves to the verified `AuthInfo` — pass it to the handler as `{ authInfo }` and handlers read it as `ctx.http.authInfo` — or to the ready-to-return challenge `Response`. Status codes, error bodies, and the `WWW-Authenticate` challenge (including `resourceMetadataUrl`) are identical to the Express middleware: both are adapters over one core, so a verifier written for one serves the other unchanged.
## Verify tokens your way
`verifyAccessToken` is the one function you supply: take the raw token string, return an `AuthInfo`. Local JWT verification, [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) introspection, or a call to your identity provider all fit behind it.
@ -107,6 +127,7 @@ Responding `403 insufficient_scope` at the HTTP layer instead triggers the clien
## Recap
- `requireBearerAuth` from `@modelcontextprotocol/server` is the same gate for web-standard `fetch` hosts; the Express middleware adapts the same core.
- `requireBearerAuth` plus a `verifyAccessToken` you write turn an Express-mounted MCP route into an OAuth resource server; the SDK never issues tokens.
- Missing, invalid, or expired tokens get `401 invalid_token`; a token missing a `requiredScopes` entry gets `403 insufficient_scope`; both carry a `WWW-Authenticate: Bearer` challenge.
- `mcpAuthMetadataRouter` publishes the RFC 9728 document that challenge points at, plus a mirror of the AS metadata.