|
| 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 | +}); |
0 commit comments