Skip to main content
Glama
node-debug-advanced.test.ts8.09 kB
import { test } from 'node:test'; import assert from 'node:assert'; import { spawn, ChildProcess } from 'node:child_process'; import CDP from 'chrome-remote-interface'; type Client = any; import path from 'node:path'; const scriptPath = path.resolve('tests/fixtures/advanced-script.js'); const debug = !!process.env.DEBUG_CDP_TEST; const log = (...args: unknown[]) => { if (debug) console.log(...args); }; function mcpCall(tool: string, params: unknown) { if (!debug) return; console.log(JSON.stringify({ event: 'mcp.tool_call', tool, params }, null, 2)); } function mcpResult(tool: string, payloadObj: unknown) { if (!debug) return; const response = { content: [ { type: 'text', text: JSON.stringify(payloadObj, null, 2) }, { type: 'json', json: payloadObj } ] }; console.log(JSON.stringify({ event: 'mcp.tool_result', tool, response }, null, 2)); } async function startDebuggedProcess(file: string): Promise<{ proc: ChildProcess, port: number }>{ const proc = spawn('node', ['--inspect-brk=0', file], { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, NODE_OPTIONS: '' } }); const port = await new Promise<number>((resolve, reject) => { let resolved = false; proc.stderr?.on('data', (data: Buffer) => { const msg = data.toString(); const match = msg.match(/ws:\/\/127\.0\.0\.1:(\d+)/); if (match) { resolved = true; resolve(Number(match[1])); } }); proc.on('exit', () => { if (!resolved) reject(new Error('process exited early')); }); }); return { proc, port }; } function frameUrl(frame: any, parsedScripts: any[]): string { const sid = frame && frame.location && frame.location.scriptId; const match = parsedScripts.find((ev) => ev.scriptId === sid && ev.url); return (frame && frame.url) || (match && match.url) || '<unknown>'; } async function getProps(Runtime: Client['Runtime'], objectId: string, limit = 20) { if (!objectId) return []; const { result } = await Runtime.getProperties({ objectId, ownProperties: true, generatePreview: true }); return (result || []).slice(0, limit).map((p: any) => { const v = p.value || {}; const val = Object.prototype.hasOwnProperty.call(v, 'value') ? v.value : (v.description || v.type); return { name: p.name, type: v.type, value: val, objectId: v.objectId }; }); } async function collectScopes(Runtime: Client['Runtime'], frame: any, parsedScripts: any[]) { const scopes: any[] = []; for (const s of frame.scopeChain || []) { const type = s.type; if (!s.object || !s.object.objectId) continue; const variables = await getProps(Runtime, s.object.objectId, type === 'global' ? 5 : 20); scopes.push({ type, variables, truncated: type === 'global' }); } let thisSummary: any = null; if (frame.this) { const t = frame.this; thisSummary = { type: t.type, description: t.description, className: t.className }; if (t.objectId) thisSummary.preview = await getProps(Runtime, t.objectId, 8); } return { frame: { functionName: frame.functionName || null, url: frameUrl(frame, parsedScripts), line: frame.location.lineNumber + 1, column: (frame.location.columnNumber ?? 0) + 1 }, this: thisSummary, scopes }; } test('advanced: scopes, this, arguments, and call stack across steps', { timeout: 30000 }, async () => { const { proc, port } = await startDebuggedProcess(scriptPath); const client: Client = await CDP({ host: '127.0.0.1', port }); const { Debugger, Runtime } = client; const parsedScripts: any[] = []; const unScript = Debugger.scriptParsed((ev: any) => parsedScripts.push(ev)); const consoleBuf: string[] = []; const unConsole = Runtime.consoleAPICalled(({ type, args }: any) => { const text = (args || []).map((a: any) => (Object.prototype.hasOwnProperty.call(a, 'value') ? a.value : a.description)).join(' '); consoleBuf.push(`[${type}] ${text}`); }); try { await Debugger.enable(); await Runtime.enable(); const onStart = Debugger.paused(); await Runtime.runIfWaitingForDebugger(); await onStart; const pause1P = Debugger.paused(); await Debugger.resume(); const pause1 = await pause1P; const f1 = pause1.callFrames[0]; assert.equal(f1.functionName, 'inc'); mcpCall('inspect_scopes', { maxProps: 15 }); const scope1 = await collectScopes(Runtime, f1, parsedScripts); mcpResult('inspect_scopes', scope1); log('pause1', scope1); const names1 = scope1.scopes.reduce((acc: string[], s: any) => acc.concat((s.variables || []).map((v: any) => v.name)), []); assert.ok(names1.includes('before') && names1.includes('s')); const sVar = scope1.scopes.find((s: any) => s.type === 'local').variables.find((v: any) => v.name === 's'); const countVar = scope1.scopes.flatMap((s: any) => s.variables || []).find((v: any) => v.name === 'count'); assert.equal(sVar && sVar.value, 2); assert.equal(countVar && countVar.value, 12); const cfId = f1.callFrameId; const a0 = await Debugger.evaluateOnCallFrame({ callFrameId: cfId, expression: 'arguments[0]', returnByValue: true }); const metaNested = await Debugger.evaluateOnCallFrame({ callFrameId: cfId, expression: 'meta.nested.a', returnByValue: true }); mcpCall('evaluate_expression', { expr: 'arguments[0]' }); mcpResult('evaluate_expression', { result: a0.result.value, consoleOutput: [] }); mcpCall('evaluate_expression', { expr: 'meta.nested.a' }); mcpResult('evaluate_expression', { result: metaNested.result.value, consoleOutput: [] }); assert.equal(a0.result.value, 2); assert.equal(metaNested.result.value, 1); const onAfterOut = Debugger.paused(); mcpCall('step_out', {}); await Debugger.stepOut(); const pause2 = await onAfterOut; const f2 = pause2.callFrames[0]; mcpResult('step_out', { status: `Paused at ${frameUrl(f2, parsedScripts)}:${f2.location.lineNumber + 1} (reason: ${pause2.reason})`, consoleOutput: consoleBuf.splice(0) }); const stack2 = (pause2.callFrames || []).slice(0, 5).map((cf: any) => ({ functionName: cf.functionName || null, url: frameUrl(cf, parsedScripts), line: cf.location.lineNumber + 1 })); mcpCall('list_call_stack', { depth: 5 }); mcpResult('list_call_stack', { frames: stack2 }); const pause3P = Debugger.paused(); await Debugger.resume(); const pause3 = await pause3P; const f3 = pause3.callFrames[0]; assert.equal(f3.functionName, 'times'); mcpCall('inspect_scopes', { maxProps: 10 }); const scope3 = await collectScopes(Runtime, f3, parsedScripts); mcpResult('inspect_scopes', scope3); const local3 = scope3.scopes.find((s: any) => s.type === 'local'); const nVar = local3 && local3.variables.find((v: any) => v.name === 'n'); assert.equal(nVar && nVar.value, 5); const thisPreview = scope3.this && scope3.this.preview; const multProp = (thisPreview || []).find((p: any) => p.name === 'mult'); assert.equal(multProp && multProp.value, 3); const thisMult = await Debugger.evaluateOnCallFrame({ callFrameId: f3.callFrameId, expression: 'this.mult', returnByValue: true }); mcpCall('evaluate_expression', { expr: 'this.mult' }); mcpResult('evaluate_expression', { result: thisMult.result.value, consoleOutput: [] }); assert.equal(thisMult.result.value, 3); const pause4P = Debugger.paused(); mcpCall('step_over', {}); await Debugger.stepOver(); const pause4 = await pause4P; const f4 = pause4.callFrames[0]; mcpResult('step_over', { status: `Paused at ${frameUrl(f4, parsedScripts)}:${f4.location.lineNumber + 1} (reason: ${pause4.reason})`, consoleOutput: consoleBuf.splice(0) }); await Debugger.resume(); try { await client.close(); } catch {} await new Promise((r) => proc.once('exit', r)); } finally { try { await client.close(); } catch {} if (!proc.killed) { try { proc.kill('SIGKILL'); } catch {} } try { if (typeof unScript === 'function') unScript(); } catch {} try { if (typeof unConsole === 'function') unConsole(); } catch {} } });

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/ScriptedAlchemy/devtools-debugger-mcp'

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