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
JavaScript
"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;