UNPKG

@ai-capabilities-suite/mcp-debugger-server

Version:

Enterprise-grade MCP server providing advanced debugging capabilities for Node.js and TypeScript applications. Features 25+ debugging tools including breakpoints, variable inspection, execution control, CPU/memory profiling, hang detection, source map sup

1,239 lines (1,238 loc) 91.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.McpDebuggerServer = void 0; exports.startMcpDebuggerServer = startMcpDebuggerServer; const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const zod_1 = require("zod"); const mcp_debugger_core_1 = require("@ai-capabilities-suite/mcp-debugger-core"); /** * MCP Debugger Server * Provides debugging capabilities for Node.js and TypeScript applications * through the Model Context Protocol */ class McpDebuggerServer { constructor() { this.server = new mcp_js_1.McpServer({ name: "debugger-server", version: "1.1.6", }, { capabilities: { tools: {}, }, }); this.sessionManager = new mcp_debugger_core_1.SessionManager(); this.hangDetector = new mcp_debugger_core_1.HangDetector(); this.shutdownHandler = new mcp_debugger_core_1.GracefulShutdownHandler(30000); this.registerTools(); this.setupShutdownHandlers(); } /** * Setup graceful shutdown handlers */ setupShutdownHandlers() { // Register cleanup for session manager this.shutdownHandler.registerCleanup("sessions", async () => { console.log("Cleaning up all debug sessions..."); await this.sessionManager.cleanupAll(); }); // Register cleanup for MCP server this.shutdownHandler.registerCleanup("mcp-server", async () => { console.log("Closing MCP server..."); await this.server.close(); }); // Initialize signal handlers this.shutdownHandler.initialize(); } /** * Register all MCP tools */ registerTools() { this.registerDebuggerStart(); this.registerDebuggerSetBreakpoint(); this.registerDebuggerContinue(); this.registerDebuggerStepOver(); this.registerDebuggerStepInto(); this.registerDebuggerStepOut(); this.registerDebuggerPause(); this.registerDebuggerRemoveBreakpoint(); this.registerDebuggerToggleBreakpoint(); this.registerDebuggerListBreakpoints(); this.registerDebuggerGetLocalVariables(); this.registerDebuggerGetGlobalVariables(); this.registerDebuggerInspectObject(); this.registerDebuggerAddWatch(); this.registerDebuggerRemoveWatch(); this.registerDebuggerGetWatches(); this.registerDebuggerSwitchStackFrame(); this.registerDebuggerStopSession(); this.registerDebuggerInspect(); this.registerDebuggerGetStack(); this.registerDebuggerDetectHang(); // Advanced breakpoint types this.registerDebuggerSetLogpoint(); this.registerDebuggerSetExceptionBreakpoint(); this.registerDebuggerSetFunctionBreakpoint(); this.registerDebuggerSetHitCountCondition(); // Performance profiling tools this.registerDebuggerStartCPUProfile(); this.registerDebuggerStopCPUProfile(); this.registerDebuggerTakeHeapSnapshot(); this.registerDebuggerGetPerformanceMetrics(); } /** * Tool: debugger_start * Start a new debug session * Requirements: 2.1, 9.1 */ registerDebuggerStart() { this.server.registerTool("debugger_start", { description: "Start a new debug session with a Node.js process. The process will be paused at the start.", inputSchema: { command: zod_1.z .string() .describe('The command to execute (e.g., "node", "npm")'), args: zod_1.z .array(zod_1.z.string()) .optional() .describe('Command arguments (e.g., ["test.js"])'), cwd: zod_1.z .string() .optional() .describe("Working directory for the process"), timeout: zod_1.z .number() .optional() .describe("Timeout in milliseconds (default: 30000)"), }, }, async (args) => { try { const config = { command: args.command, args: args.args, cwd: args.cwd, timeout: args.timeout || 30000, }; const session = await this.sessionManager.createSession(config); return { content: [ { type: "text", text: JSON.stringify({ status: "success", sessionId: session.id, state: session.getState(), pid: session.getProcess()?.pid, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_START_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_set_breakpoint * Set a breakpoint in the debug session * Requirements: 1.1, 1.2, 9.1 */ registerDebuggerSetBreakpoint() { this.server.registerTool("debugger_set_breakpoint", { description: "Set a breakpoint at a specific file and line number. Optionally provide a condition.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), file: zod_1.z.string().describe("The file path (absolute or relative)"), line: zod_1.z.number().describe("The line number (1-indexed)"), condition: zod_1.z .string() .optional() .describe('Optional condition expression (e.g., "x > 10")'), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } const breakpoint = await session.setBreakpoint(args.file, args.line, args.condition); return { content: [ { type: "text", text: JSON.stringify({ status: "success", breakpointId: breakpoint.id, file: breakpoint.file, line: breakpoint.line, condition: breakpoint.condition, enabled: breakpoint.enabled, verified: !!breakpoint.cdpBreakpointId, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "BREAKPOINT_SET_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_continue * Resume execution of a paused debug session * Requirements: 2.2, 9.1 */ registerDebuggerContinue() { this.server.registerTool("debugger_continue", { description: "Resume execution of a paused debug session until the next breakpoint or program termination.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } await session.resume(); return { content: [ { type: "text", text: JSON.stringify({ status: "success", state: session.getState(), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "CONTINUE_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_step_over * Step over the current line * Requirements: 2.3, 9.1 */ registerDebuggerStepOver() { this.server.registerTool("debugger_step_over", { description: "Execute the current line and pause at the next line in the same scope.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } await session.stepOver(); // Wait a bit for the paused event to populate call frames await new Promise((resolve) => setTimeout(resolve, 100)); const stack = await session.getCallStack(); const location = stack.length > 0 ? { file: stack[0].file, line: stack[0].line } : null; return { content: [ { type: "text", text: JSON.stringify({ status: "success", state: session.getState(), location, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "STEP_OVER_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_inspect * Evaluate an expression in the current execution context * Requirements: 3.4, 9.1, 9.3 */ registerDebuggerInspect() { this.server.registerTool("debugger_inspect", { description: "Evaluate a JavaScript expression in the current execution context and return the result with type information.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), expression: zod_1.z .string() .describe('The JavaScript expression to evaluate (e.g., "x + 1")'), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } const result = await session.evaluateExpression(args.expression); return { content: [ { type: "text", text: JSON.stringify({ status: "success", expression: args.expression, value: result.value, type: result.type, objectId: result.objectId, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "INSPECT_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_get_stack * Get the current call stack * Requirements: 4.1, 9.1, 9.4 */ registerDebuggerGetStack() { this.server.registerTool("debugger_get_stack", { description: "Get the current call stack with function names, file locations (absolute paths), and line numbers.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } const stack = await session.getCallStack(); return { content: [ { type: "text", text: JSON.stringify({ status: "success", stack: stack.map((frame) => ({ function: frame.functionName, file: frame.file, line: frame.line, column: frame.column, })), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "GET_STACK_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_detect_hang * Detect if a process hangs or enters an infinite loop * Requirements: 5.1, 5.2, 5.3, 5.4, 9.1 */ registerDebuggerDetectHang() { this.server.registerTool("debugger_detect_hang", { description: "Run a command and detect if it hangs or enters an infinite loop. Returns hang status, location, and stack trace if hung.", inputSchema: { command: zod_1.z .string() .describe('The command to execute (e.g., "node", "npm")'), args: zod_1.z .array(zod_1.z.string()) .optional() .describe('Command arguments (e.g., ["test.js"])'), cwd: zod_1.z .string() .optional() .describe("Working directory for the process"), timeout: zod_1.z.number().describe("Timeout in milliseconds (e.g., 5000)"), sampleInterval: zod_1.z .number() .optional() .describe("Sample interval in milliseconds for infinite loop detection (e.g., 100)"), }, }, async (args) => { try { console.log("[MCP Server] debugger_detect_hang called with:", args); const result = await this.hangDetector.detectHang({ command: args.command, args: args.args, cwd: args.cwd, timeout: args.timeout, sampleInterval: args.sampleInterval, }); console.log("[MCP Server] detectHang completed:", result); if (result.hung) { return { content: [ { type: "text", text: JSON.stringify({ status: "success", hung: true, location: result.location, stack: result.stack, message: result.message, duration: result.duration, }, null, 2), }, ], }; } else { return { content: [ { type: "text", text: JSON.stringify({ status: "success", hung: false, completed: result.completed, exitCode: result.exitCode, duration: result.duration, }, null, 2), }, ], }; } } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "HANG_DETECTION_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_step_into * Step into the current line * Requirements: 2.4, 9.1 */ registerDebuggerStepInto() { this.server.registerTool("debugger_step_into", { description: "Execute the current line and pause at the first line inside any called function.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } await session.stepInto(); // Wait a bit for the paused event to populate call frames await new Promise((resolve) => setTimeout(resolve, 100)); const stack = await session.getCallStack(); const location = stack.length > 0 ? { file: stack[0].file, line: stack[0].line } : null; return { content: [ { type: "text", text: JSON.stringify({ status: "success", state: session.getState(), location, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "STEP_INTO_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_step_out * Step out of the current function * Requirements: 2.5, 9.1 */ registerDebuggerStepOut() { this.server.registerTool("debugger_step_out", { description: "Execute until the current function returns and pause at the calling location.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } await session.stepOut(); // Wait a bit for the paused event to populate call frames await new Promise((resolve) => setTimeout(resolve, 100)); const stack = await session.getCallStack(); const location = stack.length > 0 ? { file: stack[0].file, line: stack[0].line } : null; return { content: [ { type: "text", text: JSON.stringify({ status: "success", state: session.getState(), location, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "STEP_OUT_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_pause * Pause running execution * Requirements: 2.6, 9.1 */ registerDebuggerPause() { this.server.registerTool("debugger_pause", { description: "Pause a running debug session and return the current execution location.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } await session.pause(); const stack = await session.getCallStack(); const location = stack.length > 0 ? { file: stack[0].file, line: stack[0].line } : null; return { content: [ { type: "text", text: JSON.stringify({ status: "success", state: session.getState(), location, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "PAUSE_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_remove_breakpoint * Remove a breakpoint from the session * Requirements: 1.4, 9.1 */ registerDebuggerRemoveBreakpoint() { this.server.registerTool("debugger_remove_breakpoint", { description: "Remove a breakpoint from the debug session.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), breakpointId: zod_1.z.string().describe("The breakpoint ID to remove"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } const removed = await session.removeBreakpoint(args.breakpointId); if (!removed) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "BREAKPOINT_NOT_FOUND", message: `Breakpoint ${args.breakpointId} not found`, }, null, 2), }, ], isError: true, }; } return { content: [ { type: "text", text: JSON.stringify({ status: "success", breakpointId: args.breakpointId, removed: true, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "REMOVE_BREAKPOINT_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_toggle_breakpoint * Toggle a breakpoint's enabled state * Requirements: 1.5, 9.1 */ registerDebuggerToggleBreakpoint() { this.server.registerTool("debugger_toggle_breakpoint", { description: "Toggle a breakpoint between enabled and disabled states.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), breakpointId: zod_1.z.string().describe("The breakpoint ID to toggle"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } const breakpoint = await session.toggleBreakpoint(args.breakpointId); if (!breakpoint) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "BREAKPOINT_NOT_FOUND", message: `Breakpoint ${args.breakpointId} not found`, }, null, 2), }, ], isError: true, }; } return { content: [ { type: "text", text: JSON.stringify({ status: "success", breakpointId: breakpoint.id, file: breakpoint.file, line: breakpoint.line, condition: breakpoint.condition, enabled: breakpoint.enabled, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "TOGGLE_BREAKPOINT_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_list_breakpoints * List all breakpoints in the session * Requirements: 1.3, 9.1 */ registerDebuggerListBreakpoints() { this.server.registerTool("debugger_list_breakpoints", { description: "Get all breakpoints for a debug session with their file, line, condition, and enabled state.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } const breakpoints = session.getAllBreakpoints(); return { content: [ { type: "text", text: JSON.stringify({ status: "success", breakpoints: breakpoints.map((bp) => ({ id: bp.id, file: bp.file, line: bp.line, condition: bp.condition, enabled: bp.enabled, verified: !!bp.cdpBreakpointId, })), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "LIST_BREAKPOINTS_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_get_local_variables * Get all local variables in the current scope * Requirements: 3.1, 9.1, 9.3 */ registerDebuggerGetLocalVariables() { this.server.registerTool("debugger_get_local_variables", { description: "Get all local variables in the current scope with their names, values, and types.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } if (!session.isPaused()) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "NOT_PAUSED", message: "Process must be paused to get local variables", }, null, 2), }, ], isError: true, }; } const callFrames = session.getCurrentCallFrames(); if (!callFrames || callFrames.length === 0) { return { content: [ { type: "text", text: JSON.stringify({ status: "success", variables: [], }, null, 2), }, ], }; } const currentFrame = callFrames[session.getCurrentFrameIndex()]; const scopeChain = currentFrame.scopeChain || []; // Get local scope (first scope in chain is usually local) const localScope = scopeChain.find((scope) => scope.type === "local"); if (!localScope || !localScope.object?.objectId) { return { content: [ { type: "text", text: JSON.stringify({ status: "success", variables: [], }, null, 2), }, ], }; } const properties = await session.getObjectProperties(localScope.object.objectId); return { content: [ { type: "text", text: JSON.stringify({ status: "success", variables: properties.map((prop) => ({ name: prop.name, value: prop.value, type: prop.type, })), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "GET_LOCAL_VARIABLES_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_get_global_variables * Get global variables accessible from the current scope * Requirements: 3.2, 9.1, 9.3 */ registerDebuggerGetGlobalVariables() { this.server.registerTool("debugger_get_global_variables", { description: "Get global variables accessible from the current scope with their names, values, and types.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } if (!session.isPaused()) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "NOT_PAUSED", message: "Process must be paused to get global variables", }, null, 2), }, ], isError: true, }; } const callFrames = session.getCurrentCallFrames(); if (!callFrames || callFrames.length === 0) { return { content: [ { type: "text", text: JSON.stringify({ status: "success", variables: [], }, null, 2), }, ], }; } const currentFrame = callFrames[session.getCurrentFrameIndex()]; const scopeChain = currentFrame.scopeChain || []; // Get global scope (last scope in chain is usually global) const globalScope = scopeChain.find((scope) => scope.type === "global"); if (!globalScope || !globalScope.object?.objectId) { return { content: [ { type: "text", text: JSON.stringify({ status: "success", variables: [], }, null, 2), }, ], }; } const properties = await session.getObjectProperties(globalScope.object.objectId); // Filter out built-in globals to reduce noise const userGlobals = properties.filter((prop) => !["console", "process", "Buffer", "global", "require"].includes(prop.name)); return { content: [ { type: "text", text: JSON.stringify({ status: "success", variables: userGlobals.map((prop) => ({ name: prop.name, value: prop.value, type: prop.type, })), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "GET_GLOBAL_VARIABLES_FAILED", message: error instanceof Error ? error.message : String(error), }, null, 2), }, ], isError: true, }; } }); } /** * Tool: debugger_inspect_object * Inspect an object's properties with nested resolution * Requirements: 3.3, 9.1, 9.3 */ registerDebuggerInspectObject() { this.server.registerTool("debugger_inspect_object", { description: "Inspect an object by its object reference, returning properties with values. Handles nested objects and arrays up to a specified depth.", inputSchema: { sessionId: zod_1.z.string().describe("The debug session ID"), objectId: zod_1.z .string() .describe("The object ID (from a previous inspection or evaluation)"), maxDepth: zod_1.z .number() .optional() .describe("Maximum depth to traverse (default: 2)"), }, }, async (args) => { try { const session = this.sessionManager.getSession(args.sessionId); if (!session) { return { content: [ { type: "text", text: JSON.stringify({ status: "error", code: "SESSION_NOT_FOUND", message: `Session ${args.sessionId} not found`, }, null, 2), }, ], isError: true, }; } if (!session.isPaused()) { return { content: [ { type: "text", text: JSON.stringify({