MCP NodeJS Debugger
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import WebSocket from 'ws';
import fetch from 'node-fetch';
// Create an MCP server
const server = new McpServer({
name: "NodeJS Debugger",
version: "0.2.0",
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 {
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) {
console.error('No WebSocket debugger URL found');
this.scheduleRetry();
return;
}
console.log(`Connecting to debugger at: ${debuggerUrl}`);
this.ws = new WebSocket(debuggerUrl);
this.ws.on('open', () => {
console.log('WebSocket connection established');
this.connected = true;
this.retryCount = 0;
this.enableDebugger();
});
this.ws.on('error', (error) => {
console.error('WebSocket error:', error.message);
this.scheduleRetry();
});
this.ws.on('close', () => {
console.log('WebSocket connection closed');
this.connected = false;
this.scheduleRetry();
});
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) {
console.error('Error initializing inspector:', error.message);
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++;
// If we're in continuous retry mode and have exceeded the initial retry count
if (this.continuousRetryEnabled && this.retryCount > this.retryOptions.maxRetries) {
// Only log every 10 attempts to avoid flooding the console
if (this.retryCount % 10 === 0) {
console.log(`Waiting for debugger connection... (retry ${this.retryCount})`);
}
} else {
console.log(`Retrying connection (${this.retryCount}/${this.retryOptions.maxRetries})...`);
}
// 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);
} else {
console.error(`Failed to connect after ${this.retryOptions.maxRetries} attempts`);
}
}
async enableDebugger() {
if (!this.debuggerEnabled && this.connected) {
try {
await this.send('Debugger.enable', {});
console.log('Debugger enabled');
this.debuggerEnabled = true;
// Setup event listeners
await this.send('Runtime.enable', {});
// Also activate possible domains we'll need
await this.send('Runtime.runIfWaitingForDebugger', {});
} catch (err) {
console.error('Failed to enable debugger:', err);
}
}
}
handleEvent(event) {
// console.log('Event received:', event.method, event.params);
switch (event.method) {
case 'Debugger.paused':
this.paused = true;
this.currentCallFrames = event.params.callFrames;
console.log('Execution paused at breakpoint');
// 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 = [];
console.log('Execution resumed');
// 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':
console.log('Exception thrown:',
event.params.exceptionDetails.text,
event.params.exceptionDetails.exception?.description || '');
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();
}
console.log(`[Console.${event.params.type}]`, args);
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) {
console.error('Error getting script source:', 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) {
console.error('Error evaluating expression:', err);
throw err;
}
}
async getProperties(objectId, ownProperties = true) {
try {
return await this.send('Runtime.getProperties', {
objectId,
ownProperties,
accessorPropertiesOnly: false,
generatePreview: true
});
} catch (err) {
console.error('Error getting properties:', err);
throw err;
}
}
}
// Create the inspector instance with continuous retry enabled
const inspector = new Inspector(9229, {
maxRetries: 5,
retryInterval: 1000,
continuousRetry: true
});
// Initialize console output storage
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();
}
// 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
}
`;
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}`
}]
};
}
}
);
// Set breakpoint tool
server.tool(
"set_breakpoint",
"Sets a breakpoint at specified line and file",
{
file: z.string().describe("File path where to set breakpoint"),
line: z.number().describe("Line number for breakpoint")
},
async ({ file, line }) => {
try {
// Ensure debugger is enabled
if (!inspector.debuggerEnabled) {
await inspector.enableDebugger();
}
// Convert file path to a URL-like format that the debugger can understand
// For local files, typically file:///path/to/file.js
let fileUrl = file;
if (!file.startsWith('file://') && !file.startsWith('http://') && !file.startsWith('https://')) {
fileUrl = `file://${file.startsWith('/') ? '' : '/'}${file}`;
}
const response = await inspector.send('Debugger.setBreakpointByUrl', {
lineNumber: line - 1, // Chrome DevTools Protocol uses 0-based line numbers
urlRegex: fileUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), // Escape special regex characters
columnNumber: 0
});
// Store the breakpoint for future reference
inspector.breakpoints.set(response.breakpointId, { file, line, id: response.breakpointId });
return {
content: [{
type: "text",
text: `Breakpoint set successfully. ID: ${response.breakpointId}`
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error setting breakpoint: ${err.message}`
}]
};
}
}
);
// Inspect variables tool
server.tool(
"inspect_variables",
"Inspects variables in current scope",
{
scope: z.string().optional().describe("Scope to inspect (local/global)")
},
async ({ scope = 'local' }) => {
try {
// Ensure debugger is enabled
if (!inspector.debuggerEnabled) {
await inspector.enableDebugger();
}
if (scope === 'global' || !inspector.paused) {
// For global scope or when not paused, use Runtime.globalProperties
const response = await inspector.send('Runtime.globalLexicalScopeNames', {});
// Get global object properties for a more complete picture
const globalObjResponse = await inspector.send('Runtime.evaluate', {
expression: 'this',
contextId: 1,
returnByValue: true
});
return {
content: [{
type: "text",
text: JSON.stringify({
lexicalNames: response.names,
globalThis: globalObjResponse.result.value
}, null, 2)
}]
};
} else {
// For local scope when paused, get variables from the current call frame
if (inspector.currentCallFrames.length === 0) {
return {
content: [{
type: "text",
text: "No active call frames. Debugger is not paused at a breakpoint."
}]
};
}
const frame = inspector.currentCallFrames[0]; // Get top frame
const scopeChain = frame.scopeChain;
// Create a formatted output of variables in scope
const result = {};
for (const scopeObj of scopeChain) {
const { scope, type, name } = scopeObj;
if (type === 'global') continue; // Skip global scope for local inspection
const objProperties = await inspector.getProperties(scope.object.objectId);
const variables = {};
for (const prop of objProperties.result) {
if (prop.value && prop.configurable) {
if (prop.value.type === 'object' && prop.value.subtype !== 'null') {
variables[prop.name] = `[${prop.value.subtype || prop.value.type}]`;
} else if (prop.value.type === 'function') {
variables[prop.name] = '[function]';
} else if (prop.value.value !== undefined) {
variables[prop.name] = prop.value.value;
} else {
variables[prop.name] = `[${prop.value.type}]`;
}
}
}
result[type] = variables;
}
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
} catch (err) {
return {
content: [{
type: "text",
text: `Error inspecting variables: ${err.message}`
}]
};
}
}
);
// Step over tool
server.tool(
"step_over",
"Steps over to the next line of code",
{},
async () => {
try {
// Ensure debugger is enabled
if (!inspector.debuggerEnabled) {
await inspector.enableDebugger();
}
if (!inspector.paused) {
return {
content: [{
type: "text",
text: "Debugger is not paused at a breakpoint"
}]
};
}
await inspector.send('Debugger.stepOver', {});
return {
content: [{
type: "text",
text: "Stepped over to next line"
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error stepping over: ${err.message}`
}]
};
}
}
);
// Step into tool
server.tool(
"step_into",
"Steps into function calls",
{},
async () => {
try {
// Ensure debugger is enabled
if (!inspector.debuggerEnabled) {
await inspector.enableDebugger();
}
if (!inspector.paused) {
return {
content: [{
type: "text",
text: "Debugger is not paused at a breakpoint"
}]
};
}
await inspector.send('Debugger.stepInto', {});
return {
content: [{
type: "text",
text: "Stepped into function call"
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error stepping into: ${err.message}`
}]
};
}
}
);
// Step out tool
server.tool(
"step_out",
"Steps out of current function",
{},
async () => {
try {
// Ensure debugger is enabled
if (!inspector.debuggerEnabled) {
await inspector.enableDebugger();
}
if (!inspector.paused) {
return {
content: [{
type: "text",
text: "Debugger is not paused at a breakpoint"
}]
};
}
await inspector.send('Debugger.stepOut', {});
return {
content: [{
type: "text",
text: "Stepped out of current function"
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error stepping out: ${err.message}`
}]
};
}
}
);
// Continue execution tool
server.tool(
"continue",
"Continues code execution",
{},
async () => {
try {
// Ensure debugger is enabled
if (!inspector.debuggerEnabled) {
await inspector.enableDebugger();
}
if (!inspector.paused) {
return {
content: [{
type: "text",
text: "Debugger is not paused at a breakpoint"
}]
};
}
await inspector.send('Debugger.resume', {});
return {
content: [{
type: "text",
text: "Execution resumed"
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error continuing execution: ${err.message}`
}]
};
}
}
);
// Delete breakpoint tool
server.tool(
"delete_breakpoint",
"Deletes a specified breakpoint",
{
breakpointId: z.string().describe("ID of the breakpoint to remove")
},
async ({ breakpointId }) => {
try {
// Ensure debugger is enabled
if (!inspector.debuggerEnabled) {
await inspector.enableDebugger();
}
await inspector.send('Debugger.removeBreakpoint', {
breakpointId: breakpointId
});
// Remove from our local tracking
inspector.breakpoints.delete(breakpointId);
return {
content: [{
type: "text",
text: `Breakpoint ${breakpointId} removed`
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error removing breakpoint: ${err.message}`
}]
};
}
}
);
// List all breakpoints tool
server.tool(
"list_breakpoints",
"Lists all active breakpoints",
{},
async () => {
try {
// Ensure debugger is enabled
if (!inspector.debuggerEnabled) {
await inspector.enableDebugger();
}
if (inspector.breakpoints.size === 0) {
return {
content: [{
type: "text",
text: "No active breakpoints"
}]
};
}
const breakpointsList = Array.from(inspector.breakpoints.values());
return {
content: [{
type: "text",
text: JSON.stringify(breakpointsList, null, 2)
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error listing breakpoints: ${err.message}`
}]
};
}
}
);
// Evaluate expression tool
server.tool(
"evaluate",
"Evaluates a JavaScript expression in the current context",
{
expression: z.string().describe("JavaScript expression to evaluate")
},
async ({ expression }) => {
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 expression in a try-catch to better handle errors
const wrappedExpression = `
try {
${expression}
} catch (e) {
console.error('Evaluation error:', e);
e; // Return the error
}
`;
let result;
if (inspector.paused && inspector.currentCallFrames.length > 0) {
// When paused at a breakpoint, evaluate in the context of the call frame
const frame = inspector.currentCallFrames[0];
result = await inspector.evaluateOnCallFrame(frame.callFrameId, wrappedExpression);
} else {
// Otherwise, evaluate in the global context
result = await inspector.send('Runtime.evaluate', {
expression: wrappedExpression,
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');
let valueRepresentation;
if (result.result) {
if (result.result.type === 'object') {
if (result.result.value) {
// If we have a value, use it
valueRepresentation = JSON.stringify(result.result.value, null, 2);
} else if (result.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(result.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}]`;
}
}
}
valueRepresentation = JSON.stringify(formattedObject, null, 2);
} catch (propErr) {
// If we can't get properties, at least show the object description
valueRepresentation = result.result.description || `[${result.result.subtype || result.result.type}]`;
}
} else {
// Fallback for objects without value or objectId
valueRepresentation = result.result.description || `[${result.result.subtype || result.result.type}]`;
}
} else if (result.result.type === 'undefined') {
valueRepresentation = 'undefined';
} else if (result.result.value !== undefined) {
valueRepresentation = result.result.value.toString();
} else {
valueRepresentation = `[${result.result.type}]`;
}
} else {
valueRepresentation = 'No result';
}
// Prepare the response content
let responseContent = [];
// Add console output if there was any
if (consoleText.length > 0) {
responseContent.push({
type: "text",
text: `Console output:\n${consoleText}`
});
}
// Add the evaluation result
responseContent.push({
type: "text",
text: `Evaluation result: ${valueRepresentation}`
});
return { content: responseContent };
} catch (err) {
return {
content: [{
type: "text",
text: `Error evaluating expression: ${err.message}`
}]
};
}
}
);
// Get current location tool
server.tool(
"get_location",
"Gets the current execution location when paused",
{},
async () => {
try {
// Ensure debugger is enabled
if (!inspector.debuggerEnabled) {
await inspector.enableDebugger();
}
if (!inspector.paused || inspector.currentCallFrames.length === 0) {
return {
content: [{
type: "text",
text: "Debugger is not paused at a breakpoint"
}]
};
}
const frame = inspector.currentCallFrames[0];
const { url, lineNumber, columnNumber } = frame.location;
// Get call stack
const callstack = inspector.currentCallFrames.map(frame => {
return {
functionName: frame.functionName || '(anonymous)',
url: frame.url,
lineNumber: frame.location.lineNumber + 1,
columnNumber: frame.location.columnNumber
};
});
// Get source code for context
let sourceContext = '';
try {
const scriptSource = await inspector.getScriptSource(frame.location.scriptId);
if (scriptSource) {
const lines = scriptSource.split('\n');
const startLine = Math.max(0, lineNumber - 3);
const endLine = Math.min(lines.length - 1, lineNumber + 3);
for (let i = startLine; i <= endLine; i++) {
const prefix = i === lineNumber ? '> ' : ' ';
sourceContext += `${prefix}${i + 1}: ${lines[i]}\n`;
}
}
} catch (err) {
sourceContext = 'Unable to retrieve source code';
}
return {
content: [{
type: "text",
text: JSON.stringify({
url,
lineNumber: lineNumber + 1,
columnNumber,
callstack,
sourceContext
}, null, 2)
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error getting location: ${err.message}`
}]
};
}
}
);
// Add a tool specifically for getting console output
server.tool(
"get_console_output",
"Gets the most recent console output from the debugged process",
{
limit: z.number().optional().describe("Maximum number of console entries to return. Defaults to 20")
},
async ({ limit = 20 }) => {
try {
if (!inspector.consoleOutput || inspector.consoleOutput.length === 0) {
return {
content: [{
type: "text",
text: "No console output captured yet"
}]
};
}
// Get the most recent console output entries
const recentOutput = inspector.consoleOutput.slice(-limit);
const formattedOutput = recentOutput.map(output => {
const timestamp = new Date(output.timestamp).toISOString();
return `[${timestamp}] [${output.type}] ${output.message}`;
}).join('\n');
return {
content: [{
type: "text",
text: `Console output (most recent ${recentOutput.length} entries):\n\n${formattedOutput}`
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error getting console output: ${err.message}`
}]
};
}
}
);
// 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",
{
port: z.number().optional().describe("Optional port to connect to. Defaults to current port (9229)")
},
async ({ port }) => {
try {
// If a new port is specified, update the inspector's port
if (port && port !== inspector.port) {
inspector.port = port;
console.log(`Updated debugger port to ${port}`);
}
// If already connected, disconnect first
if (inspector.connected && inspector.ws) {
inspector.ws.close();
inspector.connected = false;
}
// Reset retry count and initialize
inspector.retryCount = 0;
inspector.initialize();
return {
content: [{
type: "text",
text: `Attempting to connect to Node.js debugger on port ${inspector.port}...`
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: `Error initiating connection retry: ${err.message}`
}]
};
}
}
);
// 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");