UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

186 lines 8.5 kB
/** * DAP connection management utilities */ export class DapConnectionManager { dapClientFactory; logger; INITIAL_CONNECT_DELAY = 500; MAX_CONNECT_ATTEMPTS = 60; CONNECT_RETRY_INTERVAL = 200; constructor(dapClientFactory, logger) { this.dapClientFactory = dapClientFactory; this.logger = logger; } /** * 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)); const client = 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) { const initializeArgs = { clientID: `mcp-proxy-${sessionId}`, clientName: 'MCP Debug Proxy', adapterID: 'python', pathFormat: 'path', linesStartAt1: true, columnsStartAt1: true, supportsVariableType: true, supportsRunInTerminalRequest: false, locale: 'en-US' }; this.logger.info('[ConnectionManager] Sending DAP "initialize" request'); await client.sendRequest('initialize', initializeArgs); this.logger.info('[ConnectionManager] DAP "initialize" request sent and response received.'); } /** * 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) { // 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); // Pass paths exactly as provided - no manipulation const launchArgs = { program: scriptPath, stopOnEntry, noDebug: false, args: scriptArgs, // Don't set cwd - let debugpy use its inherited working directory console: "internalConsole", justMyCode, }; // 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