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