ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
386 lines • 14.7 kB
JavaScript
import { EventEmitter } from 'events';
import * as inspector from 'inspector';
import WebSocket from 'ws';
import { EventListenerManager } from './utils/event-listener-manager.js';
export class BackendDebugEngine extends EventEmitter {
sessions = new Map();
inspectorSession;
metricsInterval;
queryCaptures = new Map();
apiTraces = new Map();
eventListenerManager = new EventListenerManager();
webSocketListeners = new Map();
cdpListeners = new Map();
async attachToProcess(options) {
const sessionId = this.generateSessionId();
try {
// If attaching to current process
if (!options.pid && !options.port) {
inspector.open(options.port || 9229, options.host || '127.0.0.1', true);
}
const session = new inspector.Session();
session.connect();
// Enable necessary domains
await this.enableDebugDomains(session);
const processInfo = await this.getProcessInfo();
this.sessions.set(sessionId, {
id: sessionId,
url: `${options.host || 'localhost'}:${options.port || 9229}`,
events: [],
nodeSession: session,
processInfo,
startTime: new Date()
});
// Start collecting metrics
this.startMetricsCollection(sessionId);
return { sessionId, processInfo };
}
catch (error) {
throw new Error(`Failed to attach to process: ${error}`);
}
}
async profileMemory(options) {
const session = this.sessions.get(options.sessionId);
if (!session?.nodeSession) {
throw new Error('Session not found or not attached to process');
}
const startMetrics = process.memoryUsage();
const profile = {
samples: [],
timestamps: []
};
// Start heap profiling
if (options.trackAllocations) {
await this.sendCommand(session.nodeSession, 'HeapProfiler.startTrackingHeapObjects', {
trackAllocations: true
});
}
// Collect samples over duration
const sampleInterval = setInterval(() => {
const memory = process.memoryUsage();
profile.samples.push(memory);
profile.timestamps.push(Date.now());
}, 100);
await new Promise(resolve => setTimeout(resolve, options.duration));
clearInterval(sampleInterval);
// Take heap snapshot if requested
let heapSnapshot;
if (options.heapSnapshot) {
heapSnapshot = await this.takeHeapSnapshot(session.nodeSession);
}
// Stop heap profiling
if (options.trackAllocations) {
await this.sendCommand(session.nodeSession, 'HeapProfiler.stopTrackingHeapObjects');
}
const endMetrics = process.memoryUsage();
return {
profile,
heapSnapshot,
summary: {
totalHeapSize: endMetrics.heapTotal,
usedHeapSize: endMetrics.heapUsed,
peakHeapSize: Math.max(...profile.samples.map((s) => s.heapUsed)),
gcCount: profile.samples.length, // Simplified
gcTime: options.duration
}
};
}
async traceAsyncOperations(options) {
const session = this.sessions.get(options.sessionId);
if (!session?.nodeSession) {
throw new Error('Session not found');
}
const traces = [];
// Enable async stack traces
await this.sendCommand(session.nodeSession, 'Debugger.setAsyncCallStackDepth', {
maxDepth: 32
});
// Set up async hooks
const asyncHooks = await import('async_hooks');
const hook = asyncHooks.createHook({
init(asyncId, type, triggerAsyncId) {
if ((options.includePromises && type === 'PROMISE') ||
(options.includeTimers && type.includes('TIMEOUT')) ||
(options.includeCallbacks && type === 'ASYNCRESOURCE')) {
traces.push({
id: asyncId,
type,
triggerId: triggerAsyncId,
timestamp: Date.now(),
stack: new Error().stack
});
}
},
destroy(asyncId) {
const trace = traces.find(t => t.id === asyncId);
if (trace) {
trace.endTime = Date.now();
trace.duration = trace.endTime - trace.timestamp;
}
}
});
hook.enable();
await new Promise(resolve => setTimeout(resolve, options.duration));
hook.disable();
return {
traces,
summary: {
totalOperations: traces.length,
pendingPromises: traces.filter(t => t.type === 'PROMISE' && !t.endTime).length,
activeTimers: traces.filter(t => t.type.includes('TIMEOUT') && !t.endTime).length,
callbackQueue: traces.filter(t => t.type === 'ASYNCRESOURCE' && !t.endTime).length
}
};
}
async captureQueries(options) {
const sessionId = options.sessionId;
const queries = [];
// Store for this session
this.queryCaptures.set(sessionId, queries);
// Monkey-patch common database libraries
this.patchDatabaseLibraries(sessionId, options);
await new Promise(resolve => setTimeout(resolve, options.duration));
// Unpatch
this.unpatchDatabaseLibraries();
const capturedQueries = this.queryCaptures.get(sessionId) || [];
const minTime = options.minExecutionTime;
const filtered = minTime !== undefined
? capturedQueries.filter(q => q.duration >= minTime)
: capturedQueries;
return {
queries: filtered,
summary: {
total: filtered.length,
slowest: filtered.sort((a, b) => b.duration - a.duration)[0],
totalTime: filtered.reduce((sum, q) => sum + q.duration, 0),
averageTime: filtered.length ?
filtered.reduce((sum, q) => sum + q.duration, 0) / filtered.length : 0
}
};
}
async traceAPIRequest(options) {
const trace = {
method: options.method,
path: options.path,
statusCode: 0,
duration: 0,
middleware: [],
queries: [],
timestamp: Date.now()
};
// This would integrate with Express/Fastify/Koa middleware
// For now, returning a mock structure
return trace;
}
async monitorWebSocket(options) {
const ws = new WebSocket(options.url);
const messages = [];
const latency = [];
let connections = 0;
// Store WebSocket for cleanup tracking
const wsId = `${options.sessionId}-${Date.now()}`;
this.webSocketListeners.set(wsId, ws);
// Use EventListenerManager to track WebSocket listeners
const openHandler = () => {
connections++;
if (options.trackConnections) {
// console.log('WebSocket connected');
}
};
const messageHandler = (data) => {
if (options.captureMessages) {
messages.push({
type: 'received',
data: data.toString(),
timestamp: Date.now()
});
}
};
// Track listeners with EventListenerManager
this.eventListenerManager.addEventListener(ws, 'open', openHandler);
this.eventListenerManager.addEventListener(ws, 'message', messageHandler);
// Monitor for duration
await new Promise(resolve => setTimeout(resolve, options.duration));
// Cleanup: Remove tracked listeners
this.eventListenerManager.removeEventListener(ws, 'open', openHandler);
this.eventListenerManager.removeEventListener(ws, 'message', messageHandler);
this.webSocketListeners.delete(wsId);
ws.close();
return { connections, messages, latency };
}
// Helper methods
generateSessionId() {
return `backend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
async enableDebugDomains(session) {
await this.sendCommand(session, 'Runtime.enable');
await this.sendCommand(session, 'Debugger.enable');
await this.sendCommand(session, 'HeapProfiler.enable');
await this.sendCommand(session, 'Profiler.enable');
}
sendCommand(session, method, params) {
return new Promise((resolve, reject) => {
session.post(method, params, (err, result) => {
if (err)
reject(err);
else
resolve(result);
});
});
}
async getProcessInfo() {
return {
pid: process.pid,
title: process.title,
version: process.version,
platform: process.platform,
memory: process.memoryUsage(),
uptime: process.uptime()
};
}
startMetricsCollection(sessionId) {
this.metricsInterval = setInterval(() => {
const session = this.sessions.get(sessionId);
if (!session)
return;
const metrics = {
cpu: this.getCPUUsage(),
memory: process.memoryUsage(),
eventLoop: {
latency: 0, // Would use perf_hooks
activeHandles: process._getActiveHandles?.().length || 0,
activeRequests: process._getActiveRequests?.().length || 0
}
};
session.metrics = metrics;
this.emit('metrics', { sessionId, metrics });
}, 1000);
}
getCPUUsage() {
const usage = process.cpuUsage();
return {
user: usage.user,
system: usage.system,
percent: (usage.user + usage.system) / 1000000 // Simplified
};
}
async takeHeapSnapshot(session) {
const chunks = [];
// Store session for cleanup tracking
const sessionId = `heap-${Date.now()}`;
this.cdpListeners.set(sessionId, session);
const chunkHandler = (params) => {
chunks.push(params.chunk);
};
// Track CDP session listener with EventListenerManager
this.eventListenerManager.addEventListener(session, 'HeapProfiler.addHeapSnapshotChunk', chunkHandler);
await this.sendCommand(session, 'HeapProfiler.takeHeapSnapshot');
// Cleanup: Remove tracked listener
this.eventListenerManager.removeEventListener(session, 'HeapProfiler.addHeapSnapshotChunk', chunkHandler);
this.cdpListeners.delete(sessionId);
return chunks.join('');
}
patchDatabaseLibraries(sessionId, options) {
// This would patch common database libraries like:
// - pg (PostgreSQL)
// - mysql2
// - mongodb
// - prisma
// For now, this is a placeholder
}
unpatchDatabaseLibraries() {
// Restore original methods
}
async cleanup(sessionId) {
const session = this.sessions.get(sessionId);
if (session?.nodeSession) {
session.nodeSession.disconnect();
}
if (session?.websocket) {
session.websocket.close();
}
this.sessions.delete(sessionId);
// Cleanup EventListenerManager tracked listeners for this session
const listenersToRemove = [];
for (const [id, ws] of this.webSocketListeners.entries()) {
if (id.startsWith(sessionId)) {
listenersToRemove.push(id);
}
}
listenersToRemove.forEach(id => this.webSocketListeners.delete(id));
// Cleanup CDP listeners for this session
const cdpListenersToRemove = [];
for (const [id, cdpSession] of this.cdpListeners.entries()) {
if (id.startsWith(sessionId)) {
cdpListenersToRemove.push(id);
}
}
cdpListenersToRemove.forEach(id => this.cdpListeners.delete(id));
if (this.sessions.size === 0 && this.metricsInterval) {
clearInterval(this.metricsInterval);
}
}
/**
* Get count of active WebSocket listeners
*/
getActiveWebSocketListenerCount() {
return this.webSocketListeners.size;
}
/**
* Get count of active CDP session listeners
*/
getActiveCDPListenerCount() {
return this.cdpListeners.size;
}
/**
* Get detailed listener information for debugging
*/
getDetailedListenerInfo() {
const webSocketListeners = this.webSocketListeners.size;
const cdpListeners = this.cdpListeners.size;
const eventManagerListeners = this.eventListenerManager.getActiveListenerCount();
const listenerDetails = [
...Array.from(this.webSocketListeners.keys()).map(id => ({
type: 'WebSocket',
id,
addedAt: new Date()
})),
...Array.from(this.cdpListeners.keys()).map(id => ({
type: 'CDP',
id,
addedAt: new Date()
}))
];
return {
webSocketListeners,
cdpListeners,
totalListeners: webSocketListeners + cdpListeners + eventManagerListeners,
listenerDetails
};
}
/**
* Cleanup all listeners across all sessions
*/
async cleanupAll() {
// Cleanup all EventListenerManager tracked listeners
this.eventListenerManager.cleanup();
// Close all WebSocket connections
for (const [id, ws] of this.webSocketListeners.entries()) {
try {
ws.close();
}
catch (error) {
console.warn(`Failed to close WebSocket ${id}:`, error);
}
}
this.webSocketListeners.clear();
// Cleanup all CDP sessions
this.cdpListeners.clear();
// Cleanup all sessions
const sessionIds = Array.from(this.sessions.keys());
for (const sessionId of sessionIds) {
await this.cleanup(sessionId);
}
}
}
//# sourceMappingURL=backend-debug-engine.js.map