UNPKG

@rhofkens/mcp-quotes-server-claude-code

Version:

Model Context Protocol (MCP) server for managing and serving quotes

266 lines 11.3 kB
/** * HTTP Transport for MCP Server * * Implements the Streamable HTTP transport (2025-03-26 spec) * with a single POST endpoint for all MCP communication */ import { randomUUID } from 'crypto'; import express from 'express'; import { logger } from '../utils/logger.js'; /** * HTTP Transport implementation for MCP Server * Uses a single POST endpoint as per the Streamable HTTP transport spec */ export class HttpServerTransport { options; app; server; sessions = new Map(); currentResponse; isStarted = false; onclose; onerror; onmessage; constructor(options) { this.options = options; this.app = express(); this.setupMiddleware(); this.setupRoutes(); } setupMiddleware() { // Parse JSON bodies this.app.use(express.json({ limit: '10mb' })); // CORS headers for security this.app.use((req, res, next) => { const origin = req.headers.origin; // In production, validate origin more strictly if (origin) { res.header('Access-Control-Allow-Origin', origin); res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id'); res.header('Access-Control-Allow-Credentials', 'true'); } if (req.method === 'OPTIONS') { res.sendStatus(204); return; } next(); }); } setupRoutes() { // Main MCP endpoint - handles all protocol messages const mcpHandler = (req, res) => { try { const sessionId = req.headers['mcp-session-id']; const message = req.body; logger.debug('HTTP transport received message', { sessionId, method: 'method' in message ? message.method : undefined, id: 'id' in message ? message.id : undefined, headers: req.headers, }); // Handle initialization specially if ('method' in message && message.method === 'initialize') { const newSessionId = randomUUID(); // Create session this.sessions.set(newSessionId, { pendingResponses: new Map(), }); // Process the initialize message if (this.onmessage) { // For initialization, we'll handle the response directly this.currentResponse = res; const messageId = 'id' in message ? message.id : undefined; // Store response handler for this specific request const session = this.sessions.get(newSessionId); if (messageId !== undefined) { session.pendingResponses.set(messageId, (response) => { logger.debug('Sending initialize response', { sessionId: newSessionId, responseId: 'id' in response ? response.id : undefined, }); res.setHeader('Mcp-Session-Id', newSessionId); res.json(response); session.pendingResponses.delete(messageId); }); } // Send the message to the MCP server this.onmessage(message); return; } else { res.status(500).json({ jsonrpc: '2.0', id: 'id' in message ? message.id : undefined, error: { code: -32603, message: 'Server not initialized', }, }); return; } } else { // For all other requests, require session ID if (!sessionId || !this.sessions.has(sessionId)) { res.status(400).json({ jsonrpc: '2.0', id: 'id' in message ? message.id : null, error: { code: -32603, message: 'Missing or invalid Mcp-Session-Id header', }, }); return; } // Process the message const session = this.sessions.get(sessionId); if (this.onmessage) { // For request messages that expect a response if ('id' in message && message.id !== null) { const messageId = message.id; // Store response handler for this specific request session.pendingResponses.set(messageId, (response) => { logger.debug('Sending response', { sessionId, method: 'method' in message ? message.method : undefined, responseId: 'id' in response ? response.id : undefined, }); res.json(response); session.pendingResponses.delete(messageId); }); // Set current response for immediate handling this.currentResponse = res; // Send the message to the MCP server this.onmessage(message); return; } else { // For notification messages that don't expect a response this.onmessage(message); res.sendStatus(204); return; } } else { res.status(500).json({ jsonrpc: '2.0', id: 'id' in message ? message.id : null, error: { code: -32603, message: 'Server not initialized', }, }); return; } } } catch (error) { logger.error('HTTP transport error', error); res.status(500).json({ jsonrpc: '2.0', id: req.body && typeof req.body === 'object' && 'id' in req.body ? req.body.id : null, error: { code: -32603, message: `Internal error: ${error instanceof Error ? error.message : String(error)}`, }, }); return; } }; this.app.post(this.options.path, mcpHandler); // Health check endpoint this.app.get('/health', (_, res) => { res.json({ status: 'ok', transport: 'http', sessions: this.sessions.size, }); }); } start() { if (this.isStarted) { logger.warn('HTTP transport already started'); return Promise.resolve(); } // Log startup immediately logger.info('Starting HTTP transport...'); return new Promise((resolve, reject) => { try { this.server = this.app.listen(this.options.port, this.options.host, () => { if (this.isStarted) { // Prevent duplicate startup messages return; } this.isStarted = true; const url = `http://${this.options.host}:${this.options.port}${this.options.path}`; logger.info('HTTP transport started', { host: this.options.host, port: this.options.port, path: this.options.path, url, }); logger.info(`MCP HTTP Server listening at ${url}`); resolve(); }); this.server.on('error', (error) => { logger.error('HTTP server error', error); this.onerror?.(error); reject(error); }); } catch (error) { reject(error); } }); } close() { return new Promise((resolve) => { if (this.server) { this.server.close(() => { logger.info('HTTP transport closed'); this.sessions.clear(); this.isStarted = false; this.onclose?.(); resolve(); }); } else { resolve(); } }); } async send(message) { // This is called by the MCP server to send messages to clients logger.debug('HTTP transport send called', { method: 'method' in message ? message.method : undefined, id: 'id' in message ? message.id : undefined, }); // For responses (they have an id but no method) if ('id' in message && message.id !== null && !('method' in message)) { // Find the pending response handler across all sessions for (const [sessionId, session] of this.sessions) { const handler = session.pendingResponses.get(message.id); if (handler) { logger.debug('Found response handler for message', { id: message.id, sessionId }); handler(message); return; } } // If we have a current response object (for immediate responses) if (this.currentResponse && !this.currentResponse.headersSent) { logger.debug('Using current response object', { id: message.id }); this.currentResponse.json(message); delete this.currentResponse; return; } logger.warn('No handler found for response', { id: message.id }); } // For server-initiated messages (notifications) // These would need to be queued or sent via SSE if we supported it logger.debug('Server-initiated message (not supported in basic HTTP)', message); } } //# sourceMappingURL=http.js.map