diff --git a/auth-server/src/index.ts b/auth-server/src/index.ts index 25a841e..79ff128 100644 --- a/auth-server/src/index.ts +++ b/auth-server/src/index.ts @@ -14,7 +14,7 @@ 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('authorization for 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'); diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index e035268..83cffe0 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -17,6 +17,7 @@ import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from "@mo import cors from "cors"; import rateLimit from "express-rate-limit"; import express from "express"; +import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { ExternalAuthVerifier } from "./auth/external-verifier.js"; @@ -141,72 +142,60 @@ const staticFileRateLimit = rateLimit({ message: { error: 'too_many_requests', error_description: 'Static file rate limit exceeded' } }); -// Separate mode: MCP server uses external auth server -logger.info('Starting MCP server in SEPARATE mode', { +// MCP server using external auth server +logger.info('Starting MCP server', { baseUri: BASE_URI, port: PORT, authServerUrl: AUTH_SERVER_URL }); -// Fetch metadata from external auth server with retry logic -let authMetadata; -const maxRetries = 5; -const retryDelay = 3000; // 3 seconds +// Auth server state - will be populated asynchronously +let authMetadata: any; +let authServerAvailable = false; -for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - logger.info(`Attempting to connect to auth server (attempt ${attempt}/${maxRetries})`, { - authServerUrl: AUTH_SERVER_URL +// OAuth metadata endpoint - responds based on current auth server status +app.get('/.well-known/oauth-authorization-server', (req, res) => { + if (authServerAvailable && authMetadata) { + // Return the auth server metadata + res.json(authMetadata); + } else { + // Auth server unavailable + res.status(503).json({ + error: 'service_unavailable', + error_description: 'Authentication server is currently unavailable. Please try again later.' }); - - const authMetadataResponse = await fetch(`${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`); - if (!authMetadataResponse.ok) { - throw new Error(`Failed to fetch auth server metadata: ${authMetadataResponse.status} ${authMetadataResponse.statusText}`); - } - authMetadata = await authMetadataResponse.json(); - logger.info('Successfully fetched auth server metadata', { - issuer: authMetadata.issuer, - authorizationEndpoint: authMetadata.authorization_endpoint, - tokenEndpoint: authMetadata.token_endpoint - }); - break; // Success, exit retry loop - - } catch (error) { - if (attempt < maxRetries) { - logger.info(`Failed to connect to auth server, retrying in ${retryDelay/1000} seconds...`, { - attempt, - maxRetries, - error: (error as Error).message - }); - await new Promise(resolve => setTimeout(resolve, retryDelay)); - } else { - logger.error('Failed to fetch auth server metadata after all retries', error as Error); - logger.error('Make sure the auth server is running at', undefined, { authServerUrl: AUTH_SERVER_URL }); - process.exit(1); - } } -} +}); -// BACKWARDS COMPATIBILITY: We serve OAuth metadata from the MCP server even in separate mode -// This is technically redundant since the auth server handles all OAuth operations, -// but some clients may expect to find .well-known/oauth-authorization-server on the -// resource server itself. The metadata points to the external auth server endpoints. -app.use(mcpAuthMetadataRouter({ - oauthMetadata: authMetadata, - resourceServerUrl: new URL(BASE_URI), - resourceName: "MCP Feature Reference Server" -})); +// Configure bearer auth middleware that checks auth availability dynamically +const bearerAuth: express.RequestHandler = (req, res, next) => { + if (!authServerAvailable) { + // Degraded mode: return 503 for protected endpoints + res.status(503).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Authentication service unavailable', + data: { + authServerUrl: AUTH_SERVER_URL, + hint: 'The authentication server is not responding. Please ensure it is running and try again.' + } + }, + id: null + }); + return; + } -// Configure bearer auth with external verifier -const externalVerifier = new ExternalAuthVerifier(AUTH_SERVER_URL); - -const bearerAuthOptions: BearerAuthMiddlewareOptions = { - verifier: externalVerifier, - resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(BASE_URI)), + // Auth is available, use the real bearer auth middleware + const externalVerifier = new ExternalAuthVerifier(AUTH_SERVER_URL); + const bearerAuthOptions: BearerAuthMiddlewareOptions = { + verifier: externalVerifier, + resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(BASE_URI)), + }; + const realBearerAuth = requireBearerAuth(bearerAuthOptions); + realBearerAuth(req, res, next); }; -const bearerAuth = requireBearerAuth(bearerAuthOptions); - // MCP routes (legacy SSE transport) app.get("/sse", cors(corsOptions), bearerAuth, sseHeaders, handleSSEConnection); app.post("/message", cors(corsOptions), bearerAuth, sensitiveDataHeaders, handleMessage); @@ -216,6 +205,19 @@ app.get("/mcp", cors(corsOptions), bearerAuth, handleStreamableHTTP); app.post("/mcp", cors(corsOptions), bearerAuth, handleStreamableHTTP); app.delete("/mcp", cors(corsOptions), bearerAuth, handleStreamableHTTP); +// Health check endpoint +app.get("/health", (req, res) => { + res.json({ + status: authServerAvailable ? 'healthy' : 'degraded', + services: { + mcp: 'operational', + auth: authServerAvailable ? 'operational' : 'unavailable', + redis: 'operational' // Will be checked if Redis connection fails + }, + authServerUrl: AUTH_SERVER_URL + }); +}); + // Static assets app.get("/mcp-logo.png", staticFileRateLimit, (req, res) => { const logoPath = path.join(__dirname, "static", "mcp.png"); @@ -231,7 +233,23 @@ app.get("/styles.css", staticFileRateLimit, (req, res) => { // Splash page app.get("/", staticFileRateLimit, (req, res) => { const splashPath = path.join(__dirname, "static", "index.html"); - res.sendFile(splashPath); + + if (!authServerAvailable) { + // Inject warning banner for degraded mode + let html = fs.readFileSync(splashPath, 'utf8'); + const warningBanner = ` +