Skip to main content
Glama
expressHttpManager.ts7.61 kB
import type { Server as HttpServer } from "node:http"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { RequestHandler } from "express"; import type { ILogger } from "../core/logger.js"; import type { Result } from "../core/result.js"; import { createErrorResult, createSuccessResult } from "../core/result.js"; import type { StreamableHttpTransportConfig } from "./config.js"; import { TransportRegistry } from "./transportRegistry.js"; /** * Express HTTP Manager * * Manages HTTP server lifecycle for Streamable HTTP transport. * Handles multiple concurrent sessions by creating per-session transport and server instances. * * Key Features: * - /mcp endpoint → routes to appropriate transport via TransportRegistry * - /health endpoint → reports active session count * - Per-session isolation → each client gets independent MCP protocol state * - Graceful shutdown → cleans up all active sessions * * Architecture: * ``` * Client 1 → POST /mcp → TransportRegistry.getOrCreate() → Transport 1 + Server 1 * Client 2 → POST /mcp → TransportRegistry.getOrCreate() → Transport 2 + Server 2 * ``` * * @example * ```typescript * const manager = new ExpressHttpManager( * config, * () => TouchDesignerServer.create(), // Server factory * sessionManager, * logger * ); * * // Start server * const result = await manager.start(); * * // Graceful shutdown * await manager.stop(); * ``` */ export class ExpressHttpManager { private readonly config: StreamableHttpTransportConfig; private readonly serverFactory: () => McpServer; private readonly logger: ILogger; private readonly registry: TransportRegistry; private server: HttpServer | null = null; /** * Create ExpressHttpManager with server factory * * @param config - Streamable HTTP transport configuration * @param serverFactory - Factory function to create new Server instances per session * @param sessionManager - Session manager for TTL tracking (optional) * @param logger - Logger instance */ constructor( config: StreamableHttpTransportConfig, serverFactory: () => McpServer, sessionManager: import("./sessionManager.js").ISessionManager | null, logger: ILogger, ) { this.config = config; this.serverFactory = serverFactory; this.logger = logger; this.registry = new TransportRegistry(config, sessionManager, logger); } /** * Start HTTP server with Express app from SDK * * @returns Result indicating success or failure */ async start(): Promise<Result<void, Error>> { try { if (this.server?.listening) { return createErrorResult( new Error("Express HTTP server is already running"), ); } // Create Express app using SDK's factory // This automatically includes DNS rebinding protection for localhost const app = createMcpExpressApp({ host: this.config.host, }); // MCP endpoint handler - routes to appropriate transport via registry const handleMcpRequest: RequestHandler = async (req, res) => { try { // Extract session ID from header const sessionId = req.headers["mcp-session-id"] as string | undefined; // Get or create transport for this session const transport = await this.registry.getOrCreate( sessionId, req.body, this.serverFactory, ); if (!transport) { // Invalid session (session ID provided but not found, or non-initialize without session) res.status(400).json({ error: { code: -32000, message: "Invalid session", }, id: null, jsonrpc: "2.0", }); return; } // Delegate request to transport await transport.handleRequest(req, res, req.body); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.sendLog({ data: `Error handling MCP request: ${errorMessage}`, level: "error", logger: "ExpressHttpManager", }); if (!res.headersSent) { res.status(500).json({ error: "Internal server error", }); } } }; // Configure /mcp endpoints // POST: JSON-RPC requests (initialize, tool calls, etc.) // GET: SSE streaming for notifications // DELETE: Session termination app.post(this.config.endpoint, handleMcpRequest); app.get(this.config.endpoint, handleMcpRequest); app.delete(this.config.endpoint, handleMcpRequest); // Configure /health endpoint app.get("/health", (_req, res) => { const sessionCount = this.registry.getCount(); res.json({ sessions: sessionCount, status: "ok", timestamp: new Date().toISOString(), }); }); // Start HTTP server await new Promise<void>((resolve, reject) => { try { this.server = app.listen(this.config.port, this.config.host, () => { this.logger.sendLog({ data: `Express HTTP server listening on ${this.config.host}:${this.config.port}`, level: "info", logger: "ExpressHttpManager", }); this.logger.sendLog({ data: `MCP endpoint: ${this.config.endpoint}`, level: "info", logger: "ExpressHttpManager", }); this.logger.sendLog({ data: "Health check: GET /health", level: "info", logger: "ExpressHttpManager", }); resolve(); }); this.server.on("error", (error) => { reject(error); }); } catch (error) { reject(error); } }); return createSuccessResult(undefined); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); return createErrorResult( new Error(`Failed to start Express HTTP server: ${err.message}`), ); } } /** * Graceful shutdown * * Stops HTTP server and cleans up all active sessions. * * @returns Result indicating success or failure */ async stop(): Promise<Result<void, Error>> { try { if (!this.server) { return createSuccessResult(undefined); } this.logger.sendLog({ data: "Stopping Express HTTP server...", level: "info", logger: "ExpressHttpManager", }); // Cleanup all active sessions first const cleanupResult = await this.registry.cleanup(); if (!cleanupResult.success) { this.logger.sendLog({ data: `Warning: Session cleanup failed: ${cleanupResult.error.message}`, level: "warning", logger: "ExpressHttpManager", }); } // Close HTTP server await new Promise<void>((resolve, reject) => { this.server?.close((error) => { if (error) { reject(error); } else { resolve(); } }); }); this.logger.sendLog({ data: "Express HTTP server stopped", level: "info", logger: "ExpressHttpManager", }); this.server = null; return createSuccessResult(undefined); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); return createErrorResult( new Error(`Failed to stop Express HTTP server: ${err.message}`), ); } } /** * Check if server is running * * @returns True if server is running */ isRunning(): boolean { return this.server?.listening ?? false; } /** * Get active session count * * @returns Number of active sessions */ getActiveSessionCount(): number { return this.registry.getCount(); } /** * Get all active session IDs * * @returns Array of session IDs */ getActiveSessionIds(): string[] { return this.registry.getSessionIds(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8beeeaaat/touchdesigner-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server