import type { IncomingMessage, ServerResponse } from 'node:http'; import { createServer, type Server as HttpServer } from 'node:http'; import type { AddressInfo } from 'node:net'; import { createMcpHandler, type McpHttpHandler } from '@modelcontextprotocol/server'; import { toNodeHandler, type NodeMcpRequestHandler } from '@modelcontextprotocol/node'; import type { MCPServer } from './MCPServer.js'; import type { StreamableHttpConfig } from './TransportConfig.js'; interface RuntimeDependencies { createAppServer: () => Promise; createHttpServer?: (handler: (req: IncomingMessage, res: ServerResponse) => void) => HttpServer; } export class StreamableHttpRuntime { private readonly mcpHandler: McpHttpHandler; private readonly nodeHandler: NodeMcpRequestHandler; private httpServer?: HttpServer; private isStopping = false; private preloadedAppServer?: MCPServer; private runtimeLogger?: ReturnType; constructor( private readonly config: StreamableHttpConfig, private readonly dependencies: RuntimeDependencies, ) { this.mcpHandler = createMcpHandler( async () => { const appServer = await this.takeAppServer(); const sdkServer = appServer.getServer(); const previousOnClose = sdkServer.onclose; sdkServer.onclose = () => { previousOnClose?.(); void appServer.cleanupAfterTransportClose().catch((error) => this.reportError(error)); }; return sdkServer; }, { legacy: 'stateless', responseMode: this.config.jsonResponse ? 'json' : 'auto', onerror: (error) => this.reportError(error), }, ); this.nodeHandler = toNodeHandler(this.mcpHandler, { onerror: (error) => this.reportError(error), }); } async start(): Promise { if (this.httpServer) { throw new Error('StreamableHttpRuntime already started'); } this.preloadedAppServer = await this.dependencies.createAppServer(); this.runtimeLogger = this.preloadedAppServer.getLogger(); this.preloadedAppServer.logStartupBegin(); try { const createHttpServer = this.dependencies.createHttpServer ?? createServer; this.httpServer = createHttpServer((req, res) => { void this.handleNodeRequest(req, res); }); await new Promise((resolve, reject) => { const onError = (error: Error) => { this.httpServer?.off('listening', onListening); reject(error); }; const onListening = () => { this.httpServer?.off('error', onError); resolve(); }; this.httpServer?.once('error', onError); this.httpServer?.once('listening', onListening); this.httpServer?.listen(this.config.port, this.config.host); }); this.preloadedAppServer.logStartupSuccess(); } catch (error) { const preloadedAppServer = this.preloadedAppServer; this.preloadedAppServer = undefined; if (preloadedAppServer) { preloadedAppServer.cleanup(); await preloadedAppServer.stop().catch(() => undefined); } throw error; } } async stop(): Promise { if (this.isStopping) return; this.isStopping = true; await this.mcpHandler.close(); const preloadedAppServer = this.preloadedAppServer; this.preloadedAppServer = undefined; if (preloadedAppServer) { await preloadedAppServer.stop(); } if (this.httpServer) { await new Promise((resolve, reject) => { this.httpServer?.close((error) => { if (error) { reject(error); return; } resolve(); }); }); this.httpServer = undefined; } } getAddress(): { host: string; port: number; path: string } { const boundAddress = this.httpServer?.address(); const boundPort = boundAddress && typeof boundAddress !== 'string' ? (boundAddress as AddressInfo).port : this.config.port; return { host: this.config.host, port: boundPort, path: this.config.path, }; } logInfo(message: string): void { this.runtimeLogger?.info(message); } async handleNodeRequest(req: IncomingMessage, res: ServerResponse): Promise { try { if (!this.isPathMatch(req.url)) { this.writePlainResponse(res, 404, 'Not Found'); return; } const hostError = this.validateHostHeader(req.headers.host); if (hostError) { this.writePlainResponse(res, 403, hostError); return; } const originError = this.validateOriginHeader(req.headers.origin); if (originError) { this.writePlainResponse(res, 403, originError); return; } await this.nodeHandler(req, res); } catch (error) { this.reportError(error); if (!res.headersSent) { this.writeJsonRpcError( res, 500, -32603, error instanceof Error ? error.message : String(error), ); } else { res.end(); } } } private async takeAppServer(): Promise { const appServer = this.preloadedAppServer; if (appServer) { this.preloadedAppServer = undefined; return appServer; } return this.dependencies.createAppServer(); } private reportError(error: unknown): void { const message = error instanceof Error ? error.message : String(error); this.runtimeLogger?.error('Streamable HTTP request failed', { error: message }); } private isPathMatch(url: string | undefined): boolean { if (!url) return false; const host = this.config.host.includes(':') && !this.config.host.startsWith('[') ? `[${this.config.host}]` : this.config.host; const parsedUrl = new globalThis.URL(url, `http://${host}`); return parsedUrl.pathname === this.config.path; } private validateHostHeader(hostHeader: string | undefined): string | undefined { if (!this.config.allowedHosts || this.config.allowedHosts.length === 0) { return undefined; } if (!hostHeader) { return 'Forbidden: Missing Host header'; } const normalized = this.normalizeHostHeader(hostHeader); if (this.config.allowedHosts.includes(normalized)) { return undefined; } return `Forbidden: Host header "${normalized}" is not allowed`; } private validateOriginHeader(originHeader: string | undefined): string | undefined { if (!originHeader || !this.config.allowedOrigins || this.config.allowedOrigins.length === 0) { return undefined; } if (this.config.allowedOrigins.includes(originHeader)) { return undefined; } return `Forbidden: Origin "${originHeader}" is not allowed`; } private normalizeHostHeader(hostHeader: string): string { if (hostHeader.startsWith('[')) { const closingBracketIndex = hostHeader.indexOf(']'); if (closingBracketIndex >= 0) { return hostHeader.slice(0, closingBracketIndex + 1); } return hostHeader; } const colonIndex = hostHeader.indexOf(':'); return colonIndex >= 0 ? hostHeader.slice(0, colonIndex) : hostHeader; } private writePlainResponse(res: ServerResponse, statusCode: number, body: string): void { res.statusCode = statusCode; res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.end(body); } private writeJsonRpcError( res: ServerResponse, statusCode: number, code: number, message: string, ): void { res.statusCode = statusCode; res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.end( JSON.stringify({ jsonrpc: '2.0', error: { code, message }, id: null, }), ); } }