Skip to content

Commit b840559

Browse files
committed
feat: add 13 JavaScript Debugger tools via CDP Debugger domain
Add full debugger capabilities: enable/disable, breakpoints (set/remove/list), pause state inspection, stepping (over/into/out), call frame evaluation, script source retrieval, and script listing. New file: src/tools/debugger.ts Modified: McpPage (per-page debugger state + CDP event listeners), McpContext (13 debugger method implementations), ToolDefinition (Context interface extension), tools.ts (tool registration), types.ts (debugger interfaces)
1 parent 21634e6 commit b840559

6 files changed

Lines changed: 586 additions & 2 deletions

File tree

src/McpContext.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,4 +1126,135 @@ export class McpContext implements Context {
11261126
getExtension(id: string): InstalledExtension | undefined {
11271127
return this.#extensionRegistry.getById(id);
11281128
}
1129+
1130+
// ── Debugger methods ──
1131+
async enableDebugger(targetPage?: Page): Promise<void> {
1132+
const page = targetPage ?? this.getSelectedPage();
1133+
const mcpPage = this.#getMcpPage(page);
1134+
await mcpPage.enableDebugger();
1135+
}
1136+
1137+
async disableDebugger(targetPage?: Page): Promise<void> {
1138+
const page = targetPage ?? this.getSelectedPage();
1139+
const mcpPage = this.#getMcpPage(page);
1140+
await mcpPage.disableDebugger();
1141+
}
1142+
1143+
isDebuggerEnabled(targetPage?: Page): boolean {
1144+
const page = targetPage ?? this.getSelectedPage();
1145+
const mcpPage = this.#mcpPages.get(page);
1146+
return mcpPage?.debuggerState.enabled ?? false;
1147+
}
1148+
getDebuggerPausedState(targetPage?: Page) {
1149+
const page = targetPage ?? this.getSelectedPage();
1150+
const mcpPage = this.#getMcpPage(page);
1151+
return mcpPage.debuggerState.paused;
1152+
}
1153+
getDebuggerScripts(targetPage?: Page) {
1154+
const page = targetPage ?? this.getSelectedPage();
1155+
const mcpPage = this.#getMcpPage(page);
1156+
return [...mcpPage.debuggerState.scripts.values()];
1157+
}
1158+
getBreakpoints(targetPage?: Page) {
1159+
const page = targetPage ?? this.getSelectedPage();
1160+
const mcpPage = this.#getMcpPage(page);
1161+
return [...mcpPage.debuggerState.breakpoints.values()];
1162+
}
1163+
async setBreakpoint(
1164+
targetPage: Page | undefined,
1165+
url: string,
1166+
lineNumber: number,
1167+
columnNumber?: number,
1168+
condition?: string,
1169+
) {
1170+
const page = targetPage ?? this.getSelectedPage();
1171+
const mcpPage = this.#getMcpPage(page);
1172+
const session = mcpPage.getCdpSession();
1173+
const result = await session.send('Debugger.setBreakpointByUrl', {
1174+
urlRegex: url.replace(/[.*+?^$()|[\]\\]/g, '\\$&'),
1175+
lineNumber,
1176+
columnNumber,
1177+
condition,
1178+
});
1179+
const info = {
1180+
breakpointId: result.breakpointId,
1181+
url,
1182+
lineNumber,
1183+
columnNumber,
1184+
condition,
1185+
locations: (result.locations ?? []).map(loc => ({
1186+
scriptId: loc.scriptId,
1187+
lineNumber: loc.lineNumber,
1188+
columnNumber: loc.columnNumber ?? 0,
1189+
})),
1190+
};
1191+
mcpPage.debuggerState.breakpoints.set(info.breakpointId, info);
1192+
return info;
1193+
}
1194+
async removeBreakpoint(
1195+
targetPage: Page | undefined,
1196+
breakpointId: string,
1197+
): Promise<void> {
1198+
const page = targetPage ?? this.getSelectedPage();
1199+
const mcpPage = this.#getMcpPage(page);
1200+
const session = mcpPage.getCdpSession();
1201+
await session.send('Debugger.removeBreakpoint', {breakpointId});
1202+
mcpPage.debuggerState.breakpoints.delete(breakpointId);
1203+
}
1204+
async resumeDebugger(targetPage?: Page): Promise<void> {
1205+
const page = targetPage ?? this.getSelectedPage();
1206+
const mcpPage = this.#getMcpPage(page);
1207+
const session = mcpPage.getCdpSession();
1208+
await session.send('Debugger.resume');
1209+
}
1210+
async stepOver(targetPage?: Page): Promise<void> {
1211+
const page = targetPage ?? this.getSelectedPage();
1212+
const mcpPage = this.#getMcpPage(page);
1213+
const session = mcpPage.getCdpSession();
1214+
await session.send('Debugger.stepOver');
1215+
}
1216+
async stepInto(targetPage?: Page): Promise<void> {
1217+
const page = targetPage ?? this.getSelectedPage();
1218+
const mcpPage = this.#getMcpPage(page);
1219+
const session = mcpPage.getCdpSession();
1220+
await session.send('Debugger.stepInto');
1221+
}
1222+
async stepOut(targetPage?: Page): Promise<void> {
1223+
const page = targetPage ?? this.getSelectedPage();
1224+
const mcpPage = this.#getMcpPage(page);
1225+
const session = mcpPage.getCdpSession();
1226+
await session.send('Debugger.stepOut');
1227+
}
1228+
async evaluateOnCallFrame(
1229+
targetPage: Page | undefined,
1230+
callFrameId: string,
1231+
expression: string,
1232+
): Promise<string> {
1233+
const page = targetPage ?? this.getSelectedPage();
1234+
const mcpPage = this.#getMcpPage(page);
1235+
const session = mcpPage.getCdpSession();
1236+
const result = await session.send(
1237+
'Debugger.evaluateOnCallFrame',
1238+
{callFrameId, expression, returnByValue: true},
1239+
);
1240+
const remoteObj = result.result;
1241+
if (remoteObj.type === 'undefined') return 'undefined';
1242+
if (remoteObj.value !== undefined) {
1243+
return JSON.stringify(remoteObj.value);
1244+
}
1245+
return remoteObj.description ?? String(remoteObj.type);
1246+
}
1247+
async getScriptSource(
1248+
targetPage: Page | undefined,
1249+
scriptId: string,
1250+
): Promise<string> {
1251+
const page = targetPage ?? this.getSelectedPage();
1252+
const mcpPage = this.#getMcpPage(page);
1253+
const session = mcpPage.getCdpSession();
1254+
const result = await session.send(
1255+
'Debugger.getScriptSource',
1256+
{scriptId},
1257+
);
1258+
return result.scriptSource;
1259+
}
11291260
}

src/McpPage.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type {Dialog, Page, Viewport} from './third_party/index.js';
7+
import type {CDPSession, Dialog, Page, Protocol, Viewport} from './third_party/index.js';
88
import type {
9+
DebuggerBreakpointInfo,
10+
DebuggerPausedState,
11+
DebuggerScriptInfo,
12+
DebuggerState,
913
EmulationSettings,
1014
GeolocationOptions,
1115
TextSnapshot,
@@ -34,6 +38,86 @@ export class McpPage {
3438
isolatedContextName?: string;
3539
devToolsPage?: Page;
3640

41+
// Debugger
42+
debuggerState: DebuggerState = {
43+
enabled: false,
44+
paused: null,
45+
breakpoints: new Map(),
46+
scripts: new Map(),
47+
};
48+
#cdpSession: CDPSession | null = null;
49+
50+
async enableDebugger(): Promise<void> {
51+
if (this.debuggerState.enabled) return;
52+
// @ts-expect-error internal Puppeteer API
53+
const session = this.page._client() as CDPSession;
54+
this.#cdpSession = session;
55+
session.on('Debugger.paused', this.#onDebuggerPaused);
56+
session.on('Debugger.resumed', this.#onDebuggerResumed);
57+
session.on('Debugger.scriptParsed', this.#onScriptParsed);
58+
await session.send('Debugger.enable');
59+
this.debuggerState.enabled = true;
60+
}
61+
62+
async disableDebugger(): Promise<void> {
63+
if (!this.debuggerState.enabled || !this.#cdpSession) return;
64+
this.#cdpSession.off('Debugger.paused', this.#onDebuggerPaused);
65+
this.#cdpSession.off('Debugger.resumed', this.#onDebuggerResumed);
66+
this.#cdpSession.off('Debugger.scriptParsed', this.#onScriptParsed);
67+
await this.#cdpSession.send('Debugger.disable');
68+
this.debuggerState = {
69+
enabled: false,
70+
paused: null,
71+
breakpoints: new Map(),
72+
scripts: new Map(),
73+
};
74+
this.#cdpSession = null;
75+
}
76+
77+
getCdpSession(): CDPSession {
78+
if (!this.#cdpSession) {
79+
throw new Error('Debugger is not enabled. Call debugger_enable first.');
80+
}
81+
return this.#cdpSession;
82+
}
83+
#onDebuggerPaused = (params: Protocol.Debugger.PausedEvent): void => {
84+
const callFrames = params.callFrames.map(frame => ({
85+
callFrameId: frame.callFrameId,
86+
functionName: frame.functionName,
87+
url: frame.url ?? '',
88+
lineNumber: frame.location.lineNumber ?? 0,
89+
columnNumber: frame.location.columnNumber ?? 0,
90+
scopeChain: frame.scopeChain.map(scope => ({
91+
type: scope.type,
92+
name: scope.name,
93+
objectId: scope.object.objectId,
94+
})),
95+
}));
96+
this.debuggerState.paused = {
97+
callFrames,
98+
reason: params.reason ?? 'unknown',
99+
hitBreakpoints: params.hitBreakpoints,
100+
};
101+
};
102+
#onDebuggerResumed = (): void => {
103+
this.debuggerState.paused = null;
104+
};
105+
106+
#onScriptParsed = (params: Protocol.Debugger.ScriptParsedEvent): void => {
107+
const scriptId = params.scriptId;
108+
const url = params.url ?? '';
109+
if (!url) return;
110+
this.debuggerState.scripts.set(scriptId, {
111+
scriptId,
112+
url,
113+
startLine: params.startLine ?? 0,
114+
startColumn: params.startColumn ?? 0,
115+
endLine: params.endLine ?? 0,
116+
endColumn: params.endColumn ?? 0,
117+
sourceMapURL: params.sourceMapURL,
118+
});
119+
};
120+
37121
// Dialog
38122
#dialog?: Dialog;
39123
#dialogHandler: (dialog: Dialog) => void;
@@ -81,5 +165,6 @@ export class McpPage {
81165

82166
dispose(): void {
83167
this.page.off('dialog', this.#dialogHandler);
168+
void this.disableDebugger().catch(() => {});
84169
}
85170
}

src/tools/ToolDefinition.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ import type {
1414
Viewport,
1515
} from '../third_party/index.js';
1616
import type {InsightName, TraceResult} from '../trace-processing/parse.js';
17-
import type {TextSnapshotNode, GeolocationOptions} from '../types.js';
17+
import type {
18+
TextSnapshotNode,
19+
GeolocationOptions,
20+
DebuggerPausedState,
21+
DebuggerBreakpointInfo,
22+
DebuggerScriptInfo,
23+
} from '../types.js';
1824
import type {InstalledExtension} from '../utils/ExtensionRegistry.js';
1925
import type {PaginationOptions} from '../utils/types.js';
2026

@@ -189,6 +195,31 @@ export type Context = Readonly<{
189195
uninstallExtension(id: string): Promise<void>;
190196
listExtensions(): InstalledExtension[];
191197
getExtension(id: string): InstalledExtension | undefined;
198+
// Debugger
199+
enableDebugger(page: Page): Promise<void>;
200+
disableDebugger(page: Page): Promise<void>;
201+
isDebuggerEnabled(page: Page): boolean;
202+
getDebuggerPausedState(page: Page): DebuggerPausedState | null;
203+
setBreakpoint(
204+
page: Page,
205+
url: string,
206+
lineNumber: number,
207+
columnNumber?: number,
208+
condition?: string,
209+
): Promise<DebuggerBreakpointInfo>;
210+
removeBreakpoint(page: Page, breakpointId: string): Promise<void>;
211+
getBreakpoints(page: Page): DebuggerBreakpointInfo[];
212+
resumeDebugger(page: Page): Promise<void>;
213+
stepOver(page: Page): Promise<void>;
214+
stepInto(page: Page): Promise<void>;
215+
stepOut(page: Page): Promise<void>;
216+
evaluateOnCallFrame(
217+
page: Page,
218+
callFrameId: string,
219+
expression: string,
220+
): Promise<string>;
221+
getScriptSource(page: Page, scriptId: string): Promise<string>;
222+
getDebuggerScripts(page: Page): DebuggerScriptInfo[];
192223
}>;
193224

194225
export function defineTool<Schema extends zod.ZodRawShape>(

0 commit comments

Comments
 (0)