UNPKG

mcp-cve-intelligence-server-lite-test

Version:

Lite Model Context Protocol server for comprehensive CVE intelligence gathering with multi-source exploit discovery, designed for security professionals and cybersecurity researchers - Alpha Release

392 lines 14.9 kB
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { randomUUID } from 'crypto'; import { createContextLogger } from '../utils/logger.js'; import express from 'express'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js'; import { createRequestLoggingMiddleware, requestLoggingPresets, } from '../middleware/request-logging-middleware.js'; import { InputSanitizationMiddleware } from '../middleware/input-sanitization-middleware.js'; import { secureErrorHandler, ErrorType } from '../utils/secure-error-handler.js'; const logger = createContextLogger('HttpTransportServer'); export class HttpTransportServer { config; expressApp; httpServer; activeTransports = new Map(); mcpServerFactory; healthService; constructor(config, mcpServerFactory, healthService) { this.config = config; this.mcpServerFactory = mcpServerFactory; this.healthService = healthService; this.expressApp = express(); this.setupMiddleware(); this.setupRoutes(); } /** * Set up Express middleware */ setupMiddleware() { // Input sanitization middleware - must be first const sanitizationMiddleware = new InputSanitizationMiddleware({ enableRateLimiting: true, }); this.expressApp.use(sanitizationMiddleware.middleware); this.expressApp.use(express.json()); // Observability middleware - should be early in the chain this.setupObservabilityMiddleware(); // Request tracking middleware if (this.healthService) { this.expressApp.use((req, res, next) => { // Only track MCP endpoint requests, not health checks if (req.path === '/mcp') { this.healthService?.incrementRequestCount(); } next(); }); } // Add request logging middleware if enabled if (this.config.requestLogging?.enabled) { this.setupRequestLogging(); } // Apply CORS if enabled if (this.config.enableCors) { this.setupCors(); } } /** * Set up request logging middleware */ setupRequestLogging() { if (!this.config.requestLogging) { return; } logger.info('Enabling request logging middleware', this.config.requestLogging); let middlewareFactory; switch (this.config.requestLogging.level) { case 'verbose': middlewareFactory = requestLoggingPresets.verbose; break; case 'debug': middlewareFactory = requestLoggingPresets.development; break; case 'info': default: middlewareFactory = requestLoggingPresets.production; break; } let requestLoggingMiddleware = middlewareFactory(); // Override preset with custom options if provided if (this.config.requestLogging.includeBody !== undefined || this.config.requestLogging.includeHeaders !== undefined) { const customOptions = { ...(this.config.requestLogging.includeBody !== undefined && { logBody: this.config.requestLogging.includeBody, }), ...(this.config.requestLogging.includeHeaders !== undefined && { logHeaders: this.config.requestLogging.includeHeaders, }), }; requestLoggingMiddleware = createRequestLoggingMiddleware(customOptions); } this.expressApp.use(requestLoggingMiddleware.middleware()); } /** * Set up CORS middleware */ setupCors() { this.expressApp.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id'); res.header('Access-Control-Expose-Headers', 'mcp-session-id'); if (req.method === 'OPTIONS') { res.sendStatus(200); return; } next(); }); } /** * Set up Express routes */ setupRoutes() { // Basic health check endpoint this.expressApp.get('/health', async (req, res) => { const requestId = randomUUID(); try { if (this.healthService) { this.healthService.incrementRequestCount(); const health = await this.healthService.getHealth(); res.status(health.status === 'healthy' ? 200 : 503).json(health); } else { // Fallback to basic health check if no health service res.json({ status: 'ok', timestamp: new Date().toISOString(), transport: 'http', protocol: 'MCP Streamable HTTP (2025-03-26)', }); } } catch (error) { const originalError = error instanceof Error ? error : new Error(String(error)); const errorDetails = secureErrorHandler.createSafeError({ type: ErrorType.INTERNAL, originalError, statusCode: 503, userMessage: 'Health check service is temporarily unavailable', }, requestId); res.status(503).json({ status: 'unhealthy', timestamp: new Date().toISOString(), error: errorDetails.message, }); } }); // Main MCP endpoint this.expressApp.all('/mcp', this.handleMcpRequest.bind(this)); } /** * Handle MCP requests (GET, POST, DELETE) */ async handleMcpRequest(req, res) { logger.info(`Received ${req.method} request to /mcp`, { body: req.body, url: req.url, }); try { const sessionId = req.headers['mcp-session-id']; let transport; logger.info('Processing MCP request', { sessionId, method: req.method, bodyMethod: req.body?.method, }); if (sessionId && this.activeTransports.has(sessionId)) { // Reuse existing transport const existingTransport = this.getExistingTransport(sessionId, res); if (!existingTransport) { return; } // Response already sent transport = existingTransport; } else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) { // Create new transport for initialization transport = await this.createNewTransport(); } else { // Invalid request this.sendBadRequestResponse(res); return; } // Handle the request with the transport logger.debug('Calling transport.handleRequest'); await transport.handleRequest(req, res, req.body); logger.debug('transport.handleRequest completed'); } catch (error) { this.handleRequestError(error, res); } } /** * Get existing transport or send error response */ getExistingTransport(sessionId, res) { const existingTransport = this.activeTransports.get(sessionId); if (existingTransport instanceof StreamableHTTPServerTransport) { return existingTransport; } else { // Transport exists but is not a StreamableHTTPServerTransport res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: Session exists but uses a different transport protocol', }, id: null, }); return null; } } /** * Create new transport for initialization */ async createNewTransport() { logger.info('Creating new StreamableHTTP transport for initialization'); const eventStore = this.config.sessionManagement ? new InMemoryEventStore() : undefined; const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore, // Enable resumability if session management is enabled onsessioninitialized: (sessionId) => { // Store the transport by session ID when session is initialized logger.info('StreamableHTTP session initialized', { sessionId }); this.activeTransports.set(sessionId, transport); }, }); // Set up onclose handler to clean up transport when closed transport.onclose = () => { const sid = transport.sessionId; if (sid && this.activeTransports.has(sid)) { logger.info(`Transport closed for session ${sid}`); this.activeTransports.delete(sid); } }; // Connect the transport to a new MCP server instance const server = this.mcpServerFactory(); logger.info('Connecting transport to MCP server'); await server.connect(transport); return transport; } /** * Send bad request response */ sendBadRequestResponse(res) { const requestId = randomUUID(); const errorDetails = secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('No valid session ID provided or not an initialization request'), statusCode: 400, }, requestId); res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: errorDetails.message, }, id: null, }); } /** * Handle request errors */ handleRequestError(error, res) { const requestId = randomUUID(); const originalError = error instanceof Error ? error : new Error(String(error)); const errorDetails = secureErrorHandler.createSafeError({ type: ErrorType.INTERNAL, originalError, statusCode: 500, sensitive: true, }, requestId); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: errorDetails.message, }, id: null, }); } } /** * Set up observability middleware (lite mode - no metrics/telemetry) */ setupObservabilityMiddleware() { // Lite mode - minimal middleware without observability this.expressApp.use((req, res, next) => { // Basic request processing without metrics collection next(); }); } /** * Start the HTTP server */ async start() { return new Promise((resolve, reject) => { try { const server = this.expressApp.listen(this.config.port, this.config.host, () => { logger.info('HTTP server started', { port: this.config.port, host: this.config.host, url: `http://${this.config.host}:${this.config.port}/mcp`, sessionManagement: this.config.sessionManagement, resumability: this.config.sessionManagement, }); resolve(); }); server.on('error', reject); this.httpServer = server; } catch (error) { reject(error); } }); } /** * Stop the HTTP server */ async stop() { // First, close all active transports const activeSessions = Array.from(this.activeTransports.keys()); if (activeSessions.length > 0) { logger.info(`Closing ${activeSessions.length} active transport sessions`); // Close all transports (this will trigger their onclose handlers) for (const [sessionId, transport] of this.activeTransports) { try { transport.close(); logger.debug(`Closed transport for session ${sessionId}`); } catch (error) { logger.warn(`Error closing transport for session ${sessionId}`, { error: error instanceof Error ? error.message : String(error), }); } } // Clear the map (onclose handlers should have done this, but ensure it's clean) this.activeTransports.clear(); } // Then close the HTTP server if (this.httpServer) { return new Promise((resolve, reject) => { const shutdownTimeout = setTimeout(() => { reject(new Error('Server shutdown timeout after 30 seconds')); }, 30000); this.httpServer?.close((err) => { clearTimeout(shutdownTimeout); if (err) { reject(err); } else { logger.info('HTTP server stopped'); this.httpServer = undefined; resolve(); } }); }); } } /** * Get the Express app instance */ getExpressApp() { return this.expressApp; } /** * Get active transport sessions */ getActiveSessions() { return Array.from(this.activeTransports.keys()); } /** * Get session count */ getSessionCount() { return this.activeTransports.size; } /** * Get server status information */ getServerStatus() { return { running: !!this.httpServer, activeSessions: this.activeTransports.size, port: this.httpServer ? this.config.port : undefined, host: this.httpServer ? this.config.host : undefined, }; } } //# sourceMappingURL=http-transport-server.js.map