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