feat(express): restore Resource-Server auth glue (requireBearerAuth, mcpAuthMetadataRouter) (#1907)

This commit is contained in:
Felix Weinberger 2026-04-28 15:18:33 +01:00 • committed by GitHub
commit 7cccc2aca8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 693 additions and 184 deletions

View file

@ -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';
@ -235,7 +230,7 @@ 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
@ -243,10 +238,9 @@ setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true, demoMode: t
app.use(createProtectedResourceMetadataRouter('/mcp'));
authMiddleware = requireBearerAuth({
verifier: demoTokenVerifier,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
strictResource: true,
expectedResource: mcpServerUrl
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
});
/**

View file

@ -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) => {
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`);
});

View file

@ -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,
@ -25,7 +20,6 @@ import { InMemoryEventStore } from './inMemoryEventStore.js';
// Check for OAuth flag
const useOAuth = process.argv.includes('--oauth');
const strictOAuth = process.argv.includes('--oauth-strict');
const dangerousLoggingEnabled = process.argv.includes('--dangerous-logging-enabled');
// Create shared task store for demonstration
@ -624,7 +618,7 @@ 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
@ -632,10 +626,9 @@ if (useOAuth) {
app.use(createProtectedResourceMetadataRouter('/mcp'));
authMiddleware = requireBearerAuth({
verifier: demoTokenVerifier,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
strictResource: strictOAuth,
expectedResource: mcpServerUrl
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
});
}
@ -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;
@ -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