UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

678 lines 22.4 kB
#!/usr/bin/env node import * as path from 'path'; import * as net from 'net'; // Simple DAP connection implementation class DAPConnection { input; output; messageBuffer = ''; constructor(input = process.stdin, output = process.stdout) { this.input = input; this.output = output; } start() { this.input.on('data', (chunk) => { this.messageBuffer += chunk.toString(); this.processMessages(); }); } on(event, handler) { if (event === 'request') { this.onRequest = handler; } else if (event === 'disconnect') { this.input.on('end', () => handler({})); this.input.on('close', () => handler({})); } } sendResponse(response) { this.sendMessage(response); } sendEvent(event) { this.sendMessage(event); } onRequest; processMessages() { while (true) { const idx = this.messageBuffer.indexOf('\r\n\r\n'); if (idx === -1) break; const header = this.messageBuffer.substring(0, idx); const contentLengthMatch = header.match(/Content-Length: (\d+)/); if (!contentLengthMatch) { this.messageBuffer = this.messageBuffer.substring(idx + 4); continue; } const contentLength = parseInt(contentLengthMatch[1], 10); const messageStart = idx + 4; if (this.messageBuffer.length < messageStart + contentLength) break; const messageContent = this.messageBuffer.substring(messageStart, messageStart + contentLength); this.messageBuffer = this.messageBuffer.substring(messageStart + contentLength); try { const message = JSON.parse(messageContent); if (message.type === 'request' && this.onRequest) { this.onRequest(message); } } catch { // Ignore parse errors } } } sendMessage(message) { const json = JSON.stringify(message); const contentLength = Buffer.byteLength(json, 'utf8'); this.output.write(`Content-Length: ${contentLength}\r\n\r\n${json}`, 'utf8'); } } function createConnection(input, output) { return new DAPConnection(input, output); } /** * Mock DAP server implementation */ class MockDebugAdapterProcess { connection; server; isInitialized = false; breakpoints = new Map(); variableHandles = new Map(); nextVariableReference = 1000; currentLine = 1; isRunning = false; threads = [{ id: 1, name: 'main' }]; constructor() { // Parse command line arguments const args = process.argv.slice(2); let port; let host = 'localhost'; let sessionId = 'mock-session'; for (let i = 0; i < args.length; i++) { switch (args[i]) { case '--port': port = parseInt(args[i + 1], 10); i++; break; case '--host': host = args[i + 1]; i++; break; case '--session': sessionId = args[i + 1]; i++; break; } } // Log startup this.log(`Mock Debug Adapter Process started - session: ${sessionId}, host: ${host}, port: ${port || 'stdio'}`); if (port) { // Set up TCP server this.setupTCPServer(host, port); } else { // Use stdio this.connection = createConnection(); this.setupConnection(this.connection); this.connection.start(); } } setupTCPServer(host, port) { this.server = net.createServer((socket) => { this.log(`Client connected from ${socket.remoteAddress}:${socket.remotePort}`); // Create connection for this socket this.connection = createConnection(socket, socket); this.setupConnection(this.connection); this.connection.start(); socket.on('close', () => { this.log('Client socket closed'); // Don't exit the process, allow reconnections }); socket.on('error', (err) => { this.log(`Socket error: ${err.message}`); }); }); this.server.listen(port, host, () => { this.log(`TCP server listening on ${host}:${port}`); }); this.server.on('error', (err) => { this.log(`Server error: ${err.message}`); process.exit(1); }); } setupConnection(connection) { // Set up message handlers connection.on('request', this.handleRequest.bind(this)); connection.on('disconnect', () => { this.log('Client disconnected'); // For TCP connections, don't exit - allow reconnection if (!this.server) { process.exit(0); } }); } log(message) { // Log to stderr so it doesn't interfere with protocol messages console.error(`[MockDAP] ${message}`); } handleRequest(request) { this.log(`Received request: ${request.command}`); switch (request.command) { case 'initialize': this.handleInitialize(request); break; case 'configurationDone': this.handleConfigurationDone(request); break; case 'launch': this.handleLaunch(request); break; case 'setBreakpoints': this.handleSetBreakpoints(request); break; case 'threads': this.handleThreads(request); break; case 'stackTrace': this.handleStackTrace(request); break; case 'scopes': this.handleScopes(request); break; case 'variables': this.handleVariables(request); break; case 'continue': this.handleContinue(request); break; case 'next': this.handleNext(request); break; case 'stepIn': this.handleStepIn(request); break; case 'stepOut': this.handleStepOut(request); break; case 'pause': this.handlePause(request); break; case 'disconnect': this.handleDisconnect(request); break; case 'terminate': this.handleTerminate(request); break; default: this.sendErrorResponse(request, 1000, `Unhandled command: ${request.command}`); } } handleInitialize(request) { const response = { seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true, body: { supportsConfigurationDoneRequest: true, supportsFunctionBreakpoints: false, supportsConditionalBreakpoints: true, supportsHitConditionalBreakpoints: false, supportsEvaluateForHovers: true, exceptionBreakpointFilters: [], supportsStepBack: false, supportsSetVariable: true, supportsRestartFrame: false, supportsGotoTargetsRequest: false, supportsStepInTargetsRequest: false, supportsCompletionsRequest: false, supportsModulesRequest: false, supportsRestartRequest: false, supportsExceptionOptions: false, supportsValueFormattingOptions: false, supportsExceptionInfoRequest: false, supportTerminateDebuggee: true, supportSuspendDebuggee: false, supportsDelayedStackTraceLoading: false, supportsLoadedSourcesRequest: false, supportsLogPoints: false, supportsTerminateThreadsRequest: false, supportsSetExpression: false, supportsTerminateRequest: true, supportsDataBreakpoints: false, supportsReadMemoryRequest: false, supportsWriteMemoryRequest: false, supportsDisassembleRequest: false, supportsCancelRequest: false, supportsBreakpointLocationsRequest: false, supportsClipboardContext: false, supportsSteppingGranularity: false, supportsInstructionBreakpoints: false, supportsExceptionFilterOptions: false, supportsSingleThreadExecutionRequests: false } }; this.sendResponse(response); this.sendEvent({ seq: 0, type: 'event', event: 'initialized' }); this.isInitialized = true; } handleConfigurationDone(request) { this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }); } handleLaunch(request) { const args = request.arguments; this.log(`Launching with args: ${JSON.stringify(args)}`); this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }); // If stopOnEntry is set, send a stopped event if (args.stopOnEntry) { setTimeout(() => { this.log(`Sending stopped event for stopOnEntry`); this.sendEvent({ seq: 0, type: 'event', event: 'stopped', body: { reason: 'entry', threadId: 1, allThreadsStopped: true } }); }, 100); } else { this.isRunning = true; this.log(`Running without stopOnEntry, will hit first breakpoint`); // Simulate running to first breakpoint setTimeout(() => { const allBreakpoints = Array.from(this.breakpoints.entries()) .flatMap(([path, bps]) => bps.map(bp => ({ path, ...bp }))) .filter(bp => bp.line !== undefined) .sort((a, b) => (a.line || 0) - (b.line || 0)); if (allBreakpoints.length > 0) { const firstBreakpoint = allBreakpoints[0]; this.currentLine = firstBreakpoint.line || 1; this.isRunning = false; this.log(`Hit first breakpoint at line ${this.currentLine}`); this.sendEvent({ seq: 0, type: 'event', event: 'stopped', body: { reason: 'breakpoint', threadId: 1, allThreadsStopped: true } }); } else { this.log(`No breakpoints set, program would run to completion`); this.sendEvent({ seq: 0, type: 'event', event: 'terminated' }); this.sendEvent({ seq: 0, type: 'event', event: 'exited', body: { exitCode: 0 } }); } }, 200); } } handleSetBreakpoints(request) { const args = request.arguments; const breakpoints = []; if (args.breakpoints) { for (const bp of args.breakpoints) { breakpoints.push({ id: Math.floor(Math.random() * 100000), verified: true, line: bp.line, source: args.source }); } } this.breakpoints.set(args.source.path || 'unknown', breakpoints); this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true, body: { breakpoints } }); } handleThreads(request) { this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true, body: { threads: this.threads } }); } handleStackTrace(request) { const stackFrames = [ { id: 0, name: 'main', source: { name: 'main.mock', path: path.join(process.cwd(), 'main.mock') }, line: this.currentLine, column: 0 }, { id: 1, name: 'mockFunction', source: { name: 'lib.mock', path: path.join(process.cwd(), 'lib.mock') }, line: 42, column: 0 } ]; this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true, body: { stackFrames, totalFrames: stackFrames.length } }); } handleScopes(request) { const scopes = [ { name: 'Locals', variablesReference: this.getOrCreateVariableReference({ type: 'locals', variables: [ { name: 'x', value: '10', type: 'int' }, { name: 'y', value: '20', type: 'int' }, { name: 'result', value: '30', type: 'int' } ] }), expensive: false }, { name: 'Globals', variablesReference: this.getOrCreateVariableReference({ type: 'globals', variables: [ { name: '__name__', value: '"__main__"', type: 'str' }, { name: '__file__', value: '"simple-mock.js"', type: 'str' } ] }), expensive: false } ]; this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true, body: { scopes } }); } handleVariables(request) { const args = request.arguments; const data = this.variableHandles.get(args.variablesReference); const variables = []; if (data && data.variables) { for (const v of data.variables) { variables.push({ name: v.name, value: v.value, type: v.type, variablesReference: 0 }); } } this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true, body: { variables } }); } handleContinue(request) { this.isRunning = true; this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true, body: { allThreadsContinued: true } }); // Simulate hitting a breakpoint or terminating setTimeout(() => { const allBreakpoints = Array.from(this.breakpoints.entries()) .flatMap(([path, bps]) => bps.map(bp => ({ path, ...bp }))) .filter(bp => bp.line !== undefined) .sort((a, b) => (a.line || 0) - (b.line || 0)); this.log(`Continue from line ${this.currentLine}. All breakpoints: ${allBreakpoints.map(bp => bp.line).join(', ')}`); // Find next breakpoint after current line const nextBreakpoint = allBreakpoints.find(bp => (bp.line || 0) > this.currentLine); this.log(`Next breakpoint after line ${this.currentLine}: ${nextBreakpoint ? nextBreakpoint.line : 'none'}`); if (nextBreakpoint && nextBreakpoint.line) { // Hit the next breakpoint this.isRunning = false; this.currentLine = nextBreakpoint.line; this.log(`Stopping at breakpoint on line ${this.currentLine}`); this.sendEvent({ seq: 0, type: 'event', event: 'stopped', body: { reason: 'breakpoint', threadId: 1, allThreadsStopped: true } }); } else { // No more breakpoints - program terminated this.log(`No more breakpoints after line ${this.currentLine}, terminating program`); this.sendEvent({ seq: 0, type: 'event', event: 'terminated' }); this.sendEvent({ seq: 0, type: 'event', event: 'exited', body: { exitCode: 0 } }); } }, 200); } handleNext(request) { this.currentLine++; this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }); setTimeout(() => { this.sendEvent({ seq: 0, type: 'event', event: 'stopped', body: { reason: 'step', threadId: 1, allThreadsStopped: true } }); }, 50); } handleStepIn(request) { this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }); setTimeout(() => { this.sendEvent({ seq: 0, type: 'event', event: 'stopped', body: { reason: 'step', threadId: 1, allThreadsStopped: true } }); }, 50); } handleStepOut(request) { this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }); setTimeout(() => { this.sendEvent({ seq: 0, type: 'event', event: 'stopped', body: { reason: 'step', threadId: 1, allThreadsStopped: true } }); }, 50); } handlePause(request) { this.isRunning = false; this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }); this.sendEvent({ seq: 0, type: 'event', event: 'stopped', body: { reason: 'pause', threadId: 1, allThreadsStopped: true } }); } handleDisconnect(request) { this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }); setTimeout(() => { process.exit(0); }, 100); } handleTerminate(request) { this.sendResponse({ seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }); this.sendEvent({ seq: 0, type: 'event', event: 'terminated' }); setTimeout(() => { process.exit(0); }, 100); } sendResponse(response) { if (this.connection) { this.connection.sendResponse(response); } } sendEvent(event) { if (this.connection) { this.connection.sendEvent(event); } } sendErrorResponse(request, id, message) { const response = { seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: false, message, body: { error: { id, format: message } } }; this.sendResponse(response); } getOrCreateVariableReference(data) { const ref = this.nextVariableReference++; this.variableHandles.set(ref, data); return ref; } } // Start the mock debug adapter process new MockDebugAdapterProcess(); //# sourceMappingURL=mock-adapter-process.js.map