import express from 'express'; import cors from 'cors'; import rateLimit from 'express-rate-limit'; import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'; import { EverythingAuthProvider } from '../src/auth/provider.js'; import { handleFakeAuthorize, handleFakeAuthorizeRedirect } from '../src/handlers/fakeauth.js'; import { redisClient } from '../src/redis.js'; import { logger } from '../src/utils/logger.js'; import { AUTH_SERVER_PORT, AUTH_SERVER_URL, BASE_URI } from '../src/config.js'; const app = express(); console.log('====================================='); console.log('MCP Demonstration Authorization Server'); console.log('====================================='); console.log('This standalone server demonstrates OAuth 2.0'); console.log('authorization separate from the MCP resource server'); console.log(''); console.log('This is for demonstration purposes only.'); console.log('In production, you would use a real OAuth provider'); console.log('like Auth0, Okta, Google, GitHub, etc.'); console.log('====================================='); // CORS for Inspector and MCP server app.use(cors({ origin: true, credentials: true })); app.use(express.json()); app.use(logger.middleware()); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', mode: 'authorization-server', endpoints: { metadata: `${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`, authorize: `${AUTH_SERVER_URL}/authorize`, token: `${AUTH_SERVER_URL}/token`, register: `${AUTH_SERVER_URL}/register`, introspect: `${AUTH_SERVER_URL}/introspect` } }); }); // Create auth provider instance for reuse const authProvider = new EverythingAuthProvider(); // OAuth endpoints via SDK's mcpAuthRouter app.use(mcpAuthRouter({ provider: authProvider, issuerUrl: new URL(AUTH_SERVER_URL), tokenOptions: { rateLimit: { windowMs: 5000, limit: 100 } }, clientRegistrationOptions: { rateLimit: { windowMs: 60000, limit: 10 } } })); // Rate limiting for custom endpoints const introspectRateLimit = rateLimit({ windowMs: 5 * 60 * 1000, // 5 minutes limit: 100, // 100 introspections per 5 minutes message: { error: 'too_many_requests', error_description: 'Token introspection rate limit exceeded' } }); const fakeAuthRateLimit = rateLimit({ windowMs: 60 * 1000, // 1 minute limit: 20, // 20 auth attempts per minute message: { error: 'too_many_requests', error_description: 'Authentication rate limit exceeded' } }); const staticFileRateLimit = rateLimit({ windowMs: 10 * 60 * 1000, // 10 minutes limit: 25, // 25 requests per 10 minutes for static files message: { error: 'too_many_requests', error_description: 'Static file rate limit exceeded' } }); // Token introspection endpoint (RFC 7662) app.post('/introspect', introspectRateLimit, express.urlencoded({ extended: false }), async (req, res) => { try { const { token } = req.body; if (!token) { return res.status(400).json({ error: 'invalid_request', error_description: 'Missing token parameter' }); } // Verify the token using the auth provider const authInfo = await authProvider.verifyAccessToken(token); // Return RFC 7662 compliant response res.json({ active: true, client_id: authInfo.clientId, scope: authInfo.scopes.join(' '), exp: authInfo.expiresAt, sub: authInfo.extra?.userId || 'unknown', userId: authInfo.extra?.userId, // Custom field for our implementation username: authInfo.extra?.username, iss: AUTH_SERVER_URL, aud: BASE_URI, // The resource server URL this token is intended for token_type: 'Bearer' }); } catch (error) { logger.debug('Token introspection failed', { error: (error as Error).message }); // Return inactive token response (don't leak error details) res.json({ active: false }); } }); // Fake upstream auth endpoints (for user authentication simulation) app.get('/fakeupstreamauth/authorize', fakeAuthRateLimit, cors(), handleFakeAuthorize); app.get('/fakeupstreamauth/callback', fakeAuthRateLimit, cors(), handleFakeAuthorizeRedirect); // Static assets (for auth page styling) import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); app.get('/mcp-logo.png', staticFileRateLimit, (req, res) => { // Serve from the main server's static directory const logoPath = path.join(__dirname, '../src/static/mcp.png'); res.sendFile(logoPath); }); // Connect to Redis (shared with MCP server in dev) try { await redisClient.connect(); logger.info('Connected to Redis', { url: redisClient.options?.url }); } catch (error) { logger.error('Could not connect to Redis', error as Error); process.exit(1); } app.listen(AUTH_SERVER_PORT, () => { logger.info('Authorization server started', { port: AUTH_SERVER_PORT, url: AUTH_SERVER_URL, endpoints: { metadata: `${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`, authorize: `${AUTH_SERVER_URL}/authorize`, token: `${AUTH_SERVER_URL}/token`, register: `${AUTH_SERVER_URL}/register`, introspect: `${AUTH_SERVER_URL}/introspect` } }); console.log(''); console.log('🚀 Auth server ready! Test with:'); console.log(` curl ${AUTH_SERVER_URL}/health`); console.log(` curl ${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`); console.log(''); console.log('💡 To test separate mode:'); console.log(' 1. Keep this server running'); console.log(' 2. In another terminal: AUTH_MODE=separate npm run dev'); console.log(' 3. Connect Inspector to http://localhost:3232/mcp'); });