nodejs_inspect
Execute JavaScript code directly in a debugged Node.js process to inspect variables, test expressions, and analyze runtime behavior during debugging sessions.
Instructions
Executes JavaScript code in the debugged process
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| js_code | Yes | JavaScript code to execute |
Implementation Reference
- src/mcp-server.js:332-472 (registration)Registration of the 'nodejs_inspect' tool using server.tool(), including description, input schema, and inline handler function.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(); } // 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) { 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 }); // 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'); // 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}]`; } } } 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 = []; // 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)}` }); return { content: responseContent }; } catch (err) { return { content: [{ type: "text", text: `Error executing code: ${err.message}` }] }; } } );
- src/mcp-server.js:338-471 (handler)The handler function executes the provided JS code in the Node.js debugged process using the inspector, handles console output, serializes complex objects by fetching properties, and returns structured content.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; // Wrap the code in a try-catch with explicit console logging for errors let codeToExecute = ` try { ${js_code} } catch (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 }); // 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'); // 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}]`; } } } 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 = []; // 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)}` }); return { content: responseContent }; } catch (err) { return { content: [{ type: "text", text: `Error executing code: ${err.message}` }] }; } }
- src/mcp-server.js:335-337 (schema)Input schema for the tool, defining 'js_code' as a required string parameter.{ js_code: z.string().describe("JavaScript code to execute") },
- src/mcp-server.js:36-319 (helper)The Inspector class is the core helper that manages connection to Node.js inspector protocol, sending CDP commands, handling events like paused/resumed/console, and providing utilities used by the nodejs_inspect handler.class Inspector { constructor(port = 9229, retryOptions = { maxRetries: 5, retryInterval: 1000, continuousRetry: true }) { this.port = port; this.connected = false; this.pendingRequests = new Map(); this.debuggerEnabled = false; this.breakpoints = new Map(); this.paused = false; this.currentCallFrames = []; this.retryOptions = retryOptions; this.retryCount = 0; this.callbackHandlers = new Map(); this.continuousRetryEnabled = retryOptions.continuousRetry; this.initialize(); } async initialize() { try { // First, get the WebSocket URL from the inspector JSON API // Use 127.0.0.1 instead of localhost to avoid IPv6 issues const response = await fetch(`http://127.0.0.1:${this.port}/json`); const data = await response.json(); const debuggerUrl = data[0]?.webSocketDebuggerUrl; if (!debuggerUrl) { this.scheduleRetry(); return; } this.ws = new WebSocket(debuggerUrl); this.ws.on('open', () => { this.connected = true; this.retryCount = 0; this.enableDebugger(); }); this.ws.on('error', (error) => { this.scheduleRetry(); this.debuggerEnabled = false; }); this.ws.on('close', () => { this.connected = false; this.scheduleRetry(); this.debuggerEnabled = false; }); this.ws.on('message', (data) => { const response = JSON.parse(data.toString()); // Handle events if (response.method) { this.handleEvent(response); return; } // Handle response for pending request if (response.id && this.pendingRequests.has(response.id)) { const { resolve, reject } = this.pendingRequests.get(response.id); this.pendingRequests.delete(response.id); if (response.error) { reject(response.error); } else { resolve(response.result); } } }); } catch (error) { this.scheduleRetry(); } } scheduleRetry() { // If continuous retry is enabled, we'll keep trying after the initial attempts if (this.retryCount < this.retryOptions.maxRetries || this.continuousRetryEnabled) { this.retryCount++; // Use a longer interval for continuous retries to reduce resource usage const interval = this.continuousRetryEnabled && this.retryCount > this.retryOptions.maxRetries ? Math.min(this.retryOptions.retryInterval * 5, 10000) // Max 10 seconds between retries : this.retryOptions.retryInterval; setTimeout(() => this.initialize(), interval); } } async enableDebugger() { try { if (!this.debuggerEnabled && this.connected) { await this.send('Debugger.enable', {}); this.debuggerEnabled = true; // Setup event listeners await this.send('Runtime.enable', {}); // Also activate possible domains we'll need await this.send('Runtime.runIfWaitingForDebugger', {}); } } catch (error) { this.scheduleRetry(); } } handleEvent(event) { switch (event.method) { case 'Debugger.paused': this.paused = true; this.currentCallFrames = event.params.callFrames; // Notify any registered callbacks for pause events if (this.callbackHandlers.has('paused')) { this.callbackHandlers.get('paused').forEach(callback => callback(event.params)); } break; case 'Debugger.resumed': this.paused = false; this.currentCallFrames = []; // Notify any registered callbacks for resume events if (this.callbackHandlers.has('resumed')) { this.callbackHandlers.get('resumed').forEach(callback => callback()); } break; case 'Debugger.scriptParsed': // Script parsing might be useful for source maps break; case 'Runtime.exceptionThrown': break; case 'Runtime.consoleAPICalled': // Handle console logs from the debugged program const args = event.params.args.map(arg => { if (arg.type === 'string') return arg.value; if (arg.type === 'number') return arg.value; if (arg.type === 'boolean') return arg.value; if (arg.type === 'object') { if (arg.value) { return JSON.stringify(arg.value, null, 2); } else if (arg.objectId) { // We'll try to get properties later as we can't do async here return arg.description || `[${arg.subtype || arg.type}]`; } else { return arg.description || `[${arg.subtype || arg.type}]`; } } return JSON.stringify(arg); }).join(' '); // Store console logs to make them available to the MCP tools if (!this.consoleOutput) { this.consoleOutput = []; } this.consoleOutput.push({ type: event.params.type, message: args, timestamp: Date.now(), raw: event.params.args }); // Keep only the last 100 console messages to avoid memory issues if (this.consoleOutput.length > 100) { this.consoleOutput.shift(); } break; } } registerCallback(event, callback) { if (!this.callbackHandlers.has(event)) { this.callbackHandlers.set(event, []); } this.callbackHandlers.get(event).push(callback); } unregisterCallback(event, callback) { if (this.callbackHandlers.has(event)) { const callbacks = this.callbackHandlers.get(event); const index = callbacks.indexOf(callback); if (index !== -1) { callbacks.splice(index, 1); } } } async send(method, params) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`Request timed out: ${method}`)); this.pendingRequests.delete(id); }, 5000); const checkConnection = () => { if (this.connected) { try { const id = Math.floor(Math.random() * 1000000); this.pendingRequests.set(id, { resolve: (result) => { clearTimeout(timeout); resolve(result); }, reject: (err) => { clearTimeout(timeout); reject(err); } }); this.ws.send(JSON.stringify({ id, method, params })); } catch (err) { clearTimeout(timeout); reject(err); } } else { const connectionCheckTimer = setTimeout(checkConnection, 100); // If still not connected after 3 seconds, reject the promise setTimeout(() => { clearTimeout(connectionCheckTimer); clearTimeout(timeout); reject(new Error('Not connected to debugger')); }, 3000); } }; checkConnection(); }); } async getScriptSource(scriptId) { try { const response = await this.send('Debugger.getScriptSource', { scriptId }); return response.scriptSource; } catch (err) { return null; } } async evaluateOnCallFrame(callFrameId, expression) { if (!this.paused) { throw new Error('Debugger is not paused'); } try { return await this.send('Debugger.evaluateOnCallFrame', { callFrameId, expression, objectGroup: 'console', includeCommandLineAPI: true, silent: false, returnByValue: true, generatePreview: true }); } catch (err) { throw err; } } async getProperties(objectId, ownProperties = true) { try { return await this.send('Runtime.getProperties', { objectId, ownProperties, accessorPropertiesOnly: false, generatePreview: true }); } catch (err) { throw err; } } }