UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

166 lines 6.83 kB
import express from 'express'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; export function createSSEApp(options, dependencies) { const { logger, serverFactory } = dependencies; const app = express(); // Store active SSE transports by session ID const sseTransports = new Map(); // CORS middleware app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, X-Session-ID'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); // SSE endpoint - for server-to-client messages app.get('/sse', async (req, res) => { try { // Create a new Debug MCP Server instance for this connection const debugMcpServer = serverFactory({ logLevel: options.logLevel, logFile: options.logFile, }); // Create SSE transport with the response object const transport = new SSEServerTransport('/sse', res); // Connect the server to the transport (this automatically calls start()) await debugMcpServer.server.connect(transport); // Store the transport and server by session ID const sessionId = transport.sessionId; const sessionData = { transport, server: debugMcpServer, isClosing: false }; sseTransports.set(sessionId, sessionData); logger.info(`SSE connection established: ${sessionId}`); // Keep the connection alive with periodic pings const pingInterval = setInterval(() => { if (!sseTransports.has(sessionId)) { clearInterval(pingInterval); return; } res.write(':ping\n\n'); }, 30000); // Every 30 seconds // Handle connection close with guard against infinite recursion const closeHandler = () => { const session = sseTransports.get(sessionId); if (!session || session.isClosing) { return; // Already closing or doesn't exist } session.isClosing = true; logger.info(`SSE connection closed: ${sessionId}`); // Clean up ping interval clearInterval(pingInterval); // Remove from map first to prevent any further operations sseTransports.delete(sessionId); // Stop the server asynchronously setImmediate(() => { debugMcpServer.stop().catch(error => { logger.error(`Error stopping server for session ${sessionId}:`, error); }); }); }; transport.onclose = closeHandler; // Also handle client disconnect req.on('close', closeHandler); req.on('end', closeHandler); // Handle errors transport.onerror = (error) => { logger.error(`SSE transport error for session ${sessionId}:`, error); }; } catch (error) { logger.error('Error establishing SSE connection:', error); if (!res.headersSent) { res.status(500).end(); } } }); // POST endpoint - for client-to-server messages app.post('/sse', async (req, res) => { try { // Extract session ID from query parameter (as per MCP SDK SSE protocol) const sessionId = req.query.sessionId; if (!sessionId || !sseTransports.has(sessionId)) { logger.warn(`Invalid session ID: ${sessionId}`, { headers: req.headers, query: req.query, hasSessionId: !!sessionId, knownSessions: Array.from(sseTransports.keys()) }); res.status(400).json({ jsonrpc: '2.0', error: { code: -32600, message: 'Invalid session ID' }, id: null }); return; } const { transport } = sseTransports.get(sessionId); // Handle the POST message through the transport await transport.handlePostMessage(req, res); } catch (error) { logger.error('Error handling SSE POST request', { error }); res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : 'Unknown error' }, id: null }); } }); // Add a simple health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', mode: 'sse', connections: sseTransports.size, sessions: Array.from(sseTransports.keys()) }); }); // Expose the transports map for graceful shutdown app.sseTransports = sseTransports; // eslint-disable-line @typescript-eslint/no-explicit-any return app; } export async function handleSSECommand(options, dependencies) { const { logger, exitProcess = process.exit } = dependencies; if (options.logLevel) { logger.level = options.logLevel; } const port = parseInt(options.port, 10); logger.info(`Starting Debug MCP Server in SSE mode on port ${port}`); try { const app = createSSEApp(options, dependencies); const server = app.listen(port, () => { logger.info(`Debug MCP Server (SSE) listening on port ${port}`); logger.info(`SSE endpoint available at http://localhost:${port}/sse`); }); // Handle graceful shutdown process.on('SIGINT', () => { logger.info('Shutting down SSE server...'); // Close all SSE connections const sseTransports = app.sseTransports; // eslint-disable-line @typescript-eslint/no-explicit-any if (sseTransports) { sseTransports.forEach(({ transport, server }) => { transport.close(); server.stop(); }); } server.close(() => { exitProcess(0); }); }); } catch (error) { logger.error('Failed to start server in SSE mode', { error }); exitProcess(1); } } //# sourceMappingURL=sse-command.js.map