UNPKG

mcp-framework

Version:

Framework for building Model Context Protocol (MCP) servers in Typescript

219 lines (218 loc) 8.27 kB
import { randomUUID } from 'node:crypto'; import { createServer } from 'node:http'; import { AbstractTransport } from '../base.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { logger } from '../../core/Logger.js'; export class HttpStreamTransport extends AbstractTransport { type = 'http-stream'; _isRunning = false; _port; _server; _endpoint; _enableJsonResponse = false; _transports = {}; _serverConfig; _serverSetupCallback; constructor(config = {}) { super(); this._port = config.port || 8080; this._endpoint = config.endpoint || '/mcp'; this._enableJsonResponse = config.responseMode === 'batch'; logger.debug(`HttpStreamTransport configured with: ${JSON.stringify({ port: this._port, endpoint: this._endpoint, responseMode: config.responseMode, batchTimeout: config.batchTimeout, maxMessageSize: config.maxMessageSize, auth: config.auth ? true : false, cors: config.cors ? true : false, })}`); } setServerConfig(serverConfig, setupCallback) { this._serverConfig = serverConfig; this._serverSetupCallback = setupCallback; } async start() { if (this._isRunning) { throw new Error('HttpStreamTransport already started'); } return new Promise((resolve, reject) => { this._server = createServer(async (req, res) => { try { const url = new URL(req.url, `http://${req.headers.host}`); if (url.pathname === this._endpoint) { await this.handleMcpRequest(req, res); } else { res.writeHead(404).end('Not Found'); } } catch (error) { logger.error(`Error handling request: ${error}`); if (!res.headersSent) { res.writeHead(500).end('Internal Server Error'); } } }); this._server.on('error', (error) => { logger.error(`HTTP server error: ${error}`); this._onerror?.(error); if (!this._isRunning) { reject(error); } }); this._server.on('close', () => { logger.info('HTTP server closed'); this._isRunning = false; this._onclose?.(); }); this._server.listen(this._port, () => { logger.info(`HTTP server listening on port ${this._port}, endpoint ${this._endpoint}`); this._isRunning = true; resolve(); }); }); } async handleMcpRequest(req, res) { const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && this._transports[sessionId]) { transport = this._transports[sessionId]; logger.debug(`Reusing existing session: ${sessionId}`); } else if (!sessionId && req.method === 'POST') { const body = await this.readRequestBody(req); if (isInitializeRequest(body)) { logger.info('Creating new session for initialization request'); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId) => { logger.info(`Session initialized: ${sessionId}`); this._transports[sessionId] = transport; }, enableJsonResponse: this._enableJsonResponse, }); transport.onclose = () => { if (transport.sessionId) { logger.info(`Transport closed for session: ${transport.sessionId}`); delete this._transports[transport.sessionId]; } }; transport.onerror = (error) => { logger.error(`Transport error for session: ${error}`); if (transport.sessionId) { delete this._transports[transport.sessionId]; } }; if (this._serverSetupCallback && this._serverConfig) { const server = new McpServer(this._serverConfig); await this._serverSetupCallback(server); await server.connect(transport); } else { transport.onmessage = async (message) => { if (this._onmessage) { await this._onmessage(message); } }; } await transport.handleRequest(req, res, body); return; } else { this.sendError(res, 400, -32000, 'Bad Request: No valid session ID provided'); return; } } else if (!sessionId) { this.sendError(res, 400, -32000, 'Bad Request: No valid session ID provided'); return; } else { this.sendError(res, 404, -32001, 'Session not found'); return; } const body = await this.readRequestBody(req); await transport.handleRequest(req, res, body); } async readRequestBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => { try { const parsed = body ? JSON.parse(body) : null; resolve(parsed); } catch (error) { reject(error); } }); req.on('error', reject); }); } sendError(res, status, code, message) { if (res.headersSent) return; res.writeHead(status).end(JSON.stringify({ jsonrpc: '2.0', error: { code, message, }, id: null, })); } async send(message) { if (!this._isRunning) { logger.warn('Attempted to send message, but HTTP transport is not running'); return; } const activeSessions = Object.entries(this._transports); if (activeSessions.length === 0) { logger.warn('No active sessions to send message to'); return; } logger.debug(`Broadcasting message to ${activeSessions.length} sessions: ${JSON.stringify(message)}`); const failedSessions = []; for (const [sessionId, transport] of activeSessions) { try { await transport.send(message); } catch (error) { logger.error(`Error sending message to session ${sessionId}: ${error}`); failedSessions.push(sessionId); } } if (failedSessions.length > 0) { failedSessions.forEach((sessionId) => delete this._transports[sessionId]); logger.warn(`Failed to send message to ${failedSessions.length} sessions.`); } } async close() { if (!this._isRunning) { return; } for (const transport of Object.values(this._transports)) { try { await transport.close(); } catch (error) { logger.error(`Error closing transport: ${error}`); } } this._transports = {}; if (this._server) { this._server.close(); this._server = undefined; } this._isRunning = false; } isRunning() { return this._isRunning; } }