@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
182 lines • 8.54 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import net from 'net';
import { EventEmitter } from 'events';
import { createLogger } from '../utils/logger.js';
const logger = createLogger('debug-mcp:minimal-dap');
export class MinimalDapClient extends EventEmitter {
socket = null;
buffer = '';
seq = 1;
pendingRequests = new Map();
host;
port;
isDisconnectingOrDisconnected = false; // Added flag
constructor(host, port) {
super();
this.host = host;
this.port = port;
}
async connect() {
return new Promise((resolve, reject) => {
logger.info(`[MinimalDapClient] Connecting to ${this.host}:${this.port}`);
this.socket = net.createConnection({ host: this.host, port: this.port }, () => {
logger.info(`[MinimalDapClient] Connected to ${this.host}:${this.port}`);
resolve();
});
this.socket.on('data', (data) => this.handleData(data));
this.socket.on('error', (err) => {
logger.error(`[MinimalDapClient] Socket error:`, err);
this.emit('error', err);
reject(err);
});
this.socket.on('close', () => {
logger.info(`[MinimalDapClient] Socket closed`);
this.emit('close');
});
});
}
handleData(data) {
this.buffer += data.toString('utf8');
logger.debug(`[MinimalDapClient] Received data, buffer is now: "${this.buffer.substring(0, 100)}..."`);
while (true) {
const headerEnd = this.buffer.indexOf('\r\n\r\n');
if (headerEnd === -1) {
logger.debug('[MinimalDapClient] No complete header found in buffer.');
break;
}
const header = this.buffer.substring(0, headerEnd);
const contentLengthMatch = header.match(/Content-Length: (\d+)/i);
if (!contentLengthMatch) {
logger.error('[MinimalDapClient] Invalid DAP header (no Content-Length):', header);
this.buffer = this.buffer.substring(headerEnd + 4); // Skip malformed header
continue;
}
const contentLength = parseInt(contentLengthMatch[1], 10);
const messageStart = headerEnd + 4;
if (this.buffer.length < messageStart + contentLength) {
logger.debug(`[MinimalDapClient] Incomplete message body. Need ${contentLength}, have ${this.buffer.length - messageStart}.`);
break;
}
const messageJson = this.buffer.substring(messageStart, messageStart + contentLength);
const nextMessageStartInOriginalBuffer = messageStart + contentLength;
// Consume the processed message (or attempted-to-process message) from the buffer *before* parsing
// This prevents reprocessing a malformed message if JSON.parse throws.
this.buffer = this.buffer.substring(nextMessageStartInOriginalBuffer);
logger.debug(`[MinimalDapClient] Extracted message JSON (length ${contentLength}): ${messageJson.substring(0, 100)}...`);
logger.debug(`[MinimalDapClient] Buffer sliced. Remaining length: ${this.buffer.length}. Preview: "${this.buffer.substring(0, 100)}..."`);
try {
const message = JSON.parse(messageJson);
logger.info(`[MinimalDapClient] Parsed DAP message:`, {
type: message.type,
seq: message.seq,
command: message.command || message.command,
request_seq: message.request_seq,
event: message.event,
success: message.success,
});
if (message.type === 'response') {
const response = message;
if (this.pendingRequests.has(response.request_seq)) {
const promise = this.pendingRequests.get(response.request_seq);
clearTimeout(promise.timer);
if (response.success) {
promise.resolve(response);
}
else {
promise.reject(new Error(response.message || `DAP request failed (seq: ${response.request_seq})`));
}
this.pendingRequests.delete(response.request_seq);
}
else {
logger.warn(`[MinimalDapClient] Received response for unknown request_seq: ${response.request_seq}`);
}
}
else if (message.type === 'event') {
// The file-level disable should cover this now.
this.emit(message.event, message.body);
this.emit('event', message); // Generic event
logger.debug(`[MinimalDapClient] Emitted event: ${message.event}`);
}
else {
logger.warn('[MinimalDapClient] Received unknown DAP message type:', message.type, message);
}
}
catch (e) {
logger.error('[MinimalDapClient] Error parsing DAP message JSON:', e, { jsonPreview: messageJson.substring(0, 200) });
// Buffer was already advanced, so we don't re-process the bad JSON.
}
}
if (this.buffer.length > 0) {
logger.debug(`[MinimalDapClient] Exited message loop with remaining data in buffer (length: ${this.buffer.length}): "${this.buffer.substring(0, 100)}..."`);
}
else {
logger.debug('[MinimalDapClient] Exited message loop, buffer is empty.');
}
}
sendRequest(command, args) {
if (!this.socket || this.socket.destroyed) {
return Promise.reject(new Error('Socket not connected or destroyed'));
}
const requestSeq = this.seq++;
const request = {
seq: requestSeq,
type: 'request',
command: command,
arguments: args,
};
const json = JSON.stringify(request);
const header = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n`;
logger.info(`[MinimalDapClient] Sending DAP request:`, { command, seq: requestSeq, args: args || {} });
this.socket.write(header + json);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
if (this.pendingRequests.has(requestSeq)) {
this.pendingRequests.delete(requestSeq);
reject(new Error(`DAP request '${command}' (seq ${requestSeq}) timed out`));
}
}, 30000); // 30s timeout
this.pendingRequests.set(requestSeq, {
resolve: resolve,
reject,
timer
});
});
}
disconnect() {
this.shutdown('DAP client disconnected');
}
/**
* Shutdown the DAP client, rejecting all pending requests and disposing resources.
* This method is idempotent.
*/
shutdown(reason = 'dap client shutdown') {
if (this.isDisconnectingOrDisconnected) {
logger.info('[MinimalDapClient] Shutdown already in progress or completed.');
return;
}
this.isDisconnectingOrDisconnected = true;
logger.info(`[MinimalDapClient] Shutdown initiated. Reason: ${reason}`);
// Close socket if needed
if (this.socket && !this.socket.destroyed) {
this.socket.end(() => {
logger.info('[MinimalDapClient] Socket ended gracefully.');
});
this.socket.destroy();
logger.info('[MinimalDapClient] Socket destroyed.');
}
this.socket = null;
// Reject and clear all pending requests
if (this.pendingRequests.size > 0) {
logger.info(`[MinimalDapClient] Rejecting ${this.pendingRequests.size} pending requests on shutdown.`);
this.pendingRequests.forEach(({ reject, timer }) => {
clearTimeout(timer);
reject(new Error(reason));
});
this.pendingRequests.clear();
}
// Remove listeners
logger.info('[MinimalDapClient] Removing all event listeners.');
this.removeAllListeners();
}
}
//# sourceMappingURL=minimal-dap.js.map