Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/express-resource-server-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/express': minor
---

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.
4 changes: 2 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ For production use, you can either:

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).

### Why did we remove `server` auth exports?
### Where are the server auth helpers?

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`.
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`.

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

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

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

### Server-side auth

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
`examples/server/src/` for demos.
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.

### Host header validation (Express)

Expand Down Expand Up @@ -502,6 +501,6 @@ Access validators explicitly:
6. Replace plain header objects with `new Headers({...})` and bracket access (`headers['x']`) with `.get()` calls per section 7
7. If using `hostHeaderValidation` from server, update import and signature per section 8
8. If using server SSE transport, migrate to Streamable HTTP
9. If using server auth from the SDK, migrate to an external auth library
9. If using server auth from the SDK: RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`) → `@modelcontextprotocol/express`; AS helpers → external IdP/OAuth library
10. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true`
11. Verify: build with `tsc` / run tests
6 changes: 3 additions & 3 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
```

### Server auth removed
### Server auth split

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

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`.
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`.

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

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

```bash
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/simpleStreamableHttp.ts --oauth
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/simpleStreamableHttp.ts --oauth --oauth-strict
```

## URL elicitation example (server + client)
Expand Down
16 changes: 5 additions & 11 deletions examples/server/src/elicitationUrlExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,8 @@

import { randomUUID } from 'node:crypto';

import {
createProtectedResourceMetadataRouter,
getOAuthProtectedResourceMetadataUrl,
requireBearerAuth,
setupAuthServer
} from '@modelcontextprotocol/examples-shared';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { createProtectedResourceMetadataRouter, demoTokenVerifier, setupAuthServer } from '@modelcontextprotocol/examples-shared';
import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBearerAuth } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult, ElicitRequestURLParams, ElicitResult } from '@modelcontextprotocol/server';
import { isInitializeRequest, McpServer, UrlElicitationRequiredError } from '@modelcontextprotocol/server';
Expand Down Expand Up @@ -235,18 +230,17 @@ let authMiddleware = null;
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);

setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true, demoMode: true });
setupAuthServer({ authServerUrl, mcpServerUrl, demoMode: true });

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

authMiddleware = requireBearerAuth({
verifier: demoTokenVerifier,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
strictResource: true,
expectedResource: mcpServerUrl
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
});

/**
Expand Down
87 changes: 87 additions & 0 deletions examples/server/src/resourceServerOnly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Minimal Resource-Server-only auth using the SDK's RS helpers
* (`mcpAuthMetadataRouter`, `requireBearerAuth`, `OAuthTokenVerifier`).
*
* No better-auth. The Authorization Server is external; this example points
* its metadata at a placeholder issuer. For a full AS+RS setup with a real
* demo Authorization Server, see {@link ./simpleStreamableHttp.ts}.
*
* Run: pnpm tsx src/resourceServerOnly.ts
* Probe: curl http://localhost:3000/.well-known/oauth-protected-resource/mcp
* curl -H 'Authorization: Bearer demo-token' -X POST http://localhost:3000/mcp ...
*/

import type { OAuthTokenVerifier } from '@modelcontextprotocol/express';
import {
createMcpExpressApp,
getOAuthProtectedResourceMetadataUrl,
mcpAuthMetadataRouter,
requireBearerAuth
} from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { AuthInfo, CallToolResult, OAuthMetadata } from '@modelcontextprotocol/server';
import { McpServer, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import * as z from 'zod/v4';

const PORT = 3000;
const mcpServerUrl = new URL(`http://localhost:${PORT}/mcp`);

// In a real deployment this is your external Authorization Server's metadata
// (RFC 8414). The SDK router serves it verbatim at
// /.well-known/oauth-authorization-server so clients probing the RS origin
// can still discover the AS.
const oauthMetadata: OAuthMetadata = {
issuer: 'https://auth.example.com',
authorization_endpoint: 'https://auth.example.com/authorize',
token_endpoint: 'https://auth.example.com/token',
response_types_supported: ['code']
};

// Replace with JWT verification, RFC 7662 introspection, etc.
const staticTokenVerifier: OAuthTokenVerifier = {
async verifyAccessToken(token): Promise<AuthInfo> {
if (token !== 'demo-token') {
throw new OAuthError(OAuthErrorCode.InvalidToken, 'unknown token');
}
return { token, clientId: 'demo-client', scopes: ['mcp'], expiresAt: Math.floor(Date.now() / 1000) + 3600 };
}
};

const server = new McpServer({ name: 'rs-only', version: '1.0.0' }, { capabilities: {} });
server.registerTool(
'whoami',
{ description: 'Returns the authenticated subject.', inputSchema: z.object({}) },
async (_args, ctx): Promise<CallToolResult> => ({
content: [{ type: 'text', text: `client=${ctx.http?.authInfo?.clientId ?? 'anon'}` }]
})
);

const app = createMcpExpressApp();

app.use(
mcpAuthMetadataRouter({
oauthMetadata,
resourceServerUrl: mcpServerUrl,
resourceName: 'RS-only example'
})
);

const auth = requireBearerAuth({
verifier: staticTokenVerifier,
requiredScopes: ['mcp'],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
});

app.post('/mcp', auth, async (req: Request, res: Response) => {
Comment thread
felixweinberger marked this conversation as resolved.
Dismissed
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on('close', () => void transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});

app.listen(PORT, () => {
console.log(`RS-only MCP server on http://localhost:${PORT}/mcp`);
console.log(` PRM: ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`);
console.log(` AS metadata mirror: http://localhost:${PORT}/.well-known/oauth-authorization-server`);
});
25 changes: 9 additions & 16 deletions examples/server/src/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { randomUUID } from 'node:crypto';

import {
createProtectedResourceMetadataRouter,
getOAuthProtectedResourceMetadataUrl,
requireBearerAuth,
setupAuthServer
} from '@modelcontextprotocol/examples-shared';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { createProtectedResourceMetadataRouter, demoTokenVerifier, setupAuthServer } from '@modelcontextprotocol/examples-shared';
import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBearerAuth } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type {
CallToolResult,
Expand All @@ -25,7 +20,6 @@ import { InMemoryEventStore } from './inMemoryEventStore.js';

// Check for OAuth flag
const useOAuth = process.argv.includes('--oauth');
Comment thread
claude[bot] marked this conversation as resolved.
const strictOAuth = process.argv.includes('--oauth-strict');
const dangerousLoggingEnabled = process.argv.includes('--dangerous-logging-enabled');

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

setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth, demoMode: true, dangerousLoggingEnabled });
setupAuthServer({ authServerUrl, mcpServerUrl, demoMode: true, dangerousLoggingEnabled });

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

authMiddleware = requireBearerAuth({
verifier: demoTokenVerifier,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
strictResource: strictOAuth,
expectedResource: mcpServerUrl
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
});
}

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

if (useOAuth && req.app.locals.auth) {
console.log('Authenticated user:', req.app.locals.auth);
if (useOAuth && req.auth) {
console.log('Authenticated user:', req.auth);
}
try {
let transport: NodeStreamableHTTPServerTransport;
Expand Down Expand Up @@ -742,8 +735,8 @@ const mcpGetHandler = async (req: Request, res: Response) => {
return;
}

if (useOAuth && req.app.locals.auth) {
console.log('Authenticated SSE connection from user:', req.app.locals.auth);
if (useOAuth && req.auth) {
console.log('Authenticated SSE connection from user:', req.auth);
}

// Check for Last-Event-ID header for resumability
Expand Down
Loading
Loading