UNPKG

@bloopai/debugger-mcp

Version:

An MCP server that provides interactive debugging capabilities to AI coding agents

679 lines (678 loc) 33.1 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InteractiveDebuggerMcpServer = void 0; const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js"); const http = __importStar(require("http")); const path = __importStar(require("path")); const crypto_1 = require("crypto"); const yargs_1 = __importDefault(require("yargs/yargs")); const helpers_1 = require("yargs/helpers"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const types_1 = require("./types"); const ergonomic_debugger_client_1 = require("@bloopai/ergonomic-debugger-client"); const tools_1 = require("./tools"); const errorUtils_1 = require("./errorUtils"); const schemas_1 = require("./schemas"); // Constants for HTTP and JSON-RPC const MCP_HTTP_ENDPOINT = '/mcp'; const HTTP_STATUS_NOT_FOUND = 404; const HTTP_STATUS_BAD_REQUEST = 400; const HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE = 415; const JSONRPC_ERROR_METHOD_NOT_FOUND = -32601; const JSONRPC_ERROR_PARSE_ERROR = -32700; const JSONRPC_ERROR_INVALID_REQUEST = -32600; const toolRegistry = new Map(); function registerTool(name, description, schema, handler) { toolRegistry.set(name, { description, schema, handler: handler, }); } /** * AI Debugger MCP Server * * Provides debugging capabilities to AI agents through the Model Context Protocol (MCP) */ class InteractiveDebuggerMcpServer { server; serverInfo; edcSessionManager; logger; activeSessions = new Map(); asyncEventQueue = []; MAX_ASYNC_EVENT_QUEUE_SIZE = 100; constructor() { this.serverInfo = { name: 'interactive-debugger', version: '0.1.0' }; this.server = new index_js_1.Server(this.serverInfo, { capabilities: { resources: {}, tools: {} }, }); this.logger = this._createLogger(); this.edcSessionManager = new ergonomic_debugger_client_1.SessionManager(this.logger, {}); this._loadAdapterConfigurations(); this._initializeServerComponents(); this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', this.handleShutdown.bind(this)); } _createLogger() { const createChildLogger = (parentContext = {}) => { return { trace: (...args) => console.error('[TRACE]', JSON.stringify(parentContext), ...args), debug: (...args) => console.error('[DEBUG]', JSON.stringify(parentContext), ...args), info: (...args) => console.error('[INFO]', JSON.stringify(parentContext), ...args), warn: (...args) => console.warn('[WARN]', JSON.stringify(parentContext), ...args), error: (...args) => console.error('[ERROR]', JSON.stringify(parentContext), ...args), child: (options) => { const newContext = { ...parentContext, ...options }; return createChildLogger(newContext); // Recursively create a new logger with the new context }, }; }; return createChildLogger(); } _loadAdapterConfigurations() { const adapterConfigLoader = new ergonomic_debugger_client_1.AdapterConfigLoader(this.logger); const mergedAdapterConfigs = new Map(); // Load default configurations const defaultConfigsPath = path.join(__dirname, 'config', 'defaultAdapterConfigs.json'); try { this.logger.info(`Loading default adapter configurations from: ${defaultConfigsPath}`); const defaultConfigs = adapterConfigLoader.loadFromFile(defaultConfigsPath); defaultConfigs.forEach((config) => { mergedAdapterConfigs.set(config.type, config); }); this.logger.info(`Loaded ${defaultConfigs.length} default adapter configurations.`); } catch (error) { this.logger.warn(`Could not load default adapter configurations from ${defaultConfigsPath}. Error: ${error instanceof Error ? error.message : String(error)}`); } // Process CLI arguments and merge user configurations this._processCliArgumentsAndMergeConfigs(adapterConfigLoader, mergedAdapterConfigs); // Register final configurations if (mergedAdapterConfigs.size > 0) { this.logger.info(`Registering ${mergedAdapterConfigs.size} final adapter configurations with SessionManager.`); mergedAdapterConfigs.forEach((config) => { this.edcSessionManager.registerAdapterConfiguration(config); }); } else { this.logger.warn('No adapter configurations were loaded or defined. Debugging functionality might be limited.'); } } _processCliArgumentsAndMergeConfigs(adapterConfigLoader, mergedAdapterConfigs) { const cliArgs = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) .option('adapter-config', { alias: 'ac', type: 'string', description: 'Path to a custom adapter configurations JSON file (overrides defaults)', }) .help(false) .version(false) .parseSync(); if (cliArgs.adapterConfig) { const userConfigPath = cliArgs.adapterConfig; try { this.logger.info(`Loading user-defined adapter configurations from: ${userConfigPath}`); const userConfigs = adapterConfigLoader.loadFromFile(userConfigPath); userConfigs.forEach((config) => { this.logger.info(`User config for '${config.type}' will override default if present.`); mergedAdapterConfigs.set(config.type, config); }); this.logger.info(`Loaded and merged ${userConfigs.length} user-defined adapter configurations.`); } catch (error) { this.logger.error(`Failed to load user-defined adapter configurations from ${userConfigPath}. Error: ${error instanceof Error ? error.message : String(error)}. Proceeding with defaults (if any).`); } } else { this.logger.info('No custom adapter configuration file provided via CLI. Using defaults (if loaded).'); } } _initializeServerComponents() { this.initializeTools(); this.setupToolHandlers(); } async handleShutdown() { console.error('Shutting down AI Debugger MCP Server (SIGINT)...'); await this.edcSessionManager.disposeAllSessions(); this.activeSessions.clear(); if (this.server) { await this.server.close(); } console.error('AI Debugger MCP Server shutdown complete.'); process.exit(0); } getServerInstance() { return this.server; } getSessionManager() { return this.edcSessionManager; } getDebugSession(sessionId) { this.logger.info(`[getDebugSession] Attempting to retrieve session with ID: ${sessionId}. Current active session IDs: ${Array.from(this.activeSessions.keys()).join(', ')}`); const wrappedSession = this.activeSessions.get(sessionId); if (!wrappedSession) { this.logger.warn(`[getDebugSession] Session not found for ID: ${sessionId}`); return undefined; } this.logger.info(`[getDebugSession] Session found for ID: ${sessionId}`); return wrappedSession?.originalSession; } getDAPSessionHandler(sessionId) { this.logger.info(`[getDAPSessionHandler] Attempting to retrieve DAPSessionHandler for session ID: ${sessionId}.`); const wrappedSession = this.activeSessions.get(sessionId); if (!wrappedSession) { this.logger.warn(`[getDAPSessionHandler] No WrappedDebugSession found for ID: ${sessionId}`); return undefined; } const dapHandler = wrappedSession.dapSessionHandler; if (!dapHandler) { this.logger.warn(`[getDAPSessionHandler] WrappedDebugSession for ID: ${sessionId} does not have a DAPSessionHandler.`); } return dapHandler; } createDebugSession(dapHandler, targetType, targetPath, logger) { return new ergonomic_debugger_client_1.DebugSession(targetType, targetPath, dapHandler, logger); } storeDebugSession(mcpSessionId, session, startTime, targetType, targetPath) { this.logger.info(`[storeDebugSession] Storing session with MCP ID: ${mcpSessionId} (DebugClientSession ID: ${session.id}), Type: ${targetType}, Path: ${targetPath}`); const wrappedSession = new types_1.WrappedDebugSession(mcpSessionId, session, targetType, targetPath, startTime, this.logger); this.activeSessions.set(mcpSessionId, wrappedSession); this.logger.info(`[storeDebugSession] Session ${mcpSessionId} stored. Current active session IDs: ${Array.from(this.activeSessions.keys()).join(', ')}`); const dapHandler = wrappedSession.dapSessionHandler; this.logger.debug(`[storeDebugSession] Setting up event handlers for session ${mcpSessionId}. DapHandler: ${!!dapHandler}`); if (dapHandler && typeof dapHandler.on === 'function') { dapHandler.on('sessionEnded', (payload) => { this.logger.info(`[DAPEvent] SessionEnded for mcpSessionId: ${mcpSessionId}, dcSessionId: ${payload.sessionId}. Reason: ${payload.reason}`); const wasActive = this.activeSessions.delete(mcpSessionId); if (wasActive) { this.logger.info(`[storeDebugSession - dapHandler.on('sessionEnded')] Session ${mcpSessionId} removed from activeSessions.`); } else { this.logger.warn(`[storeDebugSession - dapHandler.on('sessionEnded')] Session ${mcpSessionId} was already removed or not found when 'sessionEnded' received.`); } this._queueAsyncEvent(mcpSessionId, 'unexpected_session_termination', { reason: payload.reason, restart: payload.restart, underlyingError: payload.underlyingError, exitCode: payload.exitCode, signal: payload.signal, }); }); dapHandler.on('error', (payload) => { if (payload.sessionId === session.id) { this.logger.error(`[storeDebugSession - dapHandler.on('error')] Received for debugClientSessionId: ${payload.sessionId} (MCP ID: ${mcpSessionId})`, payload.error); this._queueAsyncEvent(mcpSessionId, 'dap_session_handler_error', { message: payload.error.message, name: payload.error.name, stack: payload.error.stack, }); } }); dapHandler.on('output', (payload) => { if (payload.sessionId === mcpSessionId) { this.logger.debug(`[DAPEvent] Output for mcpSessionId: ${mcpSessionId}, dcSessionId: ${payload.sessionId}, category: ${payload.category}`); this._queueAsyncEvent(mcpSessionId, 'dap_event_output', { category: payload.category, output: payload.output, data: payload.data, }); } }); dapHandler.on('stopped', (payload) => { if (payload.sessionId === mcpSessionId) { this._queueAsyncEvent(mcpSessionId, 'dap_event_stopped', payload); } }); dapHandler.on('continued', (payload) => { if (payload.sessionId === mcpSessionId) { this._queueAsyncEvent(mcpSessionId, 'dap_event_continued', payload); } }); dapHandler.on('threadAdded', (payload) => { if (payload.sessionId === mcpSessionId) { this._queueAsyncEvent(mcpSessionId, 'dap_event_thread', { event: 'started', ...payload, }); } }); dapHandler.on('threadRemoved', (payload) => { if (payload.sessionId === mcpSessionId) { this._queueAsyncEvent(mcpSessionId, 'dap_event_thread', { event: 'exited', ...payload, }); } }); dapHandler.on('capabilitiesUpdated', (payload) => { if (payload.sessionId === mcpSessionId) { this._queueAsyncEvent(mcpSessionId, 'dap_event_capabilities', payload); } }); // Add more listeners as defined in McpAsyncEventType and DAPSessionHandlerEvents. } } _queueAsyncEvent(sessionId, eventType, data) { if (this.asyncEventQueue.length >= this.MAX_ASYNC_EVENT_QUEUE_SIZE) { const oldestEvent = this.asyncEventQueue.shift(); this.logger.warn(`Async event queue full. Dropped oldest event: ${oldestEvent?.eventId} (${oldestEvent?.eventType})`); } const event = { eventId: (0, crypto_1.randomUUID)(), timestamp: new Date().toISOString(), sessionId, eventType, data, }; this.asyncEventQueue.push(event); this.logger.info(`[AsyncEventQueued] Event: ${eventType}, SessionID: ${sessionId}, EventID: ${event.eventId}, QueueSize: ${this.asyncEventQueue.length}`); } queueAsyncEvent(sessionId, eventType, data) { this._queueAsyncEvent(sessionId, eventType, data); } drainAsyncEventQueue() { const events = [...this.asyncEventQueue]; this.asyncEventQueue = []; this.logger.info(`Drained ${events.length} async events.`); return events; } /** * Finds and optionally removes the first occurrence of a specific event type for a given session ID. * @param sessionId The session ID to check for. * @param eventType The type of event to look for. * @param removeIfFound If true, removes the event from the queue. * @returns The found McpAsyncEvent or undefined. */ findAndConsumeSessionEvent(sessionId, eventType, removeIfFound = true) { const eventIndex = this.asyncEventQueue.findIndex((event) => event.sessionId === sessionId && event.eventType === eventType); if (eventIndex > -1) { const foundEvent = this.asyncEventQueue[eventIndex]; if (removeIfFound) { this.asyncEventQueue.splice(eventIndex, 1); this.logger.info(`[findAndConsumeSessionEvent] Found and removed event ${foundEvent.eventId} (${foundEvent.eventType}) for session ${sessionId}. Queue size: ${this.asyncEventQueue.length}`); } else { this.logger.info(`[findAndConsumeSessionEvent] Found event ${foundEvent.eventId} (${foundEvent.eventType}) for session ${sessionId} (did not remove).`); } return foundEvent; } return undefined; } getEventsForSession(sessionId) { return this.asyncEventQueue.filter((event) => event.sessionId === sessionId); } removeDebugSession(sessionId) { this.logger.info(`[removeDebugSession] Attempting to remove session with ID: ${sessionId}`); const deleted = this.activeSessions.delete(sessionId); if (deleted) { this.logger.info(`[removeDebugSession] Session ${sessionId} removed. Current active session IDs: ${Array.from(this.activeSessions.keys()).join(', ')}`); } else { this.logger.warn(`[removeDebugSession] Session ${sessionId} not found for removal.`); } } getLogger() { return this.logger; } mapStatusToSessionState(status) { switch (status) { case 'pending': return types_1.SessionState.INITIALIZING; case 'initializing': return types_1.SessionState.INITIALIZING; case 'initialized': return types_1.SessionState.CONFIGURED; case 'active': return types_1.SessionState.RUNNING; case 'stopped': return types_1.SessionState.STOPPED; case 'terminating': return types_1.SessionState.TERMINATED; case 'terminated': return types_1.SessionState.TERMINATED; default: // This case should not be hit if all SessionStatus enum members are covered. // If a new status is added to SessionStatus, it needs mapping here. this.logger.warn(`Unknown ergonomic-debugger-client SessionStatus: '${status}', defaulting to ModelSessionState.INITIALIZING. Please map this state.`); return types_1.SessionState.INITIALIZING; } } getAllSessionInfo() { const infos = []; for (const wrappedSession of this.activeSessions.values()) { infos.push({ id: wrappedSession.id, targetType: wrappedSession.targetType, targetPath: wrappedSession.targetPath, state: this.mapStatusToSessionState(wrappedSession.status), startTime: wrappedSession.startTime, }); } return infos; } initializeTools() { registerTool('start_debug_session', 'Starts a new debug session for a target program, returning session ID and initial state.', schemas_1.startDebugSessionSchema, tools_1.handleStartDebugSession); registerTool('set_breakpoint', 'Sets a breakpoint in a file at a given line.', schemas_1.setBreakpointSchema, tools_1.handleSetBreakpoint); registerTool('evaluate_expression', 'Evaluates an expression in the context of a debug session.', schemas_1.evaluateExpressionSchema, tools_1.handleEvaluateExpression); registerTool('get_variables', 'Retrieves child variables for a given scope or structured variable reference.', schemas_1.getVariablesSchema, tools_1.handleGetVariables); registerTool('get_call_stack', 'Retrieves the current call stack for a thread in a debug session.', schemas_1.getCallStackSchema, tools_1.handleGetCallStack); registerTool('continue', 'Continues execution of the debug target and returns the current state of the debug session.', schemas_1.continueSchema, tools_1.handleContinue); registerTool('step_over', 'Steps over the current line.', schemas_1.stepOverSchema, tools_1.handleStepOver); registerTool('step_in', 'Steps into the function call at the current execution line.', schemas_1.stepInSchema, tools_1.handleStepIn); registerTool('step_out', 'Steps out of the current function.', schemas_1.stepOutSchema, tools_1.handleStepOut); registerTool('terminate_session', 'Terminates a debug session and the debuggee process.', schemas_1.terminateSessionSchema, tools_1.handleTerminateSession); registerTool('list_sessions', 'Lists all active debug sessions.', schemas_1.listSessionsSchema, tools_1.handleListSessions); registerTool('list_adapters', 'Lists all registered debug adapter configurations.', schemas_1.listAdaptersSchema, tools_1.handleListAdapters); registerTool('get_pending_async_events', 'Retrieves any pending asynchronous debug events queued on the server.', schemas_1.getPendingAsyncEventsSchema, tools_1.handleGetPendingAsyncEvents); registerTool('get_session_details', 'Retrieves detailed information about a specific debug session, including its state (e.g., running, paused, terminated), active threads, and the call stack if the session is paused. Useful for understanding the current execution context, inspecting variables, or deciding the next debugging step.', schemas_1.getSessionDetailsSchema, tools_1.handleGetSessionDetails); } formatToolResponse(result, eventsToReturn) { let baseProperties; if (result === undefined || result === null) { baseProperties = { success: true }; } else if (Array.isArray(result)) { baseProperties = { value: result }; } else if (typeof result === 'object') { baseProperties = { ...result }; } else { baseProperties = { value: result }; } const finalPayload = { ...baseProperties, asyncEvents: eventsToReturn || [], }; if (eventsToReturn && eventsToReturn.length > 0) { this.logger.info(`[formatToolResponse] Added ${eventsToReturn.length} async events to the response payload.`); } // If the only information is 'success: true' and there are no async events, // standardize to a minimal success response. // This covers cases where the tool handler returns void, null, or {success: true} if (finalPayload.asyncEvents.length === 0 && finalPayload.success === true && Object.keys(finalPayload).filter((k) => k !== 'asyncEvents' && k !== 'success').length === 0) { return { content: [ { type: 'text', text: JSON.stringify({ success: true, asyncEvents: [], }), }, ], }; } return { content: [{ type: 'text', text: JSON.stringify(finalPayload) }], }; } handleToolError(toolName, error, sessionId) { this.logger.error(`[MCP Tool Error] ${toolName} (Session: ${sessionId || 'N/A'}):`, error); if (error instanceof types_js_1.McpError) { throw error; } let mcpDebugErrorType = 'tool_internal_error'; let additionalDebugData = {}; if (error instanceof ergonomic_debugger_client_1.AdapterProcessError) { mcpDebugErrorType = 'adapter_process_error'; additionalDebugData = { adapterType: error.adapterType, adapterConfig: error.adapterConfig, spawnErrorStage: error.stage, spawnDetails: { command: error.adapterConfig.command, args: error.adapterConfig.args, cwd: error.adapterConfig.cwd, env: error.adapterConfig.env, stderr: error.stderrOutput, exitCode: error.exitCode, signal: error.signal, }, }; } else if (error instanceof ergonomic_debugger_client_1.DAPRequestError) { mcpDebugErrorType = 'dap_request_error'; additionalDebugData = { dapRequestCommand: error.request?.command, dapResponseErrorBody: error.response?.body, }; } else if (error instanceof Error && (error.name === 'TimeoutError' || error.message.toLowerCase().includes('timeout'))) { mcpDebugErrorType = 'operation_timeout'; } throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType(mcpDebugErrorType) .sessionProvider(this) .sessionId(sessionId) .additionalDebugData(additionalDebugData) .build(); } setupToolHandlers() { this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: Array.from(toolRegistry.entries()).map(([name, { description, schema }]) => ({ name, description, inputSchema: schema, })), })); this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { const toolInfo = toolRegistry.get(name); if (!toolInfo) { throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } const result = await toolInfo.handler(this, args); const eventsToReturn = [...this.asyncEventQueue]; this.asyncEventQueue = []; if (eventsToReturn.length > 0) { this.logger.info(`[CallToolRequestSchema] Piggybacking ${eventsToReturn.length} async events with tool response for '${name}'.`); } return this.formatToolResponse(result, eventsToReturn); } catch (error) { let currentSessionId = undefined; if (args && typeof args === 'object' && args !== null && 'sessionId' in args && typeof args.sessionId === 'string') { currentSessionId = args.sessionId; } this.handleToolError(name, error, currentSessionId); // This throw is technically unreachable because handleToolError always throws. throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Unhandled error in tool ${name}`); } }); } _parseRequestBody(rawBody, contentType) { if (contentType && contentType.toLowerCase().includes('application/json')) { if (!rawBody) { // Allow empty body for GET or if no body is expected for POST/PUT return undefined; } try { return JSON.parse(rawBody); } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); this.logger.error('Failed to parse JSON request body:', errorMessage); return new types_js_1.McpError(JSONRPC_ERROR_PARSE_ERROR, 'Parse error'); } } else if (rawBody) { // If there's a body but not JSON this.logger.error(`Received request with non-JSON content type: ${contentType} and a body.`); return new types_js_1.McpError(JSONRPC_ERROR_INVALID_REQUEST, 'Invalid Request: Content-Type must be application/json for requests with a body'); } return undefined; } /** * Handles incoming HTTP requests for the MCP server. * This method is called by the HTTP server created in the `run` method. */ async _handleHttpRequest(req, res, mcpTransport) { if (req.url !== MCP_HTTP_ENDPOINT) { res.writeHead(HTTP_STATUS_NOT_FOUND, { 'Content-Type': 'application/json', }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: JSONRPC_ERROR_METHOD_NOT_FOUND, message: 'Method not found', }, id: null, })); return; } let rawBody = ''; req.on('data', (chunk) => { rawBody += chunk.toString(); }); req.on('end', async () => { let bodyForTransport; if (req.method === 'POST' || req.method === 'PUT') { const parsedResult = this._parseRequestBody(rawBody, req.headers['content-type']); if (parsedResult instanceof types_js_1.McpError) { res.writeHead(parsedResult.code === JSONRPC_ERROR_PARSE_ERROR ? HTTP_STATUS_BAD_REQUEST : HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: parsedResult.code, message: parsedResult.message }, id: null, })); return; } bodyForTransport = parsedResult; } else { bodyForTransport = undefined; } await mcpTransport.handleRequest(req, res, bodyForTransport); }); } /** * Runs the MCP server */ async run() { let mcpTransport; let httpServer; const runArgv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) .option('port', { alias: 'p', type: 'number', description: 'Port to run the HTTP server on for StreamableHTTPServerTransport.', }) .option('adapter-config', { alias: 'ac', type: 'string', description: 'Path to the adapter configurations JSON file.', }) .usage('Usage: $0 [options]') .help() .alias('help', 'h') .version(this.serverInfo.version) .alias('version', 'v') .parseSync(); const port = runArgv.port; console.error('AI Debugger MCP Server initializing...'); if (port && !isNaN(port)) { const transportOptions = { sessionIdGenerator: () => (0, crypto_1.randomUUID)(), onsessioninitialized: (sessionId) => { this.logger.info(`HTTP Session initialized by transport: ${sessionId}`); }, }; const httpTransportInstance = new streamableHttp_js_1.StreamableHTTPServerTransport(transportOptions); mcpTransport = httpTransportInstance; httpTransportInstance.onclose = () => { this.logger.info(`HTTP Transport closed.`); }; await this.server.connect(httpTransportInstance); httpServer = http.createServer((req, res) => { this._handleHttpRequest(req, res, httpTransportInstance); }); httpServer.listen(port, () => { console.error(`AI Debugger MCP Server started on port ${port}`); }); // Ensure SIGINT is handled gracefully for HTTP server process.removeAllListeners('SIGINT'); process.on('SIGINT', async () => { this.logger.info('SIGINT received. Closing HTTP server first...'); if (httpServer) { httpServer.close(async () => { this.logger.info('HTTP server closed.'); await this.handleShutdown(); }); } else { await this.handleShutdown(); } }); } else { mcpTransport = new stdio_js_1.StdioServerTransport(); await this.server.connect(mcpTransport); console.error('AI Debugger MCP Server running on stdio'); } } } exports.InteractiveDebuggerMcpServer = InteractiveDebuggerMcpServer; // Create and run the server only if this script is executed directly if (require.main === module) { const server = new InteractiveDebuggerMcpServer(); server.run().catch((error) => { console.error('Failed to start AI Debugger MCP Server:', error); process.exit(1); }); }