UNPKG

@debugmcp/mcp-debugger

Version:

Step-through debugging MCP server for LLMs

231 lines 11.3 kB
/** * DAP connection management utilities */ export class DapConnectionManager { dapClientFactory; logger; // Increased initial delay to give debugpy more time to start // This is especially important in CI/test environments INITIAL_CONNECT_DELAY = 500; MAX_CONNECT_ATTEMPTS = 60; CONNECT_RETRY_INTERVAL = 200; policy; constructor(dapClientFactory, logger) { this.dapClientFactory = dapClientFactory; this.logger = logger; } /** * Set the adapter policy for creating DAP clients */ setAdapterPolicy(policy) { this.policy = policy; } /** * Connect to DAP adapter with retry logic */ async connectWithRetry(host, port) { this.logger.info(`[ConnectionManager] Waiting ${this.INITIAL_CONNECT_DELAY}ms before first DAP connect attempt.`); await new Promise(resolve => setTimeout(resolve, this.INITIAL_CONNECT_DELAY)); // Create client with policy if available const client = this.policy ? this.dapClientFactory.create(host, port, this.policy) : this.dapClientFactory.create(host, port); // Temporary error handler to prevent unhandled 'error' event crashes during connect attempts const tempErrorHandler = (err) => { this.logger.debug(`[ConnectionManager] DAP client emitted 'error' during connection phase (expected for retries): ${err.message}`); }; client.on('error', tempErrorHandler); let connectAttempts = 0; while (connectAttempts < this.MAX_CONNECT_ATTEMPTS) { try { this.logger.info(`[ConnectionManager] Attempting DAP client connect (attempt ${connectAttempts + 1}/${this.MAX_CONNECT_ATTEMPTS}) to ${host}:${port}`); await client.connect(); this.logger.info('[ConnectionManager] DAP client connected to adapter successfully.'); // Remove temporary handler as connection succeeded client.off('error', tempErrorHandler); return client; } catch (err) { connectAttempts++; const errMessage = err instanceof Error ? err.message : String(err); if (connectAttempts >= this.MAX_CONNECT_ATTEMPTS) { this.logger.error(`[ConnectionManager] Failed to connect DAP client after ${this.MAX_CONNECT_ATTEMPTS} attempts. Last error: ${errMessage}`); client.off('error', tempErrorHandler); throw new Error(`Failed to connect DAP client: ${errMessage}`); } this.logger.warn(`[ConnectionManager] DAP client connect attempt ${connectAttempts} failed: ${errMessage}. Retrying in ${this.CONNECT_RETRY_INTERVAL}ms...`); await new Promise(resolve => setTimeout(resolve, this.CONNECT_RETRY_INTERVAL)); } } // This should never be reached due to the throw above, but TypeScript needs it throw new Error('Connection retry loop exited unexpectedly'); } /** * Initialize DAP session */ async initializeSession(client, sessionId, adapterId = 'python') { const initializeArgs = { clientID: `mcp-proxy-${sessionId}`, clientName: 'MCP Debug Proxy', adapterID: adapterId, pathFormat: 'path', linesStartAt1: true, columnsStartAt1: true, supportsVariableType: true, supportsRunInTerminalRequest: false, locale: 'en-US' }; this.logger.info('[ConnectionManager] Sending DAP "initialize" request'); const initResponse = await client.sendRequest('initialize', initializeArgs); this.logger.info('[ConnectionManager] DAP "initialize" request sent and response received.'); // TEMPORARY DEBUG: Log capabilities to check for breakpointLocations support console.error('[DEBUG-DAP] Initialize response capabilities:', JSON.stringify(initResponse, null, 2)); if (initResponse && typeof initResponse === 'object' && 'body' in initResponse) { const typedResponse = initResponse; const capabilities = typedResponse.body; if (capabilities) { console.error('[DEBUG-DAP] Breakpoint-related capabilities:', { supportsBreakpointLocationsRequest: capabilities.supportsBreakpointLocationsRequest, supportsConditionalBreakpoints: capabilities.supportsConditionalBreakpoints, supportsHitConditionalBreakpoints: capabilities.supportsHitConditionalBreakpoints, supportsLogPoints: capabilities.supportsLogPoints, supportsDataBreakpoints: capabilities.supportsDataBreakpoints, supportsFunctionBreakpoints: capabilities.supportsFunctionBreakpoints, supportsInstructionBreakpoints: capabilities.supportsInstructionBreakpoints, supportsExceptionInfoRequest: capabilities.supportsExceptionInfoRequest, supportsExceptionOptions: capabilities.supportsExceptionOptions, supportsExceptionConditions: capabilities.supportsExceptionConditions, supportsExceptionFilterOptions: capabilities.supportsExceptionFilterOptions }); } } } /** * Set up event handlers for a DAP client */ setupEventHandlers(client, handlers) { if (handlers.onInitialized) { client.on('initialized', handlers.onInitialized); } if (handlers.onOutput) { client.on('output', handlers.onOutput); } if (handlers.onStopped) { client.on('stopped', handlers.onStopped); } if (handlers.onContinued) { client.on('continued', handlers.onContinued); } if (handlers.onThread) { client.on('thread', handlers.onThread); } if (handlers.onExited) { client.on('exited', handlers.onExited); } if (handlers.onTerminated) { client.on('terminated', handlers.onTerminated); } if (handlers.onError) { client.on('error', handlers.onError); } if (handlers.onClose) { client.on('close', handlers.onClose); } this.logger.info('[ConnectionManager] DAP event handlers set up'); } /** * Disconnect DAP client gracefully */ async disconnect(client, terminateDebuggee = true) { if (!client) { this.logger.info('[ConnectionManager] No active DAP client to disconnect.'); return; } this.logger.info('[ConnectionManager] Attempting graceful DAP disconnect.'); try { this.logger.info('[ConnectionManager] Sending "disconnect" request to DAP adapter...'); await Promise.race([ client.sendRequest('disconnect', { terminateDebuggee }), new Promise((_, reject) => setTimeout(() => reject(new Error('DAP disconnect request timed out after 1000ms')), 1000)) ]); this.logger.info('[ConnectionManager] DAP "disconnect" request completed.'); } catch (e) { const message = e instanceof Error ? e.message : String(e); this.logger.warn(`[ConnectionManager] Error or timeout during DAP "disconnect" request: ${message}`); } // Always call the client's disconnect method to clean up try { this.logger.info('[ConnectionManager] Calling client.disconnect() for final cleanup.'); client.disconnect(); this.logger.info('[ConnectionManager] Client disconnected.'); } catch (e) { const message = e instanceof Error ? e.message : String(e); this.logger.error(`[ConnectionManager] Error calling client.disconnect(): ${message}`, e); } } /** * Send a launch request with proper configuration */ async sendLaunchRequest(client, scriptPath, scriptArgs = [], stopOnEntry = true, justMyCode = true, launchConfig) { // DIAGNOSTIC: Log the incoming scriptPath this.logger.info('[ConnectionManager] DIAGNOSTIC: Received scriptPath:', scriptPath); this.logger.info('[ConnectionManager] DIAGNOSTIC: scriptPath type:', typeof scriptPath); this.logger.info('[ConnectionManager] DIAGNOSTIC: scriptPath length:', scriptPath.length); const baseLaunchArgs = launchConfig ? { ...launchConfig } : {}; const resolvedProgram = typeof baseLaunchArgs.program === 'string' && baseLaunchArgs.program.length > 0 ? baseLaunchArgs.program : scriptPath; const resolvedArgs = Array.isArray(baseLaunchArgs.args) ? baseLaunchArgs.args.filter((arg) => typeof arg === 'string') : scriptArgs; const resolvedStopOnEntry = typeof baseLaunchArgs.stopOnEntry === 'boolean' ? baseLaunchArgs.stopOnEntry : stopOnEntry; const resolvedJustMyCode = typeof baseLaunchArgs.justMyCode === 'boolean' ? baseLaunchArgs.justMyCode : justMyCode; const launchArgs = { ...baseLaunchArgs, program: resolvedProgram, args: resolvedArgs, stopOnEntry: resolvedStopOnEntry, justMyCode: resolvedJustMyCode, }; if (!('noDebug' in launchArgs)) { launchArgs.noDebug = false; } if (!('console' in launchArgs)) { launchArgs.console = 'internalConsole'; } // DIAGNOSTIC: Log the launch args object before sending this.logger.info('[ConnectionManager] DIAGNOSTIC: launchArgs object:', JSON.stringify(launchArgs, null, 2)); this.logger.info('[ConnectionManager] DIAGNOSTIC: launchArgs.program value:', launchArgs.program); this.logger.info('[ConnectionManager] Sending "launch" request to adapter with args:', launchArgs); await client.sendRequest('launch', launchArgs); this.logger.info('[ConnectionManager] DAP "launch" request sent.'); } /** * Set breakpoints for a file */ async setBreakpoints(client, sourcePath, breakpoints) { const sourceBreakpoints = breakpoints.map(bp => ({ line: bp.line, condition: bp.condition })); const setBreakpointsArgs = { source: { path: sourcePath }, breakpoints: sourceBreakpoints }; this.logger.info(`[ConnectionManager] Setting ${breakpoints.length} breakpoint(s) for ${sourcePath}`); const response = await client.sendRequest('setBreakpoints', setBreakpointsArgs); this.logger.info('[ConnectionManager] Breakpoints set. Response:', response); return response; } /** * Send configuration done notification */ async sendConfigurationDone(client) { this.logger.info('[ConnectionManager] Sending "configurationDone" to adapter.'); await client.sendRequest('configurationDone', {}); this.logger.info('[ConnectionManager] "configurationDone" sent.'); } } //# sourceMappingURL=dap-proxy-connection-manager.js.map