Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 SDK ships several runnable server examples under `examples/server/src`. Start from the server examples index in [`examples/server/README.md`](../examples/server/README.md) and the entry-point quick start in the root [`README.md`](../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
1 change: 0 additions & 1 deletion examples/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,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
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
95 changes: 0 additions & 95 deletions examples/shared/src/authMiddleware.ts

This file was deleted.

57 changes: 14 additions & 43 deletions examples/shared/src/authServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
* See: https://www.better-auth.com/docs/plugins/mcp
*/

import type { OAuthTokenVerifier } from '@modelcontextprotocol/express';
import type { AuthInfo } from '@modelcontextprotocol/server';
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/server';
import { toNodeHandler } from 'better-auth/node';
import { oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata } from 'better-auth/plugins';
import cors from 'cors';
Expand All @@ -21,7 +24,6 @@ import { createDemoAuth, DEMO_USER_CREDENTIALS } from './auth.js';
export interface SetupAuthServerOptions {
authServerUrl: URL;
mcpServerUrl: URL;
strictResource?: boolean;
/**
* Examples should be used for **demo** only and not for production purposes, however this mode disables some logging and other features.
*/
Expand Down Expand Up @@ -284,60 +286,29 @@ export function createProtectedResourceMetadataRouter(resourcePath = '/mcp'): Ro
}

/**
* Verifies an access token using better-auth's getMcpSession.
* This can be used by MCP servers to validate tokens.
* Demo {@link OAuthTokenVerifier} backed by better-auth's `getMcpSession`.
* Pass this to `requireBearerAuth({ verifier: demoTokenVerifier, ... })` from
* `@modelcontextprotocol/express` to validate Bearer tokens against the demo
* Authorization Server started by `setupAuthServer`.
*/
export async function verifyAccessToken(
token: string,
options?: { strictResource?: boolean; expectedResource?: URL }
): Promise<{
token: string;
clientId: string;
scopes: string[];
expiresAt: number;
}> {
const auth = getAuth();
export const demoTokenVerifier: OAuthTokenVerifier = {
async verifyAccessToken(token: string): Promise<AuthInfo> {
const auth = getAuth();

try {
// Create a mock request with the Authorization header
const headers = new Headers();
headers.set('Authorization', `Bearer ${token}`);

// Use better-auth's getMcpSession API
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const session = await (auth.api as any).getMcpSession({
headers
});

const session = await (auth.api as any).getMcpSession({ headers });
if (!session) {
throw new Error('Invalid token');
throw new OAuthError(OAuthErrorCode.InvalidToken, 'Invalid token');
}

// OAuthAccessToken has:
// - accessToken, refreshToken: string
// - accessTokenExpiresAt, refreshTokenExpiresAt: Date
// - clientId, userId: string
// - scopes: string (space-separated)
const scopes = typeof session.scopes === 'string' ? session.scopes.split(' ') : ['openid'];
const expiresAt = session.accessTokenExpiresAt
? Math.floor(new Date(session.accessTokenExpiresAt).getTime() / 1000)
: Math.floor(Date.now() / 1000) + 3600;

// Note: better-auth's OAuthAccessToken doesn't have a resource field
// Resource validation would need to be done at a different layer
if (options?.strictResource && options.expectedResource) {
// For now, we skip resource validation as it's not in the session
// In production, you'd store and validate this separately
console.warn('[Auth] Resource validation requested but not available in better-auth session');
}

return {
token,
clientId: session.clientId,
scopes,
expiresAt
};
} catch (error) {
throw new Error(`Token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
return { token, clientId: session.clientId, scopes, expiresAt };
}
}
};
8 changes: 2 additions & 6 deletions examples/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
export type { CreateDemoAuthOptions, DemoAuth } from './auth.js';
export { createDemoAuth } from './auth.js';

// Auth middleware
export type { RequireBearerAuthOptions } from './authMiddleware.js';
export { getOAuthProtectedResourceMetadataUrl, requireBearerAuth } from './authMiddleware.js';

// Auth server setup
// Auth server setup + demo token verifier (pass to `requireBearerAuth` from @modelcontextprotocol/express)
export type { SetupAuthServerOptions } from './authServer.js';
export { createProtectedResourceMetadataRouter, getAuth, setupAuthServer, verifyAccessToken } from './authServer.js';
export { createProtectedResourceMetadataRouter, demoTokenVerifier, getAuth, setupAuthServer } from './authServer.js';
1 change: 1 addition & 0 deletions examples/shared/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"paths": {
"*": ["./*"],
"@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"],
"@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"],
"@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"],
"@modelcontextprotocol/core": [
"./node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/src/index.ts"
Expand Down
Loading
Loading