UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

182 lines 8.54 kB
/* 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