Skip to main content
Glama
workbackai

MCP NodeJS Debugger

by workbackai

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
NameRequiredDescriptionDefault
js_codeYesJavaScript code to execute

Implementation Reference

  • 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}`
            }]
          };
        }
      }
    );
  • 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}`
          }]
        };
      }
    }
  • Input schema for the tool, defining 'js_code' as a required string parameter.
    {
      js_code: z.string().describe("JavaScript code to execute")
    },
  • 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;
    		}
    	}
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/workbackai/mcp-nodejs-debugger'

If you have feedback or need assistance with the MCP directory API, please join our Discord server