@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
186 lines • 8.5 kB
JavaScript
/**
* 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