UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

410 lines (409 loc) 15.4 kB
"use strict"; /** * @fileoverview SSE MCP Server with Authentication Integration * @version 1.0.0 * @since 2025-08-04 * @description SSE server that integrates with our auth-enabled MCP handler * * This creates a complete SSE-based MCP server with: * 1. JWT authentication support * 2. Real-time bi-directional communication * 3. Permission-based tool access * 4. Session management and audit logging * 5. Integration with existing transport factory */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SSEMCPServer = void 0; const http_1 = require("http"); const url_1 = require("url"); const auth_config_1 = require("../../config/auth-config"); const mcp_handler_1 = require("../../mcp/handlers/mcp-handler"); const metrics_tracker_1 = require("../../mcp/handlers/utils/metrics-tracker"); const audit_logger_1 = require("../../mcp/security/audit-logger"); const JWTAuthService_1 = require("../auth/JWTAuthService"); /** * SSE MCP Server with Authentication */ class SSEMCPServer { constructor(config = {}) { this.connections = new Map(); this.authService = null; this.heartbeatTimer = null; this.config = { port: config.port || 3001, host: config.host || 'localhost', enableAuthentication: config.enableAuthentication || false, corsOrigins: config.corsOrigins || ['http://localhost:3000', 'http://127.0.0.1:3000'], connectionTimeout: config.connectionTimeout || 300000, // 5 minutes maxConnections: config.maxConnections || 100, heartbeatInterval: config.heartbeatInterval || 30000, // 30 seconds }; // Initialize components this.auditLogger = new audit_logger_1.AuditLogger(); this.metricsTracker = new metrics_tracker_1.MetricsTracker(); // Initialize auth service if enabled if (this.config.enableAuthentication) { this.authService = new JWTAuthService_1.JWTAuthService((0, auth_config_1.getSSEAuthConfig)()); // Log auth configuration for debugging console.log('🔐 SSE Server using centralized auth config'); (0, auth_config_1.logAuthConfig)(); } // Create auth-enabled MCP handler this.mcpHandler = new mcp_handler_1.ModularMCPHandler({ enableAuthentication: this.config.enableAuthentication, authService: this.authService, auditLogger: this.auditLogger, metricsTracker: this.metricsTracker, }); // Create HTTP server this.server = (0, http_1.createServer)((req, res) => { this.handleHttpRequest(req, res); }); console.log(`🚀 SSE MCP Server initialized:`); console.log(` • Port: ${this.config.port}`); console.log(` • Authentication: ${this.config.enableAuthentication ? '✅ Enabled' : '❌ Disabled'}`); console.log(` • Max Connections: ${this.config.maxConnections}`); console.log(` • CORS Origins: ${this.config.corsOrigins.join(', ')}`); } /** * Start the SSE server */ async start() { return new Promise((resolve, reject) => { this.server.listen(this.config.port, this.config.host, () => { console.log(`🎉 SSE MCP Server started on http://${this.config.host}:${this.config.port}`); console.log(` • SSE Endpoint: /sse`); console.log(` • Health Check: /health`); if (this.config.enableAuthentication) { console.log(` • Auth Required: Yes (JWT Bearer tokens)`); } // Start heartbeat this.startHeartbeat(); resolve(); }); this.server.on('error', error => { console.error('❌ SSE Server error:', error); reject(error); }); }); } /** * Stop the SSE server */ async stop() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } // Close all connections for (const connection of this.connections.values()) { connection.response.end(); } this.connections.clear(); return new Promise(resolve => { this.server.close(() => { console.log('🛑 SSE MCP Server stopped'); resolve(); }); }); } /** * Check if the SSE server is currently running */ isRunning() { return this.server.listening; } /** * Get the current number of active connections */ getConnectionCount() { return this.connections.size; } /** * Get server configuration */ getConfig() { return { ...this.config }; } /** * Handle HTTP requests */ async handleHttpRequest(req, res) { const url = new url_1.URL(req.url || '/', `http://${req.headers.host}`); // Set CORS headers this.setCORSHeaders(res); // Handle preflight requests if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } switch (url.pathname) { case '/sse': await this.handleSSEConnection(req, res); break; case '/health': this.handleHealthCheck(req, res); break; case '/mcp': await this.handleMCPRequest(req, res); break; default: res.writeHead(404); res.end('Not Found'); } } /** * Handle SSE connection establishment */ async handleSSEConnection(req, res) { var _a, _b, _c; // Check connection limits if (this.connections.size >= this.config.maxConnections) { res.writeHead(503, { 'Content-Type': 'text/plain' }); res.end('Server at capacity'); return; } // Extract auth token from query params or headers const url = new url_1.URL(req.url || '/', `http://${req.headers.host}`); const tokenFromQuery = url.searchParams.get('token'); const tokenFromHeader = (_a = req.headers.authorization) === null || _a === void 0 ? void 0 : _a.replace('Bearer ', ''); const authToken = tokenFromQuery || tokenFromHeader; let connection = { id: this.generateConnectionId(), response: res, request: req, isAlive: true, lastActivity: Date.now(), isAuthenticated: false, permissions: [], authToken, }; // Authenticate if required if (this.config.enableAuthentication && this.authService) { if (!authToken) { res.writeHead(401, { 'Content-Type': 'text/plain' }); res.end('Authentication required'); return; } try { const authResult = await this.authService.validateToken(authToken); if (authResult.isAuthenticated && authResult.user) { connection.isAuthenticated = true; connection.user = authResult.user; connection.permissions = authResult.permissions || []; connection.sessionId = authResult.sessionId; console.log(`🔐 SSE connection authenticated: ${authResult.user.username}`); } else { res.writeHead(401, { 'Content-Type': 'text/plain' }); res.end('Invalid token'); return; } } catch (error) { console.error('❌ SSE authentication failed:', error); res.writeHead(401, { 'Content-Type': 'text/plain' }); res.end('Authentication failed'); return; } } else { // No auth required connection.isAuthenticated = true; connection.permissions = ['*']; // Full access when auth disabled } // Set SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'Access-Control-Allow-Credentials': 'true', }); // Store connection this.connections.set(connection.id, connection); // Send welcome message this.sendSSEMessage(connection, { type: 'connected', data: { connectionId: connection.id, authenticated: connection.isAuthenticated, user: ((_b = connection.user) === null || _b === void 0 ? void 0 : _b.username) || 'anonymous', permissions: connection.permissions, serverInfo: { name: 'SSE MCP Server', version: '1.0.0', capabilities: ['streaming', 'realtime', 'mcp-protocol'], }, }, }); // Handle connection close res.on('close', () => { console.log(`📤 SSE connection closed: ${connection.id}`); this.connections.delete(connection.id); }); req.on('close', () => { console.log(`📤 SSE request closed: ${connection.id}`); this.connections.delete(connection.id); }); console.log(`📥 SSE connection established: ${connection.id} (${((_c = connection.user) === null || _c === void 0 ? void 0 : _c.username) || 'anonymous'})`); } /** * Handle MCP requests via HTTP POST */ async handleMCPRequest(req, res) { var _a; if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return; } try { // Parse request body const body = await this.parseRequestBody(req); const mcpRequest = JSON.parse(body); // Extract auth context const authToken = (_a = req.headers.authorization) === null || _a === void 0 ? void 0 : _a.replace('Bearer ', ''); const context = authToken ? { authToken } : undefined; // Process MCP request const mcpResponse = await this.mcpHandler.handleRequest(mcpRequest, context); // Send response res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(mcpResponse)); // Broadcast to SSE connections if relevant this.broadcastToSSEConnections({ type: 'mcp-request', data: { request: mcpRequest.method, success: !!mcpResponse.result, timestamp: new Date().toISOString(), }, }); } catch (error) { console.error('❌ MCP request error:', error); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error', }, })); } } /** * Handle health check requests */ handleHealthCheck(req, res) { const health = { status: 'healthy', timestamp: new Date().toISOString(), connections: this.connections.size, maxConnections: this.config.maxConnections, authentication: this.config.enableAuthentication ? 'enabled' : 'disabled', uptime: process.uptime(), }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(health)); } /** * Send SSE message to specific connection */ sendSSEMessage(connection, message) { if (!connection.isAlive) return; try { const data = `data: ${JSON.stringify(message)}\n\n`; connection.response.write(data); connection.lastActivity = Date.now(); } catch (error) { console.error(`❌ Failed to send SSE message to ${connection.id}:`, error); connection.isAlive = false; this.connections.delete(connection.id); } } /** * Broadcast message to all authenticated SSE connections */ broadcastToSSEConnections(message) { for (const connection of this.connections.values()) { if (connection.isAuthenticated && connection.isAlive) { this.sendSSEMessage(connection, message); } } } /** * Start heartbeat to keep connections alive */ startHeartbeat() { this.heartbeatTimer = setInterval(() => { const now = Date.now(); for (const connection of this.connections.values()) { // Check for dead connections if (now - connection.lastActivity > this.config.connectionTimeout) { console.log(`💔 Connection timeout: ${connection.id}`); connection.response.end(); this.connections.delete(connection.id); continue; } // Send heartbeat this.sendSSEMessage(connection, { type: 'heartbeat', data: { timestamp: new Date().toISOString() }, }); } }, this.config.heartbeatInterval); } /** * Set CORS headers */ setCORSHeaders(res) { // For development, allow all origins. In production, use specific origins const origin = process.env.NODE_ENV === 'production' ? this.config.corsOrigins[0] || 'http://localhost:3000' : '*'; res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Cache-Control'); res.setHeader('Access-Control-Allow-Credentials', 'true'); } /** * Parse request body */ parseRequestBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { resolve(body); }); req.on('error', reject); }); } /** * Generate unique connection ID */ generateConnectionId() { return `sse_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Get server statistics */ getStats() { return { connections: this.connections.size, maxConnections: this.config.maxConnections, authenticatedConnections: Array.from(this.connections.values()).filter(c => c.isAuthenticated) .length, uptime: process.uptime(), metricsSnapshot: this.metricsTracker.getMetrics(), }; } } exports.SSEMCPServer = SSEMCPServer; exports.default = SSEMCPServer;