Skip to content

Commit b4e9cd2

Browse files
Merge branch 'main' into fweinberger/cross-bundle-error-instanceof
2 parents 05d7e6e + 43c13c5 commit b4e9cd2

18 files changed

Lines changed: 623 additions & 130 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@modelcontextprotocol/server': minor
3+
'@modelcontextprotocol/express': patch
4+
'@modelcontextprotocol/codemod': patch
5+
---
6+
7+
Add runtime-neutral Bearer authentication to `@modelcontextprotocol/server`:
8+
`requireBearerAuth` gates web-standard `fetch(request)` hosts (Cloudflare
9+
Workers, Deno, Bun, Hono), built on the exported `verifyBearerToken` and
10+
`bearerAuthChallengeResponse` pieces, with `OAuthTokenVerifier` now defined
11+
here. The Express middleware adapts the same core and is unchanged in
12+
behavior, except that `WWW-Authenticate` challenge values are now RFC 7235
13+
quoted-string sanitized (quotes and backslashes escaped, control and
14+
non-ASCII characters replaced); `@modelcontextprotocol/express` re-exports
15+
`OAuthTokenVerifier` as before.

docs/.vitepress/config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export default defineConfig({
3030
title: 'MCP TypeScript SDK',
3131
description: 'The TypeScript SDK implementation of the Model Context Protocol specification.',
3232
base: '/v2/',
33+
// VitePress does not base-prefix head hrefs, so /v2/ is spelled out here.
34+
head: [['link', { rel: 'icon', type: 'image/svg+xml', href: '/v2/favicon.svg' }]],
3335
srcExclude: ['v1/**', '_meta/**', 'behavior-surface-pins.md'],
3436
sitemap: { hostname: `${siteUrl}/` },
3537
markdown: {

docs/.vitepress/theme/custom.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@
1313
* which forces horizontal scrolling on most of our ~100-column code blocks.
1414
* Let the layout breathe and the column grow so typical snippets fit;
1515
* genuinely long lines still scroll inside their own block.
16+
*
17+
* Clamped to the viewport because the default theme's `(min-width: 1440px)`
18+
* rules size the sidebar and nav title as
19+
* `calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + ...)`, which goes
20+
* negative on viewports narrower than the max width: the sidebar collapses
21+
* and the site title spills over the search bar. With the clamp, those rules
22+
* resolve to their stock sub-1440px values on narrower viewports instead.
1623
*/
1724
:root {
18-
--vp-layout-max-width: 1680px;
25+
--vp-layout-max-width: min(100vw, 1680px);
1926
}
2027

2128
/* !important: the default rule is a scoped component style ([data-v-…]), which

docs/migration/upgrade-to-v2.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ The codemod ([`@modelcontextprotocol/codemod`](https://github.com/modelcontextpr
6464
mechanically applies every rename whose mapping is fixed. The mappings are the
6565
**source of truth** — they live in the codemod package and are not reproduced here:
6666

67-
| Mapping | Source file |
68-
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
67+
| Mapping | Source file |
68+
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
6969
| `@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) |
7070
| 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) |
7171
| `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:
395395
import path changes.
396396
- **Server auth split.** Resource Server helpers (`requireBearerAuth`,
397397
`mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`)
398-
→ `@modelcontextprotocol/express`. Authorization Server helpers (`mcpAuthRouter`,
398+
→ `@modelcontextprotocol/express`; the runtime-neutral core (`requireBearerAuth`
399+
for web-standard `fetch` hosts, `verifyBearerToken`, `bearerAuthChallengeResponse`,
400+
`OAuthTokenVerifier`) is also exported from `@modelcontextprotocol/server`. Authorization Server helpers (`mcpAuthRouter`,
399401
`OAuthServerProvider`, `ProxyOAuthServerProvider`, `allowedMethods`,
400402
`authenticateClient`, `metadataHandler`, `createOAuthMetadata`,
401403
`authorizationHandler` / `tokenHandler` / `revocationHandler` /
@@ -1055,7 +1057,8 @@ if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient)
10551057
```
10561058
10571059
**Token verifiers must throw the v2 `OAuthError`.** `requireBearerAuth` (from
1058-
`@modelcontextprotocol/express`) classifies the error your
1060+
`@modelcontextprotocol/express`, or from `@modelcontextprotocol/server` on
1061+
web-standard hosts) classifies the error your
10591062
`OAuthTokenVerifier.verifyAccessToken()` throws: a v2
10601063
`OAuthError(OAuthErrorCode.InvalidToken)` produces the proper `401` +
10611064
`WWW-Authenticate` challenge, while the legacy `InvalidTokenError` (from

docs/public/favicon.svg

Lines changed: 11 additions & 0 deletions
Loading

docs/serving/authorization.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
shape: how-to
33
description: 'Require a bearer token on a server you run: verification, protected-resource metadata, and per-tool scopes.'
44
---
5+
56
# Require authorization
67

78
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
4243
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.
4344
:::
4445

46+
## Require a bearer token on a web-standard host
47+
48+
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`.
49+
50+
```ts source="../../examples/guides/serving/authorization.web.examples.ts#requireBearerAuth_webStandard"
51+
const gate = requireBearerAuth({ verifier, requiredScopes: ['mcp'] });
52+
const handler = createMcpHandler(buildServer);
53+
54+
export default {
55+
async fetch(request: Request): Promise<Response> {
56+
const auth = await gate(request);
57+
if (auth instanceof Response) return auth;
58+
return handler.fetch(request, { authInfo: auth });
59+
}
60+
};
61+
```
62+
63+
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.
64+
4565
## Verify tokens your way
4666

4767
`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
107127

108128
## Recap
109129

130+
- `requireBearerAuth` from `@modelcontextprotocol/server` is the same gate for web-standard `fetch` hosts; the Express middleware adapts the same core.
110131
- `requireBearerAuth` plus a `verifyAccessToken` you write turn an Express-mounted MCP route into an OAuth resource server; the SDK never issues tokens.
111132
- 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.
112133
- `mcpAuthMetadataRouter` publishes the RFC 9728 document that challenge points at, plus a mirror of the AS metadata.

docs/v1/.vitepress/config.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export default defineConfig({
2424
title: 'MCP TypeScript SDK (v1)',
2525
description: 'Documentation for v1.x of the MCP TypeScript SDK.',
2626
base: '/',
27+
// The favicon is copied into content/public/ by scripts/build-docs-site.sh
28+
// (content/ is generated; the committed source is docs/public/favicon.svg).
29+
head: [['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }]],
2730
sitemap: { hostname: 'https://ts.sdk.modelcontextprotocol.io' },
2831
srcDir: 'content',
2932
markdown: {

examples/guides/serving/authorization.examples.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
/**
33
* Companion example for `docs/serving/authorization.md`.
44
*
5-
* Every `ts` fence on that page is synced from a `//#region` in this file
5+
* Every `ts` fence on that page except the web-standard one (sourced from
6+
* `authorization.web.examples.ts`) is synced from a `//#region` in this file
67
* (`pnpm sync:snippets --check`). The Express middleware needs a listening
78
* HTTP server and a real authorization server to exercise, so this file only
89
* typechecks:
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// docs: typecheck-only
2+
/**
3+
* Companion example for the web-standard section of
4+
* `docs/serving/authorization.md`.
5+
*
6+
* Lives beside `authorization.examples.ts` in its own module because both
7+
* packages export a `requireBearerAuth`: the Express one is demonstrated
8+
* there, the web-standard one from `@modelcontextprotocol/server` here. Like
9+
* its sibling, this file only typechecks:
10+
*
11+
* pnpm --filter @modelcontextprotocol/examples typecheck
12+
*
13+
* @module
14+
*/
15+
import type { AuthInfo, OAuthTokenVerifier } from '@modelcontextprotocol/server';
16+
import { createMcpHandler, McpServer, OAuthError, OAuthErrorCode, requireBearerAuth } from '@modelcontextprotocol/server';
17+
18+
declare function verifyJwt(token: string): Promise<{ sub: string; scopes: string[]; exp: number }>;
19+
20+
const verifier: OAuthTokenVerifier = {
21+
async verifyAccessToken(token): Promise<AuthInfo> {
22+
const payload = await verifyJwt(token).catch(() => {
23+
throw new OAuthError(OAuthErrorCode.InvalidToken, 'unknown token');
24+
});
25+
return { token, clientId: payload.sub, scopes: payload.scopes, expiresAt: payload.exp };
26+
}
27+
};
28+
29+
function buildServer(): McpServer {
30+
return new McpServer({ name: 'protected-server', version: '1.0.0' });
31+
}
32+
33+
//#region requireBearerAuth_webStandard
34+
const gate = requireBearerAuth({ verifier, requiredScopes: ['mcp'] });
35+
const handler = createMcpHandler(buildServer);
36+
37+
export default {
38+
async fetch(request: Request): Promise<Response> {
39+
const auth = await gate(request);
40+
if (auth instanceof Response) return auth;
41+
return handler.fetch(request, { authInfo: auth });
42+
}
43+
};
44+
//#endregion requireBearerAuth_webStandard

packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ export interface ImportMapping {
3434
}
3535

3636
/**
37-
* Resource-server auth helpers whose maintained v2 home is `@modelcontextprotocol/express`;
38-
* the server-legacy/auth copy they route to by default is a frozen v1 snapshot, so import
37+
* Resource-server auth helpers whose maintained v2 home is `@modelcontextprotocol/express`
38+
* (with the runtime-neutral core — `requireBearerAuth` for web-standard hosts and
39+
* `OAuthTokenVerifier` — also exported from `@modelcontextprotocol/server`); the
40+
* server-legacy/auth copy they route to by default is a frozen v1 snapshot, so import
3941
* and re-export sites get a marker prompting a deliberate re-point.
4042
*/
4143
export const RS_ONLY_AUTH_SYMBOLS: ReadonlySet<string> = new Set([
@@ -141,7 +143,8 @@ export const IMPORT_MAP: Record<string, ImportMapping> = {
141143
'@modelcontextprotocol/sdk/server/auth/provider.js': {
142144
target: '@modelcontextprotocol/server-legacy/auth',
143145
status: 'moved',
144-
migrationHint: 'Legacy OAuth AS provider. For RS-only auth, see requireBearerAuth from @modelcontextprotocol/express.'
146+
migrationHint:
147+
'Legacy OAuth AS provider. For RS-only auth, see requireBearerAuth from @modelcontextprotocol/express (or, on web-standard hosts, from @modelcontextprotocol/server).'
145148
},
146149
'@modelcontextprotocol/sdk/server/auth/router.js': {
147150
target: '@modelcontextprotocol/server-legacy/auth',
@@ -151,7 +154,8 @@ export const IMPORT_MAP: Record<string, ImportMapping> = {
151154
'@modelcontextprotocol/sdk/server/auth/middleware.js': {
152155
target: '@modelcontextprotocol/server-legacy/auth',
153156
status: 'moved',
154-
migrationHint: 'Legacy OAuth AS middleware. For bearer-only auth, see requireBearerAuth from @modelcontextprotocol/express.'
157+
migrationHint:
158+
'Legacy OAuth AS middleware. For bearer-only auth, see requireBearerAuth from @modelcontextprotocol/express (or, on web-standard hosts, from @modelcontextprotocol/server).'
155159
},
156160
'@modelcontextprotocol/sdk/server/auth/errors.js': {
157161
target: '@modelcontextprotocol/server-legacy/auth',

0 commit comments

Comments
 (0)