diff --git a/package-lock.json b/package-lock.json index 6dddd90..3a8d966 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "mcp-nodejs-debugger", - "version": "0.1.0-beta.5", + "name": "@hyperdrive-eng/mcp-nodejs-debugger", + "version": "0.1.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "mcp-nodejs-debugger", - "version": "0.1.0-beta.5", + "name": "@hyperdrive-eng/mcp-nodejs-debugger", + "version": "0.1.0-beta.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index fb8084f..3a30681 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "mcp-nodejs-debugger", - "version": "0.1.0-beta.5", + "name": "@hyperdrive-eng/mcp-nodejs-debugger", + "version": "0.1.0-beta.3", "description": "An MCP server to debug Node.js at runtime", "main": "index.js", "bin": "./index.js", @@ -37,10 +37,10 @@ }, "repository": { "type": "git", - "url": "https://github.com/hyperdrive-eng/mcp-debugger" + "url": "https://github.com/hyperdrive-eng/mcp-nodejs-debugger" }, "bugs": { - "url": "https://github.com/hyperdrive-eng/mcp-debugger/issues" + "url": "https://github.com/hyperdrive-eng/mcp-nodejs-debugger/issues" }, "homepage": "https://hyperdrive.engineering" } diff --git a/src/mcp-server.js b/src/mcp-server.js index d9ac949..0801837 100644 --- a/src/mcp-server.js +++ b/src/mcp-server.js @@ -8,25 +8,25 @@ import fetch from 'node-fetch'; const server = new McpServer({ name: "Inspector", version: "1.0.0", - description: `Node.js debugger that allows setting breakpoints and inspecting variables. - When using debugging capabilities, always instruct the user to navigate to their - website after setting breakpoints. Breakpoints will only be triggered when the user - interacts with their website in a way that causes the code at the breakpoint location to execute. - When user requests debugging, always use the following format: - 1. Set breakpoint - 2. Wait for user to navigate to their website and trigger breakpoint - 3. Inspect variables - 4. Execute code - 5. Step over - 6. Step into - 7. Step out - 8. Continue execution - 9. Delete breakpoint - 10. List breakpoints - 11. Get current location - 12. Evaluate expression - 13. Get script source - 14. Get properties` + description: `Advanced Node.js debugger for runtime analysis and troubleshooting. This tool connects to Node.js's built-in Inspector Protocol to provide powerful debugging capabilities directly through Claude Code. + + DEBUGGING STRATEGY: + Use debugger for the following: + - When you need to understand the runtime state of the application + - When you need to test potential fixes for the application + - When you need to explore the codebase to find the root cause of an issue + + + IMPORTANT NOTES: + - ALWAYS assume the user has already started their Node.js application in debug mode. + - If connection issues occur, suggest using retry_connect tool instead of restarting the server + - Don't try to start the debugger or the node server yourself. + - Always ask the user to trigger breakpoints manually, give them specific instructions on how to do so. + - When user interaction is required, provide EXTREMELY specific instructions + - Take initiative to explore the runtime state thoroughly when breakpoint is hit + - Keep breakpoints active until issue is fully resolved, then clean up using delete_breakpoint + - Set multiple strategic breakpoints at once to capture the full execution path leading to an error. + - NEVER use fetch() as it will break the debugging connection.` }); class Inspector { @@ -351,156 +351,156 @@ const inspector = new Inspector(9229, { inspector.consoleOutput = []; // Execute JavaScript code -server.tool( - "nodejs_inspect", - "Executes JavaScript code in the debugged process", - { - js_code: z.string().describe("JavaScript code to execute") - }, - async ({ js_code }) => { - try { - // Ensure debugger is enabled - if (!inspector.debuggerEnabled) { - await inspector.enableDebugger(); - } +// server.tool( +// "nodejs_inspect", +// "Executes custom JavaScript code directly in the context of the running Node.js application. CRITICAL for proactive debugging: use this to trigger code paths that would hit breakpoints, simulate user actions, inject test data, modify runtime behavior, or extract detailed state information. WARNING: DO NOT use fetch() or similar browser APIs as they will interrupt the WebSocket debugging connection! Instead, use the native http module with callbacks (http.get/http.request) when making HTTP requests programmatically.", +// { +// js_code: z.string().describe("JavaScript code to execute in the context of the running application. Can include any valid JS including async/await, multiple statements, object creation, and function calls. For HTTP requests, use http.get() with callbacks instead of fetch().") +// }, +// async ({ js_code }) => { +// try { +// // Ensure debugger is enabled +// if (!inspector.debuggerEnabled) { +// await inspector.enableDebugger(); +// } - // Capture the current console output length to know where to start capturing new output - const consoleStartIndex = inspector.consoleOutput.length; +// // Capture the current console output length to know where to start capturing new output +// const consoleStartIndex = inspector.consoleOutput.length; - // Wrap the code in a try-catch with explicit console logging for errors - let codeToExecute = ` - try { - ${js_code} - } catch (e) { - console.error('Execution error:', e); - e; // Return the error - } - `; +// // Wrap the code in a try-catch with explicit console logging for errors +// let codeToExecute = ` +// try { +// ${js_code} +// } catch (e) { +// console.error('Execution error:', e); +// e; // Return the error +// } +// `; - const response = await inspector.send('Runtime.evaluate', { - expression: codeToExecute, - contextId: 1, - objectGroup: 'console', - includeCommandLineAPI: true, - silent: false, - returnByValue: true, - generatePreview: true, - awaitPromise: true // This will wait for promises to resolve - }); +// const response = await inspector.send('Runtime.evaluate', { +// expression: codeToExecute, +// contextId: 1, +// objectGroup: 'console', +// includeCommandLineAPI: true, +// silent: false, +// returnByValue: true, +// generatePreview: true, +// awaitPromise: true // This will wait for promises to resolve +// }); - // Give some time for console logs to be processed - await new Promise(resolve => setTimeout(resolve, 200)); +// // Give some time for console logs to be processed +// await new Promise(resolve => setTimeout(resolve, 200)); - // Get any console output that was generated during execution - const consoleOutputs = inspector.consoleOutput.slice(consoleStartIndex); - const consoleText = consoleOutputs.map(output => - `[${output.type}] ${output.message}` - ).join('\n'); +// // Get any console output that was generated during execution +// const consoleOutputs = inspector.consoleOutput.slice(consoleStartIndex); +// const consoleText = consoleOutputs.map(output => +// `[${output.type}] ${output.message}` +// ).join('\n'); - // Process the return value - let result; - if (response.result) { - if (response.result.type === 'object') { - if (response.result.value) { - // If we have a value, use it - result = response.result.value; - } else if (response.result.objectId) { - // If we have an objectId but no value, the object was too complex to serialize directly - // Get more details about the object - try { - const objectProps = await inspector.getProperties(response.result.objectId); - const formattedObject = {}; +// // Process the return value +// let result; +// if (response.result) { +// if (response.result.type === 'object') { +// if (response.result.value) { +// // If we have a value, use it +// result = response.result.value; +// } else if (response.result.objectId) { +// // If we have an objectId but no value, the object was too complex to serialize directly +// // Get more details about the object +// try { +// const objectProps = await inspector.getProperties(response.result.objectId); +// const formattedObject = {}; - for (const prop of objectProps.result) { - if (prop.value) { - if (prop.value.type === 'object' && prop.value.subtype !== 'null') { - // For nested objects, try to get their details too - if (prop.value.objectId) { - try { - const nestedProps = await inspector.getProperties(prop.value.objectId); - const nestedObj = {}; - for (const nestedProp of nestedProps.result) { - if (nestedProp.value) { - if (nestedProp.value.value !== undefined) { - nestedObj[nestedProp.name] = nestedProp.value.value; - } else { - nestedObj[nestedProp.name] = nestedProp.value.description || - `[${nestedProp.value.subtype || nestedProp.value.type}]`; - } - } - } - formattedObject[prop.name] = nestedObj; - } catch (nestedErr) { - formattedObject[prop.name] = prop.value.description || - `[${prop.value.subtype || prop.value.type}]`; - } - } else { - formattedObject[prop.name] = prop.value.description || - `[${prop.value.subtype || prop.value.type}]`; - } - } else if (prop.value.type === 'function') { - formattedObject[prop.name] = '[function]'; - } else if (prop.value.value !== undefined) { - formattedObject[prop.name] = prop.value.value; - } else { - formattedObject[prop.name] = `[${prop.value.type}]`; - } - } - } +// for (const prop of objectProps.result) { +// if (prop.value) { +// if (prop.value.type === 'object' && prop.value.subtype !== 'null') { +// // For nested objects, try to get their details too +// if (prop.value.objectId) { +// try { +// const nestedProps = await inspector.getProperties(prop.value.objectId); +// const nestedObj = {}; +// for (const nestedProp of nestedProps.result) { +// if (nestedProp.value) { +// if (nestedProp.value.value !== undefined) { +// nestedObj[nestedProp.name] = nestedProp.value.value; +// } else { +// nestedObj[nestedProp.name] = nestedProp.value.description || +// `[${nestedProp.value.subtype || nestedProp.value.type}]`; +// } +// } +// } +// formattedObject[prop.name] = nestedObj; +// } catch (nestedErr) { +// formattedObject[prop.name] = prop.value.description || +// `[${prop.value.subtype || prop.value.type}]`; +// } +// } else { +// formattedObject[prop.name] = prop.value.description || +// `[${prop.value.subtype || prop.value.type}]`; +// } +// } else if (prop.value.type === 'function') { +// formattedObject[prop.name] = '[function]'; +// } else if (prop.value.value !== undefined) { +// formattedObject[prop.name] = prop.value.value; +// } else { +// formattedObject[prop.name] = `[${prop.value.type}]`; +// } +// } +// } - result = formattedObject; - } catch (propErr) { - // If we can't get properties, at least show the object description - result = response.result.description || `[${response.result.subtype || response.result.type}]`; - } - } else { - // Fallback for objects without value or objectId - result = response.result.description || `[${response.result.subtype || response.result.type}]`; - } - } else if (response.result.type === 'undefined') { - result = undefined; - } else if (response.result.value !== undefined) { - result = response.result.value; - } else { - result = `[${response.result.type}]`; - } - } +// result = formattedObject; +// } catch (propErr) { +// // If we can't get properties, at least show the object description +// result = response.result.description || `[${response.result.subtype || response.result.type}]`; +// } +// } else { +// // Fallback for objects without value or objectId +// result = response.result.description || `[${response.result.subtype || response.result.type}]`; +// } +// } else if (response.result.type === 'undefined') { +// result = undefined; +// } else if (response.result.value !== undefined) { +// result = response.result.value; +// } else { +// result = `[${response.result.type}]`; +// } +// } - let responseContent = []; +// let responseContent = []; - // Add console output if there was any - if (consoleText.length > 0) { - responseContent.push({ - type: "text", - text: `Console output:\n${consoleText}` - }); - } +// // Add console output if there was any +// if (consoleText.length > 0) { +// responseContent.push({ +// type: "text", +// text: `Console output:\n${consoleText}` +// }); +// } - // Add the result - responseContent.push({ - type: "text", - text: `Code executed successfully. Result: ${JSON.stringify(result, null, 2)}` - }); +// // Add the result +// responseContent.push({ +// type: "text", +// text: `Code executed successfully. Result: ${JSON.stringify(result, null, 2)}` +// }); - return { content: responseContent }; - } catch (err) { - return { - content: [{ - type: "text", - text: `Error executing code: ${err.message}` - }] - }; - } - } -); +// return { content: responseContent }; +// } catch (err) { +// return { +// content: [{ +// type: "text", +// text: `Error executing code: ${err.message}` +// }] +// }; +// } +// } +// ); // Set breakpoint tool server.tool( "set_breakpoint", - "Sets a breakpoint at specified line and file", + "Places a debugging breakpoint at a specific line in your code to pause execution and inspect state. Code execution will stop exactly at this line when triggered, allowing you to examine variables, call stack, and program state. Always set breakpoints BEFORE the problematic code areas to capture the state leading to issues.", { - file: z.string().describe("File path where to set breakpoint"), - line: z.number().describe("Line number for breakpoint") + file: z.string().describe("Full absolute file path where to set breakpoint (e.g., '/path/to/your/app.js')"), + line: z.number().describe("Line number for breakpoint (1-based, as shown in code editors)") }, async ({ file, line }) => { try { @@ -545,9 +545,9 @@ server.tool( // Inspect variables tool server.tool( "inspect_variables", - "Inspects variables in current scope", + "Retrieves and displays all variables and their values in the current execution context when paused at a breakpoint. This is ESSENTIAL after hitting a breakpoint to understand the program state, identify unexpected values, and diagnose issues. Use this immediately when execution pauses to get a complete snapshot of the runtime environment.", { - scope: z.string().optional().describe("Scope to inspect (local/global)") + scope: z.string().optional().describe("Scope to inspect: 'local' for variables in current function/block (default), 'global' for application-wide variables") }, async ({ scope = 'local' }) => { try { @@ -639,7 +639,7 @@ server.tool( // Step over tool server.tool( "step_over", - "Steps over to the next line of code", + "Advances code execution to the next line within the same function scope, executing any function calls as single operations without stepping into them. Use this to progress through code line-by-line while treating function calls as 'black boxes'. When stepping through complex code, this helps you focus on the current function's logic without diving into implementation details of called functions.", {}, async () => { try { @@ -679,7 +679,7 @@ server.tool( // Step into tool server.tool( "step_into", - "Steps into function calls", + "Advances execution to the next line, but if that line contains a function call, steps into that function and continues debugging there. Essential for diving into function implementations, following the execution path into nested calls, and understanding how data flows between functions. Use this when you suspect an issue exists inside a function being called from the current line.", {}, async () => { try { @@ -719,7 +719,7 @@ server.tool( // Step out tool server.tool( "step_out", - "Steps out of current function", + "Executes the rest of the current function and pauses at the next line after the function returns to its caller. Perfect for when you've determined a function is working correctly and want to return to the calling context, or when you've stepped into a function accidentally and want to get back to the higher-level code without manually stepping through every remaining line.", {}, async () => { try { @@ -759,7 +759,7 @@ server.tool( // Continue execution tool server.tool( "continue", - "Continues code execution", + "Resumes normal code execution after being paused at a breakpoint, running at full speed until the next breakpoint is hit or the program terminates. Use this when you've examined the current state and want to let the program run naturally to the next point of interest. This is especially useful when you have multiple breakpoints set up along an execution path to analyze different stages of processing.", {}, async () => { try { @@ -799,9 +799,9 @@ server.tool( // Delete breakpoint tool server.tool( "delete_breakpoint", - "Deletes a specified breakpoint", + "Removes a specific breakpoint from the debugging session by its ID. Use this to clean up breakpoints that are no longer needed or to remove breakpoints that might be interfering with program flow. Always use list_breakpoints first to get the correct ID of the breakpoint you want to remove.", { - breakpointId: z.string().describe("ID of the breakpoint to remove") + breakpointId: z.string().describe("ID of the breakpoint to remove (get this ID from the list_breakpoints tool)") }, async ({ breakpointId }) => { try { @@ -837,7 +837,7 @@ server.tool( // List all breakpoints tool server.tool( "list_breakpoints", - "Lists all active breakpoints", + "Displays all currently active breakpoints with their IDs, file locations, and line numbers. Use this to keep track of all breakpoints set during a debugging session, verify that breakpoints are set in the intended locations, and obtain breakpoint IDs required for the delete_breakpoint tool. Good practice is to check this after setting new breakpoints to confirm they're registered correctly.", {}, async () => { try { @@ -877,9 +877,9 @@ server.tool( // Evaluate expression tool server.tool( "evaluate", - "Evaluates a JavaScript expression in the current context", + "Evaluates a JavaScript expression in the context of the paused execution point or global scope. Unlike nodejs_inspect which runs in the global context, this tool evaluates code within the specific context of the current breakpoint, giving access to local variables. Perfect for testing conditions, calculating values, or examining complex objects without modifying the application state.", { - expression: z.string().describe("JavaScript expression to evaluate") + expression: z.string().describe("JavaScript expression to evaluate - can reference local variables at the breakpoint, make function calls, access properties, or perform calculations") }, async ({ expression }) => { try { @@ -1033,7 +1033,7 @@ server.tool( // Get current location tool server.tool( "get_location", - "Gets the current execution location when paused", + "Provides detailed information about the current execution point when paused at a breakpoint, including file location, line number, call stack, and surrounding code. Use this immediately after a breakpoint is hit to understand exactly where the execution has paused and how it got there. The call stack is particularly valuable for tracing the sequence of function calls leading to the current point.", {}, async () => { try { @@ -1108,9 +1108,9 @@ server.tool( // Add a tool specifically for getting console output server.tool( "get_console_output", - "Gets the most recent console output from the debugged process", + "Retrieves recent console.log, console.error, and other console messages from the running application. This is invaluable for seeing runtime outputs, error messages, and debugging information without modifying the code. Check this output regularly during debugging sessions to catch messages that might explain the issue, especially before and after hitting breakpoints.", { - limit: z.number().optional().describe("Maximum number of console entries to return. Defaults to 20") + limit: z.number().optional().describe("Maximum number of console entries to return. Defaults to 20. Use larger values to see more historical output.") }, async ({ limit = 20 }) => { try { @@ -1150,9 +1150,9 @@ server.tool( // Add a tool for manually retrying connection to the Node.js debugger server.tool( "retry_connect", - "Manually triggers a reconnection attempt to the Node.js debugger", + "Forces the debugger to attempt reconnection to the Node.js application, which is useful when the target application has restarted or when connecting to a different debugging port. If the standard auto-reconnection isn't working, or you need to connect to a specific port different from the default 9229, use this tool to establish the connection manually. This is especially helpful when troubleshooting connection issues or working with multiple Node.js processes.", { - port: z.number().optional().describe("Optional port to connect to. Defaults to current port (9229)") + port: z.number().optional().describe("Optional port to connect to if not using the default 9229. Use this when your Node.js app is running with --inspect=XXXX where XXXX is a custom port") }, async ({ port }) => { try { @@ -1191,9 +1191,29 @@ server.tool( // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport(); -await server.connect(transport); -console.log("Inspector server ready..."); -console.log("MCP Debugger started. Connected to Node.js Inspector protocol."); -console.log("The server will continuously try to connect to any Node.js debugging session on port 9229."); -console.log("You can start a Node.js app with debugging enabled using: node --inspect yourapp.js"); \ No newline at end of file +try { + await server.connect(transport); + + console.log("Inspector server ready..."); + console.log("MCP Debugger started. Connected to Node.js Inspector protocol."); + console.log("The server will continuously try to connect to any Node.js debugging session on port 9229."); + console.log("You can start a Node.js app with debugging enabled using: node --inspect yourapp.js"); +} catch (error) { + console.error("MCP server connection error:", error); + + // Handle MCP connection closed error specifically + if (error.code === -32000 && error.message.includes("Connection closed")) { + // Send a helpful message back to the client + await transport.sendMessage({ + jsonrpc: "2.0", + id: null, + result: { + content: [{ + type: "text", + text: `MCP error -32000: Connection closed.\n\nThe connection to the MCP server has been closed. This typically happens when:\n\n1. The Claude Code session has timed out\n2. The client has disconnected\n\nPlease start a new Claude Code or MCP client session and try again.` + }] + } + }); + } +}