Skip to content

Commit 5497a22

Browse files
Merge branch 'main' into fweinberger/custom-methods-minimal
2 parents 02c9801 + 7cccc2a commit 5497a22

20 files changed

Lines changed: 693 additions & 184 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/express': minor
3+
---
4+
5+
Add OAuth Resource-Server glue to the Express adapter: `requireBearerAuth` middleware (token verification + RFC 6750 `WWW-Authenticate` challenges), `mcpAuthMetadataRouter` (serves RFC 9728 Protected Resource Metadata and mirrors RFC 8414 AS metadata at the resource origin), the `getOAuthProtectedResourceMetadataUrl` helper, and the `OAuthTokenVerifier` interface. These restore the v1 `src/server/auth` Resource-Server pieces as first-class v2 API so MCP servers can plug into an external Authorization Server with a few lines of Express wiring.

docs/faq.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ For production use, you can either:
6969

7070
The [server quickstart](./server-quickstart.md) walks you through building a weather server from scratch. Its complete source lives in [`examples/server-quickstart/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/server-quickstart/). For more advanced examples (OAuth, streaming, sessions, etc.), see the server examples index in [`examples/server/README.md`](../examples/server/README.md).
7171

72-
### Why did we remove `server` auth exports?
72+
### Where are the server auth helpers?
7373

74-
Server authentication & authorization is outside of the scope of the SDK, and the recommendation is to use packages that focus on this area specifically (or a full-fledged Authorization Server for those who use such). Example packages provide an example with `better-auth`.
74+
Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `OAuthTokenVerifier`) are first-class in `@modelcontextprotocol/express`. The Authorization Server helpers (`mcpAuthRouter`, `ProxyOAuthServerProvider`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. Example packages provide a demo with `better-auth`.
7575

7676
### Why did we remove `server` SSE transport?
7777

docs/migration-SKILL.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table.
5454
| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server` |
5555
| `@modelcontextprotocol/sdk/server/streamableHttp.js` | `@modelcontextprotocol/node` (class renamed to `NodeStreamableHTTPServerTransport`) OR `@modelcontextprotocol/server` (web-standard `WebStandardStreamableHTTPServerTransport` for Cloudflare Workers, Deno, etc.) |
5656
| `@modelcontextprotocol/sdk/server/sse.js` | REMOVED (migrate to Streamable HTTP) |
57-
| `@modelcontextprotocol/sdk/server/auth/*` | REMOVED (use external auth library) |
57+
| `@modelcontextprotocol/sdk/server/auth/*` | RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `OAuthTokenVerifier`) → `@modelcontextprotocol/express`; AS helpers removed (use external IdP/OAuth library) |
5858
| `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) |
5959

6060
### Types / shared imports
@@ -319,8 +319,7 @@ new URL(ctx.http?.req?.url).searchParams.get('debug')
319319

320320
### Server-side auth
321321

322-
All server OAuth exports removed: `mcpAuthRouter`, `OAuthServerProvider`, `OAuthTokenVerifier`, `requireBearerAuth`, `authenticateClient`, `ProxyOAuthServerProvider`, `allowedMethods`, and associated types. Use an external auth library (e.g., `better-auth`). See
323-
`examples/server/src/` for demos.
322+
Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are first-class in `@modelcontextprotocol/express`. Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) are removed from the core SDK; use an external IdP/OAuth library. See `examples/server/src/` for demos.
324323

325324
### Host header validation (Express)
326325

@@ -522,6 +521,6 @@ Access validators explicitly:
522521
6. Replace plain header objects with `new Headers({...})` and bracket access (`headers['x']`) with `.get()` calls per section 7
523522
7. If using `hostHeaderValidation` from server, update import and signature per section 8
524523
8. If using server SSE transport, migrate to Streamable HTTP
525-
9. If using server auth from the SDK, migrate to an external auth library
524+
9. If using server auth from the SDK: RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`) → `@modelcontextprotocol/express`; AS helpers → external IdP/OAuth library
526525
10. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true`
527526
11. Verify: build with `tsc` / run tests

docs/migration.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,11 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
130130
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
131131
```
132132

133-
### Server auth removed
133+
### Server auth split
134134

135-
Server-side OAuth/auth has been removed entirely from the SDK. This includes `mcpAuthRouter`, `OAuthServerProvider`, `OAuthTokenVerifier`, `requireBearerAuth`, `authenticateClient`, `ProxyOAuthServerProvider`, `allowedMethods`, and all associated types.
135+
Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are now first-class in `@modelcontextprotocol/express`.
136136

137-
Use a dedicated auth library (e.g., `better-auth`) or a full Authorization Server instead. See the [examples](../examples/server/src/) for a working demo with `better-auth`.
137+
Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. See the [examples](../examples/server/src/) for a working demo with `better-auth`.
138138

139139
Note: `AuthInfo` has moved from `server/auth/types.ts` to the core types and is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`.
140140

examples/server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pnpm tsx src/simpleStreamableHttp.ts
2929
| ----------------------------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
3030
| Streamable HTTP server (stateful) | Feature-rich server with tools/resources/prompts, logging, tasks, sampling, and optional OAuth. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
3131
| Streamable HTTP server (stateless) | No session tracking; good for simple API-style servers. | [`src/simpleStatelessStreamableHttp.ts`](src/simpleStatelessStreamableHttp.ts) |
32+
| Resource-Server-only auth | Minimal OAuth RS using SDK's `mcpAuthMetadataRouter` + `requireBearerAuth` (no better-auth). | [`src/resourceServerOnly.ts`](src/resourceServerOnly.ts) |
3233
| JSON response mode (no SSE) | Streamable HTTP with JSON-only responses and limited notifications. | [`src/jsonResponseStreamableHttp.ts`](src/jsonResponseStreamableHttp.ts) |
3334
| Server notifications over Streamable HTTP | Demonstrates server-initiated notifications via GET+SSE. | [`src/standaloneSseWithGetStreamableHttp.ts`](src/standaloneSseWithGetStreamableHttp.ts) |
3435
| Output schema server | Demonstrates tool output validation with structured output schemas. | [`src/mcpServerOutputSchema.ts`](src/mcpServerOutputSchema.ts) |
@@ -43,7 +44,6 @@ pnpm tsx src/simpleStreamableHttp.ts
4344

4445
```bash
4546
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/simpleStreamableHttp.ts --oauth
46-
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/simpleStreamableHttp.ts --oauth --oauth-strict
4747
```
4848

4949
## URL elicitation example (server + client)

examples/server/src/elicitationUrlExample.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,8 @@
99

1010
import { randomUUID } from 'node:crypto';
1111

12-
import {
13-
createProtectedResourceMetadataRouter,
14-
getOAuthProtectedResourceMetadataUrl,
15-
requireBearerAuth,
16-
setupAuthServer
17-
} from '@modelcontextprotocol/examples-shared';
18-
import { createMcpExpressApp } from '@modelcontextprotocol/express';
12+
import { createProtectedResourceMetadataRouter, demoTokenVerifier, setupAuthServer } from '@modelcontextprotocol/examples-shared';
13+
import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBearerAuth } from '@modelcontextprotocol/express';
1914
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
2015
import type { CallToolResult, ElicitRequestURLParams, ElicitResult } from '@modelcontextprotocol/server';
2116
import { isInitializeRequest, McpServer, UrlElicitationRequiredError } from '@modelcontextprotocol/server';
@@ -235,18 +230,17 @@ let authMiddleware = null;
235230
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
236231
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
237232

238-
setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true, demoMode: true });
233+
setupAuthServer({ authServerUrl, mcpServerUrl, demoMode: true });
239234

240235
// Add protected resource metadata route to the MCP server
241236
// This allows clients to discover the auth server
242237
// Pass the resource path so metadata is served at /.well-known/oauth-protected-resource/mcp
243238
app.use(createProtectedResourceMetadataRouter('/mcp'));
244239

245240
authMiddleware = requireBearerAuth({
241+
verifier: demoTokenVerifier,
246242
requiredScopes: [],
247-
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
248-
strictResource: true,
249-
expectedResource: mcpServerUrl
243+
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
250244
});
251245

252246
/**
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Minimal Resource-Server-only auth using the SDK's RS helpers
3+
* (`mcpAuthMetadataRouter`, `requireBearerAuth`, `OAuthTokenVerifier`).
4+
*
5+
* No better-auth. The Authorization Server is external; this example points
6+
* its metadata at a placeholder issuer. For a full AS+RS setup with a real
7+
* demo Authorization Server, see {@link ./simpleStreamableHttp.ts}.
8+
*
9+
* Run: pnpm tsx src/resourceServerOnly.ts
10+
* Probe: curl http://localhost:3000/.well-known/oauth-protected-resource/mcp
11+
* curl -H 'Authorization: Bearer demo-token' -X POST http://localhost:3000/mcp ...
12+
*/
13+
14+
import type { OAuthTokenVerifier } from '@modelcontextprotocol/express';
15+
import {
16+
createMcpExpressApp,
17+
getOAuthProtectedResourceMetadataUrl,
18+
mcpAuthMetadataRouter,
19+
requireBearerAuth
20+
} from '@modelcontextprotocol/express';
21+
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
22+
import type { AuthInfo, CallToolResult, OAuthMetadata } from '@modelcontextprotocol/server';
23+
import { McpServer, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/server';
24+
import type { Request, Response } from 'express';
25+
import * as z from 'zod/v4';
26+
27+
const PORT = 3000;
28+
const mcpServerUrl = new URL(`http://localhost:${PORT}/mcp`);
29+
30+
// In a real deployment this is your external Authorization Server's metadata
31+
// (RFC 8414). The SDK router serves it verbatim at
32+
// /.well-known/oauth-authorization-server so clients probing the RS origin
33+
// can still discover the AS.
34+
const oauthMetadata: OAuthMetadata = {
35+
issuer: 'https://auth.example.com',
36+
authorization_endpoint: 'https://auth.example.com/authorize',
37+
token_endpoint: 'https://auth.example.com/token',
38+
response_types_supported: ['code']
39+
};
40+
41+
// Replace with JWT verification, RFC 7662 introspection, etc.
42+
const staticTokenVerifier: OAuthTokenVerifier = {
43+
async verifyAccessToken(token): Promise<AuthInfo> {
44+
if (token !== 'demo-token') {
45+
throw new OAuthError(OAuthErrorCode.InvalidToken, 'unknown token');
46+
}
47+
return { token, clientId: 'demo-client', scopes: ['mcp'], expiresAt: Math.floor(Date.now() / 1000) + 3600 };
48+
}
49+
};
50+
51+
const server = new McpServer({ name: 'rs-only', version: '1.0.0' }, { capabilities: {} });
52+
server.registerTool(
53+
'whoami',
54+
{ description: 'Returns the authenticated subject.', inputSchema: z.object({}) },
55+
async (_args, ctx): Promise<CallToolResult> => ({
56+
content: [{ type: 'text', text: `client=${ctx.http?.authInfo?.clientId ?? 'anon'}` }]
57+
})
58+
);
59+
60+
const app = createMcpExpressApp();
61+
62+
app.use(
63+
mcpAuthMetadataRouter({
64+
oauthMetadata,
65+
resourceServerUrl: mcpServerUrl,
66+
resourceName: 'RS-only example'
67+
})
68+
);
69+
70+
const auth = requireBearerAuth({
71+
verifier: staticTokenVerifier,
72+
requiredScopes: ['mcp'],
73+
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
74+
});
75+
76+
app.post('/mcp', auth, async (req: Request, res: Response) => {
77+
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
78+
res.on('close', () => void transport.close());
79+
await server.connect(transport);
80+
await transport.handleRequest(req, res, req.body);
81+
});
82+
83+
app.listen(PORT, () => {
84+
console.log(`RS-only MCP server on http://localhost:${PORT}/mcp`);
85+
console.log(` PRM: ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`);
86+
console.log(` AS metadata mirror: http://localhost:${PORT}/.well-known/oauth-authorization-server`);
87+
});

examples/server/src/simpleStreamableHttp.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { randomUUID } from 'node:crypto';
22

3-
import {
4-
createProtectedResourceMetadataRouter,
5-
getOAuthProtectedResourceMetadataUrl,
6-
requireBearerAuth,
7-
setupAuthServer
8-
} from '@modelcontextprotocol/examples-shared';
9-
import { createMcpExpressApp } from '@modelcontextprotocol/express';
3+
import { createProtectedResourceMetadataRouter, demoTokenVerifier, setupAuthServer } from '@modelcontextprotocol/examples-shared';
4+
import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBearerAuth } from '@modelcontextprotocol/express';
105
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
116
import type {
127
CallToolResult,
@@ -25,7 +20,6 @@ import { InMemoryEventStore } from './inMemoryEventStore.js';
2520

2621
// Check for OAuth flag
2722
const useOAuth = process.argv.includes('--oauth');
28-
const strictOAuth = process.argv.includes('--oauth-strict');
2923
const dangerousLoggingEnabled = process.argv.includes('--dangerous-logging-enabled');
3024

3125
// Create shared task store for demonstration
@@ -624,18 +618,17 @@ if (useOAuth) {
624618
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
625619
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
626620

627-
setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth, demoMode: true, dangerousLoggingEnabled });
621+
setupAuthServer({ authServerUrl, mcpServerUrl, demoMode: true, dangerousLoggingEnabled });
628622

629623
// Add protected resource metadata route to the MCP server
630624
// This allows clients to discover the auth server
631625
// Pass the resource path so metadata is served at /.well-known/oauth-protected-resource/mcp
632626
app.use(createProtectedResourceMetadataRouter('/mcp'));
633627

634628
authMiddleware = requireBearerAuth({
629+
verifier: demoTokenVerifier,
635630
requiredScopes: [],
636-
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
637-
strictResource: strictOAuth,
638-
expectedResource: mcpServerUrl
631+
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
639632
});
640633
}
641634

@@ -651,8 +644,8 @@ const mcpPostHandler = async (req: Request, res: Response) => {
651644
console.log('Request body:', req.body);
652645
}
653646

654-
if (useOAuth && req.app.locals.auth) {
655-
console.log('Authenticated user:', req.app.locals.auth);
647+
if (useOAuth && req.auth) {
648+
console.log('Authenticated user:', req.auth);
656649
}
657650
try {
658651
let transport: NodeStreamableHTTPServerTransport;
@@ -742,8 +735,8 @@ const mcpGetHandler = async (req: Request, res: Response) => {
742735
return;
743736
}
744737

745-
if (useOAuth && req.app.locals.auth) {
746-
console.log('Authenticated SSE connection from user:', req.app.locals.auth);
738+
if (useOAuth && req.auth) {
739+
console.log('Authenticated SSE connection from user:', req.auth);
747740
}
748741

749742
// Check for Last-Event-ID header for resumability

0 commit comments

Comments
 (0)