From 39ecb0c8b5f24eed5dca88fc5c57bcabe89089b2 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Thu, 25 Dec 2025 21:01:21 +0100 Subject: [PATCH] Update debugger.ts --- playwriter/src/debugger.ts | 1477 +++++++++--------------------------- 1 file changed, 364 insertions(+), 1113 deletions(-) diff --git a/playwriter/src/debugger.ts b/playwriter/src/debugger.ts index 4097a54..a102890 100644 --- a/playwriter/src/debugger.ts +++ b/playwriter/src/debugger.ts @@ -1,1169 +1,420 @@ -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 type { CDPSession } from './cdp-session.js' +import type { Protocol } from 'devtools-protocol' -// Disable console output for MCP compatibility -console.log = function() {}; -console.error = function() {}; - -// Create an MCP server -const server = new McpServer({ - name: "NodeJS Debugger", - version: "0.2.2", - 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) { - 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; - } - } +export interface BreakpointInfo { + id: string + file: string + line: number } -// Create the inspector instance with continuous retry enabled -const inspector = new Inspector(9229, { - maxRetries: 5, - retryInterval: 1000, - continuousRetry: true -}); +export interface ConsoleEntry { + type: string + message: string + timestamp: number +} -// Initialize console output storage -inspector.consoleOutput = []; +export interface LocationInfo { + url: string + lineNumber: number + columnNumber: number + callstack: Array<{ + functionName: string + url: string + lineNumber: number + columnNumber: number + }> + sourceContext: string +} -// 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(); +export interface EvaluateResult { + value: unknown + consoleOutput: string[] +} + +export interface VariablesResult { + [scope: string]: Record +} + +export class Debugger { + private cdp: CDPSession + private debuggerEnabled = false + private paused = false + private currentCallFrames: Protocol.Debugger.CallFrame[] = [] + private breakpoints = new Map() + private consoleOutput: ConsoleEntry[] = [] + + constructor({ cdp }: { cdp: CDPSession }) { + this.cdp = cdp + this.setupEventListeners() + } + + private setupEventListeners() { + this.cdp.on('Debugger.paused', (params) => { + this.paused = true + this.currentCallFrames = params.callFrames + }) + + this.cdp.on('Debugger.resumed', () => { + this.paused = false + this.currentCallFrames = [] + }) + + this.cdp.on('Runtime.consoleAPICalled', (params) => { + const message = params.args + .map((arg) => { + if (arg.type === 'string' || arg.type === 'number' || arg.type === 'boolean') { + return String(arg.value) + } + if (arg.type === 'object') { + if (arg.value) { + return JSON.stringify(arg.value, null, 2) + } + return arg.description || `[${arg.subtype || arg.type}]` + } + return JSON.stringify(arg) + }) + .join(' ') + + this.consoleOutput.push({ + type: params.type, + message, + timestamp: Date.now(), + }) + + if (this.consoleOutput.length > 100) { + this.consoleOutput.shift() + } + }) + } + + async enable(): Promise { + if (this.debuggerEnabled) { + return + } + await this.cdp.send('Debugger.enable') + await this.cdp.send('Runtime.enable') + await this.cdp.send('Runtime.runIfWaitingForDebugger') + this.debuggerEnabled = true + } + + async executeCode({ code }: { code: string }): Promise { + await this.enable() + + const consoleStartIndex = this.consoleOutput.length + + const wrappedCode = ` + try { + ${code} + } catch (e) { + e; + } + ` + + const response = await this.cdp.send('Runtime.evaluate', { + expression: wrappedCode, + objectGroup: 'console', + includeCommandLineAPI: true, + silent: false, + returnByValue: true, + generatePreview: true, + awaitPromise: true, + }) + + await new Promise((resolve) => { + setTimeout(resolve, 200) + }) + + const consoleOutputs = this.consoleOutput.slice(consoleStartIndex) + const value = await this.processRemoteObject(response.result) + + return { + value, + consoleOutput: consoleOutputs.map((o) => `[${o.type}] ${o.message}`), + } + } + + async setBreakpoint({ file, line }: { file: string; line: number }): Promise { + await this.enable() + + let fileUrl = file + if (!file.startsWith('file://') && !file.startsWith('http://') && !file.startsWith('https://')) { + fileUrl = `file://${file.startsWith('/') ? '' : '/'}${file}` + } + + const response = await this.cdp.send('Debugger.setBreakpointByUrl', { + lineNumber: line - 1, + urlRegex: fileUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), + columnNumber: 0, + }) + + this.breakpoints.set(response.breakpointId, { id: response.breakpointId, file, line }) + return response.breakpointId + } + + async deleteBreakpoint({ breakpointId }: { breakpointId: string }): Promise { + await this.enable() + await this.cdp.send('Debugger.removeBreakpoint', { breakpointId }) + this.breakpoints.delete(breakpointId) + } + + listBreakpoints(): BreakpointInfo[] { + return Array.from(this.breakpoints.values()) + } + + async inspectVariables({ scope = 'local' }: { scope?: 'local' | 'global' } = {}): Promise { + await this.enable() + + if (scope === 'global' || !this.paused) { + const response = await this.cdp.send('Runtime.globalLexicalScopeNames', {}) + const globalObjResponse = await this.cdp.send('Runtime.evaluate', { + expression: 'this', + returnByValue: true, + }) + + return { + lexicalNames: response.names as unknown as Record, + globalThis: globalObjResponse.result.value as Record, + } + } + + if (this.currentCallFrames.length === 0) { + throw new Error('No active call frames') + } + + const frame = this.currentCallFrames[0] + const result: VariablesResult = {} + + for (const scopeObj of frame.scopeChain) { + if (scopeObj.type === 'global') { + continue } - // Capture the current console output length to know where to start capturing new output - const consoleStartIndex = inspector.consoleOutput.length; + if (!scopeObj.object.objectId) { + continue + } - // 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 objProperties = await this.cdp.send('Runtime.getProperties', { + objectId: scopeObj.object.objectId, + ownProperties: true, + accessorPropertiesOnly: false, + generatePreview: true, + }) + + const variables: Record = {} + for (const prop of objProperties.result) { + if (prop.value && prop.configurable) { + variables[prop.name] = this.formatPropertyValue(prop.value) } - `; + } - const response = await inspector.send('Runtime.evaluate', { - expression: codeToExecute, - contextId: 1, + result[scopeObj.type] = variables + } + + return result + } + + async evaluate({ expression }: { expression: string }): Promise { + await this.enable() + + const consoleStartIndex = this.consoleOutput.length + + const wrappedExpression = ` + try { + ${expression} + } catch (e) { + e; + } + ` + + let response: Protocol.Debugger.EvaluateOnCallFrameResponse | Protocol.Runtime.EvaluateResponse + + if (this.paused && this.currentCallFrames.length > 0) { + const frame = this.currentCallFrames[0] + response = await this.cdp.send('Debugger.evaluateOnCallFrame', { + callFrameId: frame.callFrameId, + expression: wrappedExpression, objectGroup: 'console', includeCommandLineAPI: true, silent: false, returnByValue: true, generatePreview: true, - awaitPromise: true // This will wait for promises to resolve - }); + }) + } else { + response = await this.cdp.send('Runtime.evaluate', { + expression: wrappedExpression, + objectGroup: 'console', + includeCommandLineAPI: true, + silent: false, + returnByValue: true, + generatePreview: true, + awaitPromise: true, + }) + } - // Give some time for console logs to be processed - await new Promise(resolve => setTimeout(resolve, 200)); + 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'); + const consoleOutputs = this.consoleOutput.slice(consoleStartIndex) + const value = await this.processRemoteObject(response.result) - // 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}` - }] - }; + return { + value, + consoleOutput: consoleOutputs.map((o) => `[${o.type}] ${o.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 }) => { + async getLocation(): Promise { + await this.enable() + + if (!this.paused || this.currentCallFrames.length === 0) { + throw new Error('Debugger is not paused at a breakpoint') + } + + const frame = this.currentCallFrames[0] + const { scriptId, lineNumber, columnNumber } = frame.location + + const callstack = this.currentCallFrames.map((f) => ({ + functionName: f.functionName || '(anonymous)', + url: f.url, + lineNumber: f.location.lineNumber + 1, + columnNumber: f.location.columnNumber || 0, + })) + + let sourceContext = '' try { - // Ensure debugger is enabled - if (!inspector.debuggerEnabled) { - await inspector.enableDebugger(); + const scriptSource = await this.cdp.send('Debugger.getScriptSource', { scriptId }) + const lines = scriptSource.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 { + sourceContext = 'Unable to retrieve source code' + } - // 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}` - }] - }; + return { + url: frame.url, + lineNumber: lineNumber + 1, + columnNumber: columnNumber || 0, + callstack, + sourceContext, } } -); -// 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}` - }] - }; + async stepOver(): Promise { + await this.enable() + if (!this.paused) { + throw new Error('Debugger is not paused') } + await this.cdp.send('Debugger.stepOver') } -); -// 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}` - }] - }; + async stepInto(): Promise { + await this.enable() + if (!this.paused) { + throw new Error('Debugger is not paused') } + await this.cdp.send('Debugger.stepInto') } -); -// 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}` - }] - }; + async stepOut(): Promise { + await this.enable() + if (!this.paused) { + throw new Error('Debugger is not paused') } + await this.cdp.send('Debugger.stepOut') } -); -// 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}` - }] - }; + async resume(): Promise { + await this.enable() + if (!this.paused) { + throw new Error('Debugger is not paused') } + await this.cdp.send('Debugger.resume') } -); -// Continue execution tool -server.tool( - "continue", - "Continues code execution", - {}, - async () => { - try { - // Ensure debugger is enabled - if (!inspector.debuggerEnabled) { - await inspector.enableDebugger(); - } + getConsoleOutput({ limit = 20 }: { limit?: number } = {}): ConsoleEntry[] { + return this.consoleOutput.slice(-limit) + } - if (!inspector.paused) { - return { - content: [{ - type: "text", - text: "Debugger is not paused at a breakpoint" - }] - }; - } + isPaused(): boolean { + return this.paused + } - await inspector.send('Debugger.resume', {}); - - return { - content: [{ - type: "text", - text: "Execution resumed" - }] - }; - } catch (err) { - return { - content: [{ - type: "text", - text: `Error continuing execution: ${err.message}` - }] - }; + private formatPropertyValue(value: Protocol.Runtime.RemoteObject): unknown { + if (value.type === 'object' && value.subtype !== 'null') { + return `[${value.subtype || value.type}]` } - } -); - -// 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}` - }] - }; + if (value.type === 'function') { + return '[function]' } - } -); - -// 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}` - }] - }; + if (value.value !== undefined) { + return value.value } + return `[${value.type}]` } -); -// 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) { - 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}` - }] - }; + private async processRemoteObject(obj: Protocol.Runtime.RemoteObject): Promise { + if (obj.type === 'undefined') { + return undefined } - } -); -// 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 (obj.value !== undefined) { + return obj.value + } - 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 = ''; + if (obj.type === 'object' && obj.objectId) { 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); + const props = await this.cdp.send('Runtime.getProperties', { + objectId: obj.objectId, + ownProperties: true, + accessorPropertiesOnly: false, + generatePreview: true, + }) - for (let i = startLine; i <= endLine; i++) { - const prefix = i === lineNumber ? '> ' : ' '; - sourceContext += `${prefix}${i + 1}: ${lines[i]}\n`; + const result: Record = {} + for (const prop of props.result) { + if (prop.value) { + if (prop.value.type === 'object' && prop.value.objectId && prop.value.subtype !== 'null') { + try { + const nestedProps = await this.cdp.send('Runtime.getProperties', { + objectId: prop.value.objectId, + ownProperties: true, + accessorPropertiesOnly: false, + generatePreview: true, + }) + const nestedObj: Record = {} + for (const nestedProp of nestedProps.result) { + if (nestedProp.value) { + nestedObj[nestedProp.name] = + nestedProp.value.value !== undefined + ? nestedProp.value.value + : nestedProp.value.description || `[${nestedProp.value.subtype || nestedProp.value.type}]` + } + } + result[prop.name] = nestedObj + } catch { + result[prop.name] = prop.value.description || `[${prop.value.subtype || prop.value.type}]` + } + } else if (prop.value.type === 'function') { + result[prop.name] = '[function]' + } else if (prop.value.value !== undefined) { + result[prop.name] = prop.value.value + } else { + result[prop.name] = `[${prop.value.type}]` + } } } - } catch (err) { - sourceContext = 'Unable to retrieve source code'; + return result + } catch { + return obj.description || `[${obj.subtype || obj.type}]` } - - 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}` - }] - }; } + + return obj.description || `[${obj.type}]` } -); - -// 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; - } - - // If already connected, disconnect first - if (inspector.connected && inspector.ws) { - inspector.ws.close(); - inspector.connected = false; - inspector.debuggerEnabled = 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); +}