cdp-toolkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js ADDED
@@ -0,0 +1,3056 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/tools/pages.ts
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { dirname } from "node:path";
6
+
7
+ // src/client.ts
8
+ var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
9
+ var DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
10
+
11
+ class CdpError extends Error {
12
+ code;
13
+ data;
14
+ constructor(message, code, data) {
15
+ super(message);
16
+ this.code = code;
17
+ this.data = data;
18
+ this.name = "CdpError";
19
+ }
20
+ }
21
+
22
+ class CdpConnection {
23
+ wsUrl;
24
+ opts;
25
+ ws;
26
+ nextId = 0;
27
+ pending = new Map;
28
+ listeners = new Map;
29
+ closed = false;
30
+ constructor(wsUrl, opts = {}) {
31
+ this.wsUrl = wsUrl;
32
+ this.opts = opts;
33
+ }
34
+ connect() {
35
+ return new Promise((resolve, reject) => {
36
+ const ws = new WebSocket(this.wsUrl);
37
+ this.ws = ws;
38
+ const openTimer = setTimeout(() => reject(new CdpError(`connect timeout: ${this.wsUrl}`)), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
39
+ ws.onopen = () => {
40
+ clearTimeout(openTimer);
41
+ resolve(this);
42
+ };
43
+ ws.onerror = (e) => {
44
+ clearTimeout(openTimer);
45
+ const msg = e?.message ?? "websocket error";
46
+ if (this.pending.size === 0)
47
+ reject(new CdpError(msg));
48
+ this.rejectAll(new CdpError(msg));
49
+ };
50
+ ws.onclose = () => {
51
+ this.closed = true;
52
+ this.rejectAll(new CdpError("connection closed"));
53
+ };
54
+ ws.onmessage = (ev) => this.onMessage(String(ev.data));
55
+ });
56
+ }
57
+ onMessage(raw) {
58
+ let msg;
59
+ try {
60
+ msg = JSON.parse(raw);
61
+ } catch {
62
+ return;
63
+ }
64
+ if (typeof msg.id === "number" && this.pending.has(msg.id)) {
65
+ const p = this.pending.get(msg.id);
66
+ clearTimeout(p.timer);
67
+ this.pending.delete(msg.id);
68
+ p.resolve(msg);
69
+ return;
70
+ }
71
+ if (msg.method) {
72
+ const set = this.listeners.get(msg.method);
73
+ if (set)
74
+ for (const h of set)
75
+ h(msg.params ?? {}, msg.sessionId);
76
+ const star = this.listeners.get("*");
77
+ if (star)
78
+ for (const h of star)
79
+ h({ method: msg.method, ...msg.params ?? {} }, msg.sessionId);
80
+ }
81
+ }
82
+ rejectAll(err) {
83
+ for (const [, p] of this.pending) {
84
+ clearTimeout(p.timer);
85
+ p.reject(err);
86
+ }
87
+ this.pending.clear();
88
+ }
89
+ send(method, params = {}, opts = {}) {
90
+ if (this.closed || !this.ws)
91
+ return Promise.reject(new CdpError("connection not open"));
92
+ const id = ++this.nextId;
93
+ const timeoutMs = opts.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
94
+ const payload = { id, method, params };
95
+ if (opts.sessionId)
96
+ payload.sessionId = opts.sessionId;
97
+ return new Promise((resolve, reject) => {
98
+ const timer = setTimeout(() => {
99
+ this.pending.delete(id);
100
+ reject(new CdpError(`'${method}' timed out after ${timeoutMs}ms`));
101
+ }, timeoutMs);
102
+ this.pending.set(id, {
103
+ resolve: (r) => {
104
+ if (r.error)
105
+ reject(new CdpError(`${method}: ${r.error.message}`, r.error.code, r.error.data));
106
+ else
107
+ resolve(r.result ?? {});
108
+ },
109
+ reject,
110
+ timer
111
+ });
112
+ this.ws.send(JSON.stringify(payload));
113
+ });
114
+ }
115
+ on(method, handler) {
116
+ let set = this.listeners.get(method);
117
+ if (!set) {
118
+ set = new Set;
119
+ this.listeners.set(method, set);
120
+ }
121
+ set.add(handler);
122
+ return () => set.delete(handler);
123
+ }
124
+ waitFor(method, predicate, timeoutMs = this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS) {
125
+ return new Promise((resolve, reject) => {
126
+ const timer = setTimeout(() => {
127
+ off();
128
+ reject(new CdpError(`waitFor('${method}') timed out after ${timeoutMs}ms`));
129
+ }, timeoutMs);
130
+ const off = this.on(method, (params) => {
131
+ if (!predicate || predicate(params)) {
132
+ clearTimeout(timer);
133
+ off();
134
+ resolve(params);
135
+ }
136
+ });
137
+ });
138
+ }
139
+ close() {
140
+ this.closed = true;
141
+ this.rejectAll(new CdpError("closed by caller"));
142
+ try {
143
+ this.ws?.close();
144
+ } catch {}
145
+ }
146
+ }
147
+ async function httpJson(path) {
148
+ const res = await fetch(`${BASE}${path}`);
149
+ if (!res.ok)
150
+ throw new CdpError(`${path} -> HTTP ${res.status}`);
151
+ return await res.json();
152
+ }
153
+ function listTargets() {
154
+ return httpJson("/json/list");
155
+ }
156
+ async function browserWsUrl() {
157
+ const v = await httpJson("/json/version");
158
+ return v.webSocketDebuggerUrl;
159
+ }
160
+ async function resolveTarget(selector) {
161
+ const targets = await listTargets();
162
+ const pages = targets.filter((t) => t.type === "page");
163
+ if (!selector || selector === "active") {
164
+ if (!pages.length)
165
+ throw new CdpError("no page targets open");
166
+ return pages[0];
167
+ }
168
+ if (selector.startsWith("index:")) {
169
+ const i = Number(selector.slice(6));
170
+ if (!pages[i])
171
+ throw new CdpError(`no page target at index ${i} (have ${pages.length})`);
172
+ return pages[i];
173
+ }
174
+ if (selector.startsWith("url:")) {
175
+ const needle = selector.slice(4);
176
+ const hit = pages.find((t) => t.url.includes(needle));
177
+ if (!hit)
178
+ throw new CdpError(`no page url containing '${needle}'`);
179
+ return hit;
180
+ }
181
+ if (selector.startsWith("title:")) {
182
+ const needle = selector.slice(6);
183
+ const hit = pages.find((t) => t.title.includes(needle));
184
+ if (!hit)
185
+ throw new CdpError(`no page title containing '${needle}'`);
186
+ return hit;
187
+ }
188
+ const byId = targets.find((t) => t.id === selector);
189
+ if (!byId)
190
+ throw new CdpError(`no target with id '${selector}'`);
191
+ return byId;
192
+ }
193
+ async function openBrowser(opts = {}) {
194
+ return new CdpConnection(await browserWsUrl(), opts).connect();
195
+ }
196
+ async function openPage(selector, opts = {}) {
197
+ const target = await resolveTarget(selector);
198
+ const conn = await new CdpConnection(target.webSocketDebuggerUrl, opts).connect();
199
+ return { conn, target };
200
+ }
201
+ async function withPage(selector, fn, opts = {}) {
202
+ const { conn, target } = await openPage(selector, opts);
203
+ try {
204
+ return await fn(conn, target);
205
+ } finally {
206
+ conn.close();
207
+ }
208
+ }
209
+
210
+ // src/tools/pages.ts
211
+ var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
212
+ var SELECTED_FILE = `${STATE_DIR}/selected`;
213
+ function toPageInfo(t) {
214
+ return { id: t.id, url: t.url, title: t.title, type: t.type };
215
+ }
216
+ async function listPages(args = {}) {
217
+ const targets = await listTargets();
218
+ const filtered = args.all ? targets : targets.filter((t) => t.type === "page");
219
+ const pages = filtered.map(toPageInfo);
220
+ return { pages, count: pages.length };
221
+ }
222
+ async function newPage(args = {}) {
223
+ const url = args.url ?? "about:blank";
224
+ const conn = await openBrowser();
225
+ try {
226
+ const { targetId } = await conn.send("Target.createTarget", { url });
227
+ if (!targetId)
228
+ throw new CdpError("Target.createTarget returned no targetId");
229
+ return { targetId, url };
230
+ } finally {
231
+ conn.close();
232
+ }
233
+ }
234
+ async function closePage(args) {
235
+ if (args.target === undefined || args.target === "") {
236
+ throw new CdpError("close_page requires an explicit target; refusing to guess which page to close");
237
+ }
238
+ const target = await resolveTarget(args.target);
239
+ const conn = await openBrowser();
240
+ try {
241
+ const res = await conn.send("Target.closeTarget", { targetId: target.id });
242
+ return { closed: target.id, success: res.success ?? true };
243
+ } finally {
244
+ conn.close();
245
+ }
246
+ }
247
+ async function selectPage(args) {
248
+ if (args.target === undefined || args.target === "") {
249
+ throw new CdpError("select_page requires an explicit target");
250
+ }
251
+ const target = await resolveTarget(args.target);
252
+ const conn = await openBrowser();
253
+ try {
254
+ await conn.send("Target.activateTarget", { targetId: target.id });
255
+ } finally {
256
+ conn.close();
257
+ }
258
+ await mkdir(dirname(SELECTED_FILE), { recursive: true });
259
+ await writeFile(SELECTED_FILE, target.id, "utf8");
260
+ return { selected: target.id };
261
+ }
262
+
263
+ // src/tools/navigation.ts
264
+ async function navigatePage(args) {
265
+ const reload = args.reload === true;
266
+ if (!reload && (!args.url || typeof args.url !== "string")) {
267
+ throw new CdpError("navigate_page: 'url' is required (or pass reload:true to reload the current page)");
268
+ }
269
+ const waitUntil = args.waitUntil ?? "load";
270
+ return withPage(args.target, async (conn) => {
271
+ await conn.send("Page.enable");
272
+ const milestoneMethod = waitUntil === "load" ? "Page.loadEventFired" : "Page.domContentEventFired";
273
+ const milestone = conn.waitFor(milestoneMethod, undefined, args.timeoutMs).then(() => waitUntil).catch(() => {
274
+ return;
275
+ });
276
+ const stopped = conn.waitFor("Page.frameStoppedLoading", undefined, args.timeoutMs).then(() => "frameStoppedLoading").catch(() => {
277
+ return;
278
+ });
279
+ let resolvedUrl;
280
+ let frameId;
281
+ if (reload) {
282
+ await conn.send("Page.reload", { ignoreCache: args.ignoreCache === true }, args.timeoutMs ? { timeoutMs: args.timeoutMs } : {});
283
+ const tree = await conn.send("Page.getFrameTree").catch(() => {
284
+ return;
285
+ });
286
+ frameId = tree?.frameTree?.frame?.id ?? "";
287
+ resolvedUrl = tree?.frameTree?.frame?.url ?? args.url ?? "";
288
+ } else {
289
+ const nav = await conn.send("Page.navigate", { url: args.url }, args.timeoutMs ? { timeoutMs: args.timeoutMs } : {});
290
+ if (nav.errorText) {
291
+ throw new CdpError(`navigate_page: ${nav.errorText} (${args.url})`);
292
+ }
293
+ frameId = nav.frameId;
294
+ resolvedUrl = args.url;
295
+ }
296
+ const settled = await Promise.race([milestone, stopped]);
297
+ const waitedFor = settled ?? "navigate-only";
298
+ return reload ? { url: resolvedUrl, frameId, reloaded: true, waitedFor } : { url: resolvedUrl, frameId, waitedFor };
299
+ }, args.timeoutMs ? { timeoutMs: args.timeoutMs } : {});
300
+ }
301
+ var DEFAULT_WAIT_TIMEOUT_MS = 15000;
302
+ var DEFAULT_POLL_MS = 250;
303
+ async function waitForText(args) {
304
+ if (typeof args.text !== "string" || args.text.length === 0) {
305
+ throw new CdpError("wait_for: 'text' is required");
306
+ }
307
+ const timeoutMs = args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
308
+ const pollMs = args.pollMs ?? DEFAULT_POLL_MS;
309
+ const start = Date.now();
310
+ return withPage(args.target, async (conn, _target) => {
311
+ await conn.send("Runtime.enable");
312
+ const expr = `(() => { const b = document.body; return !!b && typeof b.innerText === 'string' && b.innerText.includes(${JSON.stringify(args.text)}); })()`;
313
+ for (;; ) {
314
+ const { result } = await conn.send("Runtime.evaluate", {
315
+ expression: expr,
316
+ returnByValue: true
317
+ });
318
+ if (result?.value === true) {
319
+ return { found: true, waitedMs: Date.now() - start };
320
+ }
321
+ if (Date.now() - start >= timeoutMs) {
322
+ throw new CdpError(`wait_for: text not found within ${timeoutMs}ms: ${JSON.stringify(args.text)}`);
323
+ }
324
+ await delay(Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - start))));
325
+ }
326
+ }, { timeoutMs: Math.max(timeoutMs, DEFAULT_WAIT_TIMEOUT_MS) });
327
+ }
328
+ function delay(ms) {
329
+ return new Promise((resolve) => setTimeout(resolve, ms));
330
+ }
331
+
332
+ // src/tools/evaluate.ts
333
+ function exceptionMessage(details) {
334
+ const ex = details.exception;
335
+ const fromObject = ex?.description ?? (typeof ex?.value === "string" ? ex.value : ex?.value !== undefined ? JSON.stringify(ex.value) : undefined);
336
+ return fromObject ?? details.text ?? "evaluation threw";
337
+ }
338
+ function unwrap(obj) {
339
+ if (Object.prototype.hasOwnProperty.call(obj, "value"))
340
+ return obj.value;
341
+ if (obj.unserializableValue !== undefined)
342
+ return obj.unserializableValue;
343
+ if (obj.type === "undefined")
344
+ return;
345
+ return obj.description ?? null;
346
+ }
347
+ async function evaluateExpression(conn, expression, awaitPromise) {
348
+ const res = await conn.send("Runtime.evaluate", {
349
+ expression,
350
+ returnByValue: true,
351
+ awaitPromise,
352
+ replMode: false,
353
+ silent: false
354
+ });
355
+ if (res.exceptionDetails)
356
+ throw new CdpError(exceptionMessage(res.exceptionDetails));
357
+ return unwrap(res.result);
358
+ }
359
+ async function evaluateFunction(conn, expression, awaitPromise, args) {
360
+ const globalObj = await conn.send("Runtime.evaluate", {
361
+ expression: "globalThis",
362
+ returnByValue: false
363
+ });
364
+ const objectId = globalObj.result.objectId;
365
+ if (!objectId)
366
+ throw new CdpError("could not resolve global object for function call");
367
+ const res = await conn.send("Runtime.callFunctionOn", {
368
+ objectId,
369
+ functionDeclaration: expression,
370
+ arguments: args.map((value) => ({ value })),
371
+ returnByValue: true,
372
+ awaitPromise,
373
+ silent: false
374
+ });
375
+ if (res.exceptionDetails)
376
+ throw new CdpError(exceptionMessage(res.exceptionDetails));
377
+ return unwrap(res.result);
378
+ }
379
+ async function evaluateScript(args) {
380
+ const { expression } = args;
381
+ if (typeof expression !== "string" || expression.length === 0) {
382
+ throw new CdpError("evaluateScript: 'expression' must be a non-empty string");
383
+ }
384
+ const awaitPromise = args.awaitPromise ?? true;
385
+ const callArgs = args.args;
386
+ return withPage(args.target, async (conn) => {
387
+ await conn.send("Runtime.enable");
388
+ if (callArgs && callArgs.length > 0) {
389
+ return evaluateFunction(conn, expression, awaitPromise, callArgs);
390
+ }
391
+ return evaluateExpression(conn, expression, awaitPromise);
392
+ });
393
+ }
394
+
395
+ // src/tools/snapshot.ts
396
+ var INTERACTIVE_ROLES = new Set([
397
+ "button",
398
+ "link",
399
+ "textbox",
400
+ "searchbox",
401
+ "checkbox",
402
+ "radio",
403
+ "combobox",
404
+ "listbox",
405
+ "option",
406
+ "menuitem",
407
+ "menuitemcheckbox",
408
+ "menuitemradio",
409
+ "tab",
410
+ "switch",
411
+ "slider",
412
+ "spinbutton",
413
+ "textfield",
414
+ "MenuItem",
415
+ "Button"
416
+ ]);
417
+ function axString(v) {
418
+ if (!v)
419
+ return;
420
+ const raw = v.value;
421
+ if (raw === undefined || raw === null)
422
+ return;
423
+ const s = String(raw).trim();
424
+ return s.length ? s : undefined;
425
+ }
426
+ function propValue(node, name) {
427
+ const p = node.properties?.find((x) => x.name === name);
428
+ return axString(p?.value);
429
+ }
430
+ function isInteractive(role) {
431
+ if (!role)
432
+ return false;
433
+ return INTERACTIVE_ROLES.has(role) || INTERACTIVE_ROLES.has(role.toLowerCase());
434
+ }
435
+ function formatNode(node, interactiveOnly) {
436
+ const role = axString(node.role);
437
+ const name = axString(node.name);
438
+ const uid = node.backendDOMNodeId;
439
+ if (uid === undefined)
440
+ return;
441
+ if (interactiveOnly && !isInteractive(role))
442
+ return;
443
+ if (!interactiveOnly && !role && !name)
444
+ return;
445
+ const extras = [];
446
+ const value = axString(node.value);
447
+ if (value)
448
+ extras.push(`value=${JSON.stringify(value)}`);
449
+ const checked = propValue(node, "checked");
450
+ if (checked && checked !== "false")
451
+ extras.push(`checked=${checked}`);
452
+ const expanded = propValue(node, "expanded");
453
+ if (expanded)
454
+ extras.push(`expanded=${expanded}`);
455
+ const disabled = propValue(node, "disabled");
456
+ if (disabled === "true")
457
+ extras.push("disabled");
458
+ const url = propValue(node, "url");
459
+ if (url)
460
+ extras.push(`url=${url}`);
461
+ const focused = propValue(node, "focused");
462
+ if (focused === "true")
463
+ extras.push("focused");
464
+ const label = name ? ` ${JSON.stringify(name)}` : "";
465
+ const extra = extras.length ? ` [${extras.join(" ")}]` : "";
466
+ return `[${uid}] ${role ?? "generic"}${label}${extra}`;
467
+ }
468
+ async function takeSnapshot(args = {}) {
469
+ const interactiveOnly = args.interactiveOnly ?? false;
470
+ return withPage(args.target, async (conn, target) => {
471
+ await conn.send("Accessibility.enable");
472
+ const { nodes } = await conn.send("Accessibility.getFullAXTree");
473
+ const byId = new Map;
474
+ for (const n of nodes)
475
+ byId.set(n.nodeId, n);
476
+ const roots = nodes.filter((n) => !n.parentId || !byId.has(n.parentId));
477
+ const lines = [];
478
+ let nodeCount = 0;
479
+ const walk = (node, depth) => {
480
+ const line = node.ignored ? undefined : formatNode(node, interactiveOnly);
481
+ if (line !== undefined) {
482
+ const indent = interactiveOnly ? "" : " ".repeat(depth);
483
+ lines.push(`${indent}${line}`);
484
+ nodeCount++;
485
+ }
486
+ const childDepth = line !== undefined && !interactiveOnly ? depth + 1 : depth;
487
+ for (const childId of node.childIds ?? []) {
488
+ const child = byId.get(childId);
489
+ if (child)
490
+ walk(child, childDepth);
491
+ }
492
+ };
493
+ for (const root of roots)
494
+ walk(root, 0);
495
+ return {
496
+ snapshot: lines.join(`
497
+ `),
498
+ target: { id: target.id, url: target.url, title: target.title },
499
+ nodeCount
500
+ };
501
+ });
502
+ }
503
+ async function resolveUid(conn, uid) {
504
+ const { object } = await conn.send("DOM.resolveNode", {
505
+ backendNodeId: uid
506
+ });
507
+ if (!object?.objectId) {
508
+ throw new Error(`resolveUid: DOM.resolveNode returned no objectId for uid ${uid}`);
509
+ }
510
+ return { objectId: object.objectId };
511
+ }
512
+
513
+ // src/tools/input.ts
514
+ async function resolveElement(conn, t) {
515
+ const hasUid = t.uid !== undefined && t.uid !== null;
516
+ const hasSelector = typeof t.selector === "string" && t.selector.length > 0;
517
+ if (hasUid === hasSelector) {
518
+ throw new CdpError("provide exactly one of { uid } or { selector }");
519
+ }
520
+ if (hasUid) {
521
+ return resolveUid(conn, t.uid);
522
+ }
523
+ const { result, exceptionDetails } = await conn.send("Runtime.evaluate", {
524
+ expression: `document.querySelector(${JSON.stringify(t.selector)})`,
525
+ returnByValue: false
526
+ });
527
+ if (exceptionDetails) {
528
+ throw new CdpError(`selector '${t.selector}' evaluation failed: ${exceptionDetails.text ?? "error"}`);
529
+ }
530
+ if (!result.objectId || result.subtype === "null") {
531
+ throw new CdpError(`selector '${t.selector}' matched no element`);
532
+ }
533
+ return { objectId: result.objectId };
534
+ }
535
+ async function centerOf(conn, objectId) {
536
+ const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", {
537
+ objectId,
538
+ functionDeclaration: "function(){this.scrollIntoView({block:'center',inline:'center'});const r=this.getBoundingClientRect();if(r.width===0&&r.height===0)return null;return {x:r.left+r.width/2,y:r.top+r.height/2};}",
539
+ returnByValue: true
540
+ });
541
+ if (exceptionDetails) {
542
+ throw new CdpError(`could not measure element: ${exceptionDetails.text ?? "error"}`);
543
+ }
544
+ if (!result.value) {
545
+ throw new CdpError("element has zero size / is not visible; cannot compute a click point");
546
+ }
547
+ return result.value;
548
+ }
549
+ async function focusElement(conn, objectId) {
550
+ await conn.send("Runtime.callFunctionOn", {
551
+ objectId,
552
+ functionDeclaration: "function(){this.focus&&this.focus();}",
553
+ returnByValue: true
554
+ });
555
+ }
556
+ async function click(args) {
557
+ const button = args.button ?? "left";
558
+ const clickCount = args.clickCount ?? 1;
559
+ return withPage(args.target, async (conn) => {
560
+ const { objectId } = await resolveElement(conn, args);
561
+ const { x, y } = await centerOf(conn, objectId);
562
+ await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
563
+ await conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button, clickCount });
564
+ await conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button, clickCount });
565
+ return { clicked: true, x, y };
566
+ });
567
+ }
568
+ async function hover(args) {
569
+ return withPage(args.target, async (conn) => {
570
+ const { objectId } = await resolveElement(conn, args);
571
+ const { x, y } = await centerOf(conn, objectId);
572
+ await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
573
+ return { hovered: true, x, y };
574
+ });
575
+ }
576
+ async function drag(args) {
577
+ if (!args.from || !args.to)
578
+ throw new CdpError("drag requires { from } and { to }");
579
+ return withPage(args.target, async (conn) => {
580
+ const { objectId: srcId } = await resolveElement(conn, args.from);
581
+ const from = await centerOf(conn, srcId);
582
+ const { objectId: dstId } = await resolveElement(conn, args.to);
583
+ const to = await centerOf(conn, dstId);
584
+ await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: from.x, y: from.y });
585
+ await conn.send("Input.dispatchMouseEvent", {
586
+ type: "mousePressed",
587
+ x: from.x,
588
+ y: from.y,
589
+ button: "left",
590
+ clickCount: 1
591
+ });
592
+ await conn.send("Input.dispatchMouseEvent", {
593
+ type: "mouseMoved",
594
+ x: (from.x + to.x) / 2,
595
+ y: (from.y + to.y) / 2,
596
+ button: "left"
597
+ });
598
+ await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: to.x, y: to.y, button: "left" });
599
+ await conn.send("Input.dispatchMouseEvent", {
600
+ type: "mouseReleased",
601
+ x: to.x,
602
+ y: to.y,
603
+ button: "left",
604
+ clickCount: 1
605
+ });
606
+ return { dragged: true, from, to };
607
+ });
608
+ }
609
+ async function setValue(conn, objectId, value) {
610
+ await focusElement(conn, objectId);
611
+ await conn.send("Runtime.callFunctionOn", {
612
+ objectId,
613
+ functionDeclaration: "function(){if('value'in this){this.value='';this.dispatchEvent(new Event('input',{bubbles:true}));}else if(this.isContentEditable){this.textContent='';}}",
614
+ returnByValue: true
615
+ });
616
+ if (value.length) {
617
+ await conn.send("Input.insertText", { text: value });
618
+ }
619
+ }
620
+ async function fill(args) {
621
+ if (typeof args.value !== "string")
622
+ throw new CdpError("fill requires a string { value }");
623
+ return withPage(args.target, async (conn) => {
624
+ const { objectId } = await resolveElement(conn, args);
625
+ await setValue(conn, objectId, args.value);
626
+ return { filled: true, value: args.value };
627
+ });
628
+ }
629
+ async function typeText(args) {
630
+ if (typeof args.text !== "string")
631
+ throw new CdpError("type_text requires a string { text }");
632
+ return withPage(args.target, async (conn) => {
633
+ const { objectId } = await resolveElement(conn, args);
634
+ await focusElement(conn, objectId);
635
+ if (args.text.length)
636
+ await conn.send("Input.insertText", { text: args.text });
637
+ return { typed: true, text: args.text };
638
+ });
639
+ }
640
+ async function fillForm(args) {
641
+ if (!Array.isArray(args.fields) || args.fields.length === 0) {
642
+ throw new CdpError("fill_form requires a non-empty { fields } array");
643
+ }
644
+ return withPage(args.target, async (conn) => {
645
+ let filled = 0;
646
+ for (const field of args.fields) {
647
+ if (typeof field.value !== "string") {
648
+ throw new CdpError("each fill_form field requires a string value");
649
+ }
650
+ const { objectId } = await resolveElement(conn, field);
651
+ await setValue(conn, objectId, field.value);
652
+ filled++;
653
+ }
654
+ return { filled, fields: args.fields.length };
655
+ });
656
+ }
657
+ var NAMED_KEYS = {
658
+ enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
659
+ return: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
660
+ tab: { key: "Tab", code: "Tab", keyCode: 9 },
661
+ escape: { key: "Escape", code: "Escape", keyCode: 27 },
662
+ esc: { key: "Escape", code: "Escape", keyCode: 27 },
663
+ backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
664
+ delete: { key: "Delete", code: "Delete", keyCode: 46 },
665
+ space: { key: " ", code: "Space", keyCode: 32, text: " " },
666
+ arrowup: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
667
+ arrowdown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
668
+ arrowleft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
669
+ arrowright: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
670
+ up: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
671
+ down: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
672
+ left: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
673
+ right: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
674
+ home: { key: "Home", code: "Home", keyCode: 36 },
675
+ end: { key: "End", code: "End", keyCode: 35 },
676
+ pageup: { key: "PageUp", code: "PageUp", keyCode: 33 },
677
+ pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
678
+ };
679
+ var MODIFIER_BITS = {
680
+ alt: 1,
681
+ control: 2,
682
+ ctrl: 2,
683
+ meta: 4,
684
+ cmd: 4,
685
+ command: 4,
686
+ shift: 8
687
+ };
688
+ function resolveKey(key) {
689
+ const named = NAMED_KEYS[key.toLowerCase()];
690
+ if (named)
691
+ return named;
692
+ if (key.length === 1) {
693
+ const upper = key.toUpperCase();
694
+ const isLetter = upper >= "A" && upper <= "Z";
695
+ const isDigit = key >= "0" && key <= "9";
696
+ const code = isLetter ? `Key${upper}` : isDigit ? `Digit${key}` : `Key${upper}`;
697
+ const keyCode = upper.charCodeAt(0);
698
+ return { key, code, keyCode, text: key };
699
+ }
700
+ throw new CdpError(`unknown key '${key}' (use a named key like Enter/Tab/ArrowDown or a single character)`);
701
+ }
702
+ async function pressKey(args) {
703
+ if (typeof args.key !== "string" || !args.key.length) {
704
+ throw new CdpError("press_key requires a non-empty { key }");
705
+ }
706
+ const spec = resolveKey(args.key);
707
+ let modBits = 0;
708
+ const mods = args.modifiers ?? [];
709
+ for (const m of mods) {
710
+ const bit = MODIFIER_BITS[m.toLowerCase()];
711
+ if (bit === undefined)
712
+ throw new CdpError(`unknown modifier '${m}'`);
713
+ modBits |= bit;
714
+ }
715
+ const suppressText = (modBits & ~MODIFIER_BITS.shift) !== 0;
716
+ return withPage(args.target, async (conn) => {
717
+ const down = {
718
+ type: "keyDown",
719
+ key: spec.key,
720
+ code: spec.code,
721
+ windowsVirtualKeyCode: spec.keyCode,
722
+ nativeVirtualKeyCode: spec.keyCode,
723
+ modifiers: modBits
724
+ };
725
+ if (spec.text && !suppressText) {
726
+ down.text = spec.text;
727
+ }
728
+ await conn.send("Input.dispatchKeyEvent", down);
729
+ await conn.send("Input.dispatchKeyEvent", {
730
+ type: "keyUp",
731
+ key: spec.key,
732
+ code: spec.code,
733
+ windowsVirtualKeyCode: spec.keyCode,
734
+ nativeVirtualKeyCode: spec.keyCode,
735
+ modifiers: modBits
736
+ });
737
+ return { pressed: spec.key, modifiers: mods };
738
+ });
739
+ }
740
+ async function uploadFile(args) {
741
+ const files = Array.isArray(args.files) ? args.files : [args.files];
742
+ if (!files.length || files.some((f) => typeof f !== "string" || !f.length)) {
743
+ throw new CdpError("upload_file requires { files } as a non-empty path or array of paths");
744
+ }
745
+ return withPage(args.target, async (conn) => {
746
+ const { objectId } = await resolveElement(conn, args);
747
+ await conn.send("DOM.setFileInputFiles", { files, objectId });
748
+ return { uploaded: files };
749
+ });
750
+ }
751
+
752
+ // src/tools/screenshot.ts
753
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
754
+ import { join } from "node:path";
755
+ var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
756
+ function shortId(id) {
757
+ return id.slice(0, 8);
758
+ }
759
+ function stamp() {
760
+ return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
761
+ }
762
+ async function fullPageClip(conn) {
763
+ const m = await conn.send("Page.getLayoutMetrics");
764
+ const size = m.cssContentSize ?? m.contentSize;
765
+ if (!size)
766
+ throw new CdpError("Page.getLayoutMetrics returned no content size");
767
+ return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height), scale: 1 };
768
+ }
769
+ async function elementClip(conn, uid, selector) {
770
+ let backendNodeId;
771
+ if (uid != null) {
772
+ backendNodeId = uid;
773
+ } else if (selector) {
774
+ const { root } = await conn.send("DOM.getDocument", { depth: 0 });
775
+ const { nodeId } = await conn.send("DOM.querySelector", {
776
+ nodeId: root.nodeId,
777
+ selector
778
+ });
779
+ if (!nodeId)
780
+ throw new CdpError(`selector matched no element: ${selector}`);
781
+ const desc = await conn.send("DOM.describeNode", { nodeId });
782
+ backendNodeId = desc.node.backendNodeId;
783
+ } else {
784
+ throw new CdpError("elementClip requires uid or selector");
785
+ }
786
+ try {
787
+ await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId });
788
+ } catch {}
789
+ const box = await conn.send("DOM.getBoxModel", { backendNodeId });
790
+ const q = box.model.content;
791
+ const q0 = q[0];
792
+ const q1 = q[1];
793
+ if (q0 == null || q1 == null)
794
+ throw new CdpError("DOM.getBoxModel returned an empty content quad");
795
+ const xs = [q[0], q[2], q[4], q[6]].filter((n) => n != null);
796
+ const ys = [q[1], q[3], q[5], q[7]].filter((n) => n != null);
797
+ const minX = Math.min(...xs);
798
+ const minY = Math.min(...ys);
799
+ const maxX = Math.max(...xs);
800
+ const maxY = Math.max(...ys);
801
+ const width = Math.ceil(maxX - minX);
802
+ const height = Math.ceil(maxY - minY);
803
+ if (width <= 0 || height <= 0)
804
+ throw new CdpError("resolved element has zero area");
805
+ return { x: minX, y: minY, width, height, scale: 1 };
806
+ }
807
+ async function takeScreenshot(args = {}) {
808
+ const { target, fullPage, uid, selector, savePath, returnBase64 } = args;
809
+ if (uid != null && selector)
810
+ throw new CdpError("provide exactly one of uid or selector, not both");
811
+ const format = args.format ?? "png";
812
+ const quality = format === "jpeg" ? args.quality ?? 80 : undefined;
813
+ return withPage(target, async (conn, t) => {
814
+ await conn.send("Page.enable");
815
+ const params = { format, captureBeyondViewport: true };
816
+ if (quality != null)
817
+ params.quality = quality;
818
+ if (uid != null || selector) {
819
+ params.clip = await elementClip(conn, uid, selector);
820
+ } else if (fullPage) {
821
+ params.clip = await fullPageClip(conn);
822
+ }
823
+ const { data } = await conn.send("Page.captureScreenshot", params);
824
+ const buf = Buffer.from(data, "base64");
825
+ if (buf.byteLength === 0)
826
+ throw new CdpError("Page.captureScreenshot returned empty data");
827
+ await mkdir2(ARTIFACT_DIR, { recursive: true });
828
+ const ext = format === "jpeg" ? "jpg" : "png";
829
+ const path = savePath ?? join(ARTIFACT_DIR, `screenshot-${shortId(t.id)}-${stamp()}.${ext}`);
830
+ await writeFile2(path, buf);
831
+ const result = {
832
+ path,
833
+ bytes: buf.byteLength,
834
+ format,
835
+ target: { id: t.id, url: t.url, title: t.title }
836
+ };
837
+ if (returnBase64)
838
+ result.base64 = data;
839
+ return result;
840
+ });
841
+ }
842
+
843
+ // src/tools/emulation.ts
844
+ async function clearAll(conn) {
845
+ await conn.send("Emulation.clearDeviceMetricsOverride").catch(() => {
846
+ return;
847
+ });
848
+ await conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(() => {
849
+ return;
850
+ });
851
+ await conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(() => {
852
+ return;
853
+ });
854
+ await conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(() => {
855
+ return;
856
+ });
857
+ await conn.send("Network.enable").catch(() => {
858
+ return;
859
+ });
860
+ await conn.send("Network.emulateNetworkConditions", {
861
+ offline: false,
862
+ latency: 0,
863
+ downloadThroughput: -1,
864
+ uploadThroughput: -1
865
+ }).catch(() => {
866
+ return;
867
+ });
868
+ }
869
+ async function emulate(args = {}) {
870
+ const { target } = args;
871
+ return withPage(target, async (conn, t) => {
872
+ if (args.clearOverrides) {
873
+ await clearAll(conn);
874
+ return {
875
+ target: { id: t.id, url: t.url, title: t.title },
876
+ applied: [],
877
+ cleared: true
878
+ };
879
+ }
880
+ const applied = [];
881
+ if (args.width != null || args.height != null) {
882
+ if (args.width == null || args.height == null) {
883
+ throw new CdpError("emulate device metrics require both width and height");
884
+ }
885
+ await conn.send("Emulation.setDeviceMetricsOverride", {
886
+ width: args.width,
887
+ height: args.height,
888
+ deviceScaleFactor: args.deviceScaleFactor ?? 0,
889
+ mobile: args.mobile ?? false
890
+ });
891
+ applied.push("deviceMetrics");
892
+ }
893
+ if (args.userAgent != null) {
894
+ await conn.send("Emulation.setUserAgentOverride", { userAgent: args.userAgent });
895
+ applied.push("userAgent");
896
+ }
897
+ if (args.cpuThrottlingRate != null) {
898
+ if (args.cpuThrottlingRate < 1)
899
+ throw new CdpError("cpuThrottlingRate must be >= 1");
900
+ await conn.send("Emulation.setCPUThrottlingRate", { rate: args.cpuThrottlingRate });
901
+ applied.push("cpuThrottlingRate");
902
+ }
903
+ if (args.media != null || args.mediaFeatures != null) {
904
+ await conn.send("Emulation.setEmulatedMedia", {
905
+ media: args.media ?? "",
906
+ features: args.mediaFeatures ?? []
907
+ });
908
+ applied.push("emulatedMedia");
909
+ }
910
+ if (args.networkConditions != null) {
911
+ const n = args.networkConditions;
912
+ await conn.send("Network.enable").catch(() => {
913
+ return;
914
+ });
915
+ await conn.send("Network.emulateNetworkConditions", {
916
+ offline: n.offline ?? false,
917
+ latency: n.latency ?? 0,
918
+ downloadThroughput: n.downloadThroughput ?? -1,
919
+ uploadThroughput: n.uploadThroughput ?? -1,
920
+ ...n.connectionType ? { connectionType: n.connectionType } : {}
921
+ });
922
+ applied.push("networkConditions");
923
+ }
924
+ if (applied.length === 0) {
925
+ throw new CdpError("emulate: no overrides specified (pass clearOverrides:true to reset)");
926
+ }
927
+ return { target: { id: t.id, url: t.url, title: t.title }, applied };
928
+ });
929
+ }
930
+ async function resizePage(args) {
931
+ const { target, width, height } = args;
932
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
933
+ throw new CdpError("resize_page requires positive numeric width and height");
934
+ }
935
+ return withPage(target, async (conn, t) => {
936
+ await conn.send("Emulation.setDeviceMetricsOverride", {
937
+ width,
938
+ height,
939
+ deviceScaleFactor: args.deviceScaleFactor ?? 0,
940
+ mobile: args.mobile ?? false
941
+ });
942
+ await conn.send("Runtime.enable").catch(() => {
943
+ return;
944
+ });
945
+ const { result } = await conn.send("Runtime.evaluate", {
946
+ expression: "({ w: window.innerWidth, h: window.innerHeight })",
947
+ returnByValue: true
948
+ });
949
+ const v = result.value;
950
+ return {
951
+ target: { id: t.id, url: t.url, title: t.title },
952
+ width,
953
+ height,
954
+ innerWidth: v?.w ?? width,
955
+ innerHeight: v?.h ?? height
956
+ };
957
+ });
958
+ }
959
+
960
+ // src/tools/dialogs.ts
961
+ function armDialogHandler(conn, accept, promptText, onHandled) {
962
+ return conn.on("Page.javascriptDialogOpening", (params) => {
963
+ const ev = params;
964
+ (async () => {
965
+ try {
966
+ await conn.send("Page.handleJavaScriptDialog", {
967
+ accept,
968
+ ...promptText !== undefined ? { promptText } : {}
969
+ });
970
+ onHandled({
971
+ type: ev.type,
972
+ message: ev.message,
973
+ url: ev.url,
974
+ ...ev.defaultPrompt !== undefined ? { defaultPrompt: ev.defaultPrompt } : {},
975
+ accept,
976
+ ...promptText !== undefined ? { promptText } : {},
977
+ handled: true
978
+ });
979
+ } catch {}
980
+ })();
981
+ });
982
+ }
983
+ async function handleDialog(args) {
984
+ const { target, accept, promptText, timeoutMs = 15000, autoMs } = args;
985
+ const { conn } = await openPage(target);
986
+ try {
987
+ await conn.send("Page.enable");
988
+ if (autoMs !== undefined) {
989
+ const handled = [];
990
+ const disarm = armDialogHandler(conn, accept, promptText, (d) => handled.push(d));
991
+ await new Promise((resolve) => setTimeout(resolve, autoMs));
992
+ disarm();
993
+ return { handled, count: handled.length };
994
+ }
995
+ return await new Promise((resolve, reject) => {
996
+ const timer = setTimeout(() => {
997
+ disarm();
998
+ reject(new CdpError(`handle_dialog: no dialog opened within ${timeoutMs}ms`));
999
+ }, timeoutMs);
1000
+ const disarm = armDialogHandler(conn, accept, promptText, (d) => {
1001
+ clearTimeout(timer);
1002
+ disarm();
1003
+ resolve(d);
1004
+ });
1005
+ });
1006
+ } finally {
1007
+ conn.close();
1008
+ }
1009
+ }
1010
+
1011
+ // src/tools/console.ts
1012
+ import { readFile } from "node:fs/promises";
1013
+
1014
+ // src/tools/recorder.ts
1015
+ import { mkdir as mkdir3, appendFile, writeFile as writeFile3, copyFile } from "node:fs/promises";
1016
+ var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1017
+ function recFile(targetId) {
1018
+ return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
1019
+ }
1020
+ function captureFile(targetId, captureId) {
1021
+ return `${ARTIFACT_DIR2}/rec-${targetId}-${captureId}.jsonl`;
1022
+ }
1023
+ function delay2(ms) {
1024
+ return new Promise((r) => setTimeout(r, ms));
1025
+ }
1026
+ var NETWORK_EVENTS = [
1027
+ "Network.requestWillBeSent",
1028
+ "Network.responseReceived",
1029
+ "Network.loadingFinished",
1030
+ "Network.loadingFailed"
1031
+ ];
1032
+ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Log.entryAdded"];
1033
+ async function startRecorder(target, opts = {}) {
1034
+ const network = opts.network ?? true;
1035
+ const consoleCap = opts.console ?? true;
1036
+ await mkdir3(ARTIFACT_DIR2, { recursive: true });
1037
+ const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
1038
+ const file = opts.file ?? recFile(resolved.id);
1039
+ if (opts.truncate) {
1040
+ await writeFile3(file, "");
1041
+ }
1042
+ let chain = Promise.resolve();
1043
+ let dropped = 0;
1044
+ const persist = (kind, method, params) => {
1045
+ const line = { kind, method, ts: Date.now(), params };
1046
+ const text = `${JSON.stringify(line)}
1047
+ `;
1048
+ chain = chain.then(() => appendFile(file, text)).catch(() => {
1049
+ dropped += 1;
1050
+ });
1051
+ };
1052
+ const unsubs = [];
1053
+ try {
1054
+ if (network) {
1055
+ await conn.send("Network.enable");
1056
+ for (const ev of NETWORK_EVENTS) {
1057
+ unsubs.push(conn.on(ev, (params) => persist("network", ev, params)));
1058
+ }
1059
+ }
1060
+ if (consoleCap) {
1061
+ await conn.send("Runtime.enable");
1062
+ try {
1063
+ await conn.send("Log.enable");
1064
+ } catch {}
1065
+ for (const ev of CONSOLE_EVENTS) {
1066
+ unsubs.push(conn.on(ev, (params) => persist("console", ev, params)));
1067
+ }
1068
+ }
1069
+ } catch (err) {
1070
+ conn.close();
1071
+ throw err;
1072
+ }
1073
+ const flush = async () => {
1074
+ for (const off of unsubs)
1075
+ off();
1076
+ await chain;
1077
+ };
1078
+ return {
1079
+ file,
1080
+ target: resolved,
1081
+ conn,
1082
+ droppedWrites: () => dropped,
1083
+ flush,
1084
+ async stop() {
1085
+ await flush();
1086
+ conn.close();
1087
+ }
1088
+ };
1089
+ }
1090
+ async function captureWindow(target, durationMs, opts = {}) {
1091
+ const resolved = await resolveTarget(target);
1092
+ const captureId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1093
+ const file = captureFile(resolved.id, captureId);
1094
+ const rec = await startRecorder(resolved.id, {
1095
+ network: true,
1096
+ console: true,
1097
+ truncate: true,
1098
+ file,
1099
+ timeoutMs: opts.timeoutMs
1100
+ });
1101
+ await rec.conn.send("Page.enable");
1102
+ await rec.conn.send("Page.reload", { ignoreCache: false });
1103
+ await delay2(durationMs);
1104
+ return {
1105
+ file: rec.file,
1106
+ conn: rec.conn,
1107
+ resolved: { id: rec.target.id, url: rec.target.url, title: rec.target.title },
1108
+ droppedWrites: rec.droppedWrites,
1109
+ flush: () => rec.flush(),
1110
+ async stop() {
1111
+ await rec.stop();
1112
+ try {
1113
+ await copyFile(rec.file, recFile(rec.target.id));
1114
+ } catch {}
1115
+ }
1116
+ };
1117
+ }
1118
+
1119
+ // src/tools/console.ts
1120
+ var DEFAULT_CAPTURE_MS = 2500;
1121
+ function remoteObjectToText(arg) {
1122
+ if (!arg)
1123
+ return "";
1124
+ if (arg.value !== undefined && arg.value !== null)
1125
+ return String(arg.value);
1126
+ if (typeof arg.description === "string")
1127
+ return arg.description;
1128
+ if (typeof arg.unserializableValue === "string")
1129
+ return arg.unserializableValue;
1130
+ if (arg.type === "undefined")
1131
+ return "undefined";
1132
+ if (arg.type === "object" && arg.subtype === "null")
1133
+ return "null";
1134
+ if (typeof arg.type === "string")
1135
+ return `[${arg.type}]`;
1136
+ return "";
1137
+ }
1138
+ function lineToEntry(line, index) {
1139
+ const p = line.params;
1140
+ if (line.method === "Runtime.consoleAPICalled") {
1141
+ const args = Array.isArray(p.args) ? p.args : [];
1142
+ return {
1143
+ index,
1144
+ source: "console",
1145
+ level: typeof p.type === "string" ? p.type : "log",
1146
+ text: args.map(remoteObjectToText).join(" "),
1147
+ url: undefined,
1148
+ ts: line.ts
1149
+ };
1150
+ }
1151
+ if (line.method === "Runtime.exceptionThrown") {
1152
+ const detail = p.exceptionDetails ?? {};
1153
+ const exception = detail.exception ?? {};
1154
+ const text = remoteObjectToText(exception) || (typeof detail.text === "string" ? detail.text : "uncaught exception");
1155
+ return {
1156
+ index,
1157
+ source: "exception",
1158
+ level: "error",
1159
+ text,
1160
+ url: typeof detail.url === "string" ? detail.url : undefined,
1161
+ lineNumber: typeof detail.lineNumber === "number" ? detail.lineNumber : undefined,
1162
+ ts: line.ts
1163
+ };
1164
+ }
1165
+ if (line.method === "Log.entryAdded") {
1166
+ const entry = p.entry ?? {};
1167
+ return {
1168
+ index,
1169
+ source: "log",
1170
+ level: typeof entry.level === "string" ? entry.level : "info",
1171
+ text: typeof entry.text === "string" ? entry.text : "",
1172
+ url: typeof entry.url === "string" ? entry.url : undefined,
1173
+ lineNumber: typeof entry.lineNumber === "number" ? entry.lineNumber : undefined,
1174
+ ts: line.ts
1175
+ };
1176
+ }
1177
+ return null;
1178
+ }
1179
+ async function readConsoleEntries(file) {
1180
+ let raw;
1181
+ try {
1182
+ raw = await readFile(file, "utf8");
1183
+ } catch {
1184
+ return [];
1185
+ }
1186
+ const out = [];
1187
+ for (const rawLine of raw.split(`
1188
+ `)) {
1189
+ const trimmed = rawLine.trim();
1190
+ if (!trimmed)
1191
+ continue;
1192
+ let line;
1193
+ try {
1194
+ line = JSON.parse(trimmed);
1195
+ } catch {
1196
+ continue;
1197
+ }
1198
+ if (line.kind !== "console")
1199
+ continue;
1200
+ const entry = lineToEntry(line, out.length);
1201
+ if (entry)
1202
+ out.push(entry);
1203
+ }
1204
+ return out;
1205
+ }
1206
+ async function listConsoleMessages(args = {}) {
1207
+ if (args.reload) {
1208
+ const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS);
1209
+ await cap.stop();
1210
+ const messages2 = await readConsoleEntries(cap.file);
1211
+ return {
1212
+ target: cap.resolved,
1213
+ count: messages2.length,
1214
+ messages: messages2,
1215
+ droppedWrites: cap.droppedWrites()
1216
+ };
1217
+ }
1218
+ const target = await resolveTarget(args.target);
1219
+ const messages = await readConsoleEntries(recFile(target.id));
1220
+ return {
1221
+ target: { id: target.id, url: target.url, title: target.title },
1222
+ count: messages.length,
1223
+ messages
1224
+ };
1225
+ }
1226
+ async function getConsoleMessage(args = {}) {
1227
+ const target = await resolveTarget(args.target);
1228
+ const messages = await readConsoleEntries(recFile(target.id));
1229
+ const index = args.index ?? 0;
1230
+ const entry = messages[index];
1231
+ if (!entry) {
1232
+ throw new CdpError(`no console message at index ${index} (have ${messages.length}); run list_console_messages or list with reload:true first`);
1233
+ }
1234
+ return entry;
1235
+ }
1236
+
1237
+ // src/tools/network.ts
1238
+ import { readFile as readFile2 } from "node:fs/promises";
1239
+ var DEFAULT_CAPTURE_MS2 = 2500;
1240
+ function asHeaders(v) {
1241
+ if (!v || typeof v !== "object")
1242
+ return;
1243
+ const out = {};
1244
+ for (const [k, val] of Object.entries(v))
1245
+ out[k] = String(val);
1246
+ return out;
1247
+ }
1248
+ function correlate(lines) {
1249
+ const byId = new Map;
1250
+ const order = [];
1251
+ for (const line of lines) {
1252
+ if (line.kind !== "network")
1253
+ continue;
1254
+ const p = line.params;
1255
+ const requestId = typeof p.requestId === "string" ? p.requestId : undefined;
1256
+ if (!requestId)
1257
+ continue;
1258
+ if (line.method === "Network.requestWillBeSent") {
1259
+ const req = p.request ?? {};
1260
+ if (!byId.has(requestId))
1261
+ order.push(requestId);
1262
+ const existing = byId.get(requestId);
1263
+ const row = existing ?? {
1264
+ requestId,
1265
+ url: typeof req.url === "string" ? req.url : "",
1266
+ method: typeof req.method === "string" ? req.method : "GET",
1267
+ resourceType: typeof p.type === "string" ? p.type : undefined,
1268
+ requestHeaders: asHeaders(req.headers),
1269
+ state: "pending",
1270
+ ts: line.ts
1271
+ };
1272
+ byId.set(requestId, row);
1273
+ } else if (line.method === "Network.responseReceived") {
1274
+ const resp = p.response ?? {};
1275
+ const row = byId.get(requestId);
1276
+ if (row) {
1277
+ row.status = typeof resp.status === "number" ? resp.status : row.status;
1278
+ row.statusText = typeof resp.statusText === "string" ? resp.statusText : row.statusText;
1279
+ row.mimeType = typeof resp.mimeType === "string" ? resp.mimeType : row.mimeType;
1280
+ row.responseHeaders = asHeaders(resp.headers) ?? row.responseHeaders;
1281
+ if (typeof p.type === "string")
1282
+ row.resourceType = p.type;
1283
+ }
1284
+ } else if (line.method === "Network.loadingFinished") {
1285
+ const row = byId.get(requestId);
1286
+ if (row) {
1287
+ row.state = "finished";
1288
+ if (typeof p.encodedDataLength === "number")
1289
+ row.encodedDataLength = p.encodedDataLength;
1290
+ }
1291
+ } else if (line.method === "Network.loadingFailed") {
1292
+ const row = byId.get(requestId);
1293
+ if (row) {
1294
+ row.state = "failed";
1295
+ if (typeof p.errorText === "string")
1296
+ row.errorText = p.errorText;
1297
+ }
1298
+ }
1299
+ }
1300
+ return order.map((id) => byId.get(id)).filter(Boolean);
1301
+ }
1302
+ async function readNetworkRequests(file) {
1303
+ let raw;
1304
+ try {
1305
+ raw = await readFile2(file, "utf8");
1306
+ } catch {
1307
+ return [];
1308
+ }
1309
+ const lines = [];
1310
+ for (const rawLine of raw.split(`
1311
+ `)) {
1312
+ const trimmed = rawLine.trim();
1313
+ if (!trimmed)
1314
+ continue;
1315
+ try {
1316
+ lines.push(JSON.parse(trimmed));
1317
+ } catch {}
1318
+ }
1319
+ return correlate(lines);
1320
+ }
1321
+ async function listNetworkRequests(args = {}) {
1322
+ let file;
1323
+ let resolved;
1324
+ let droppedWrites;
1325
+ if (args.reload) {
1326
+ const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
1327
+ await cap.stop();
1328
+ file = cap.file;
1329
+ resolved = cap.resolved;
1330
+ droppedWrites = cap.droppedWrites();
1331
+ } else {
1332
+ const target = await resolveTarget(args.target);
1333
+ file = recFile(target.id);
1334
+ resolved = { id: target.id, url: target.url, title: target.title };
1335
+ }
1336
+ let requests = await readNetworkRequests(file);
1337
+ if (args.filterUrl) {
1338
+ const needle = args.filterUrl;
1339
+ requests = requests.filter((r) => r.url.includes(needle));
1340
+ }
1341
+ return { target: resolved, count: requests.length, requests, droppedWrites };
1342
+ }
1343
+ async function getNetworkRequest(args = {}) {
1344
+ if (!args.requestId && !args.url) {
1345
+ throw new CdpError("get_network_request requires either requestId or url");
1346
+ }
1347
+ const pick = (requests) => {
1348
+ if (args.requestId)
1349
+ return requests.find((r) => r.requestId === args.requestId);
1350
+ return requests.find((r) => r.url.includes(args.url));
1351
+ };
1352
+ const describeSelector = () => args.requestId ? `requestId ${args.requestId}` : `url ~ '${args.url}'`;
1353
+ if (args.includeBody && args.url) {
1354
+ const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
1355
+ try {
1356
+ await cap.flush();
1357
+ const match2 = (await readNetworkRequests(cap.file)).find((r) => r.url.includes(args.url));
1358
+ if (!match2) {
1359
+ throw new CdpError(`no network request matched ${describeSelector()} in fresh capture`);
1360
+ }
1361
+ try {
1362
+ const { body, base64Encoded } = await cap.conn.send("Network.getResponseBody", { requestId: match2.requestId });
1363
+ return { ...match2, body, bodyBase64Encoded: base64Encoded };
1364
+ } catch {
1365
+ return { ...match2, body: undefined, bodyBase64Encoded: undefined };
1366
+ }
1367
+ } finally {
1368
+ await cap.stop();
1369
+ }
1370
+ }
1371
+ const target = await resolveTarget(args.target);
1372
+ const match = pick(await readNetworkRequests(recFile(target.id)));
1373
+ if (!match) {
1374
+ throw new CdpError(`no network request matched ${describeSelector()}; run list_network_requests with reload:true first` + (args.includeBody ? `, then fetch the body with the url selector (reload re-mints requestIds)` : ``));
1375
+ }
1376
+ if (args.includeBody && args.requestId) {
1377
+ return {
1378
+ ...match,
1379
+ body: undefined,
1380
+ bodyUnavailableReason: "response bodies are only fetchable via a fresh reload capture, which re-mints requestIds; re-request with the `url` selector to retrieve the body"
1381
+ };
1382
+ }
1383
+ return match;
1384
+ }
1385
+
1386
+ // src/tools/performance.ts
1387
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, stat, unlink } from "node:fs/promises";
1388
+ import { join as join2 } from "node:path";
1389
+ var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1390
+ var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
1391
+ var DEFAULT_CATEGORIES = [
1392
+ "devtools.timeline",
1393
+ "blink.user_timing",
1394
+ "loading",
1395
+ "disabled-by-default-devtools.timeline",
1396
+ "disabled-by-default-devtools.timeline.frame"
1397
+ ];
1398
+ var liveTraces = new Map;
1399
+ function stamp2() {
1400
+ return new Date().toISOString().replace(/[:.]/g, "-");
1401
+ }
1402
+ function shortId2(id) {
1403
+ return id.slice(0, 8);
1404
+ }
1405
+ function stateFilePath() {
1406
+ return join2(STATE_DIR2, "trace-state.json");
1407
+ }
1408
+ function bufferTraceData(conn, sink) {
1409
+ return conn.on("Tracing.dataCollected", (params) => {
1410
+ const value = params.value;
1411
+ if (Array.isArray(value))
1412
+ sink.push(...value);
1413
+ });
1414
+ }
1415
+ async function beginTrace(conn, categories) {
1416
+ await conn.send("Tracing.start", {
1417
+ traceConfig: { includedCategories: categories },
1418
+ transferMode: "ReportEvents"
1419
+ });
1420
+ }
1421
+ async function endTrace(conn, sink, timeoutMs = 30000) {
1422
+ const complete = conn.waitFor("Tracing.tracingComplete", undefined, timeoutMs);
1423
+ await conn.send("Tracing.end");
1424
+ await complete;
1425
+ return sink;
1426
+ }
1427
+ async function writeTrace(events, targetId) {
1428
+ await mkdir4(ARTIFACT_DIR3, { recursive: true });
1429
+ const path = join2(ARTIFACT_DIR3, `trace-${shortId2(targetId)}-${stamp2()}.json`);
1430
+ const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
1431
+ await writeFile4(path, body, "utf8");
1432
+ const { size } = await stat(path);
1433
+ return { path, bytes: size };
1434
+ }
1435
+ async function performanceStartTrace(args = {}) {
1436
+ const categories = args.categories?.length ? args.categories : DEFAULT_CATEGORIES;
1437
+ const { conn, target } = await openPage(args.target);
1438
+ if (liveTraces.has(target.id)) {
1439
+ conn.close();
1440
+ throw new CdpError(`a trace is already in progress for target ${shortId2(target.id)}; call performanceStopTrace first`);
1441
+ }
1442
+ const events = [];
1443
+ bufferTraceData(conn, events);
1444
+ try {
1445
+ await beginTrace(conn, categories);
1446
+ } catch (err) {
1447
+ conn.close();
1448
+ throw err;
1449
+ }
1450
+ liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
1451
+ await mkdir4(STATE_DIR2, { recursive: true });
1452
+ const state = {
1453
+ targetId: target.id,
1454
+ url: target.url,
1455
+ startedAt: Date.now(),
1456
+ pid: process.pid,
1457
+ categories
1458
+ };
1459
+ await writeFile4(stateFilePath(), JSON.stringify(state), "utf8");
1460
+ return {
1461
+ target: { id: target.id, url: target.url, title: target.title },
1462
+ categories,
1463
+ note: "Trace recording. Call performanceStopTrace in THIS process to finalize. Cross-process? use performanceTrace instead."
1464
+ };
1465
+ }
1466
+ async function performanceStopTrace(args = {}) {
1467
+ args.target;
1468
+ const entry = liveTraces.size === 1 ? [...liveTraces.values()][0] : undefined;
1469
+ if (!entry) {
1470
+ let hint = "";
1471
+ try {
1472
+ const raw = await readFile3(stateFilePath(), "utf8");
1473
+ const state = JSON.parse(raw);
1474
+ hint = state.pid === process.pid ? ` (state file lists target ${shortId2(state.targetId)} but its live connection is gone)` : ` (trace was started by a DIFFERENT process pid=${state.pid}; live recordings cannot cross process boundaries)`;
1475
+ } catch {}
1476
+ throw new CdpError(`no in-process trace to stop${hint}. Use performanceTrace for a robust single-call trace.`);
1477
+ }
1478
+ const { conn, target, events } = entry;
1479
+ try {
1480
+ await endTrace(conn, events);
1481
+ } finally {
1482
+ conn.close();
1483
+ liveTraces.delete(target.id);
1484
+ await unlink(stateFilePath()).catch(() => {});
1485
+ }
1486
+ const written = await writeTrace(events, target.id);
1487
+ const metrics = analyzeEvents(events);
1488
+ return { ...written, events: events.length, metrics };
1489
+ }
1490
+ async function performanceTrace(args = {}) {
1491
+ const durationMs = args.durationMs ?? 3000;
1492
+ const categories = args.categories?.length ? args.categories : DEFAULT_CATEGORIES;
1493
+ const { conn, target } = await openPage(args.target);
1494
+ const events = [];
1495
+ bufferTraceData(conn, events);
1496
+ try {
1497
+ await conn.send("Page.enable");
1498
+ await beginTrace(conn, categories);
1499
+ if (args.navigateTo) {
1500
+ const loaded = conn.waitFor("Page.loadEventFired", undefined, durationMs + 5000);
1501
+ await conn.send("Page.navigate", { url: args.navigateTo });
1502
+ await loaded.catch(() => {});
1503
+ } else if (args.reload) {
1504
+ const loaded = conn.waitFor("Page.loadEventFired", undefined, durationMs + 5000);
1505
+ await conn.send("Page.reload", { ignoreCache: false });
1506
+ await loaded.catch(() => {});
1507
+ }
1508
+ await new Promise((r) => setTimeout(r, durationMs));
1509
+ await endTrace(conn, events);
1510
+ } finally {
1511
+ conn.close();
1512
+ }
1513
+ const written = await writeTrace(events, target.id);
1514
+ const metrics = analyzeEvents(events);
1515
+ return { ...written, events: events.length, metrics, target: { id: target.id, url: target.url } };
1516
+ }
1517
+ function analyzeEvents(events) {
1518
+ let navigationStartUs;
1519
+ let fcpUs;
1520
+ let lcpUs;
1521
+ let dclUs;
1522
+ let loadUs;
1523
+ let cls = 0;
1524
+ let layoutShifts = 0;
1525
+ let longTasks = 0;
1526
+ let longTasksTotalMs = 0;
1527
+ let totalBlockingTimeMs = 0;
1528
+ for (const e of events) {
1529
+ const name = e.name;
1530
+ if (!name)
1531
+ continue;
1532
+ if (name === "navigationStart") {
1533
+ if (typeof e.ts === "number" && (navigationStartUs === undefined || e.ts < navigationStartUs)) {
1534
+ navigationStartUs = e.ts;
1535
+ }
1536
+ continue;
1537
+ }
1538
+ if ((name === "firstContentfulPaint" || name === "FirstContentfulPaint") && typeof e.ts === "number") {
1539
+ if (fcpUs === undefined || e.ts < fcpUs)
1540
+ fcpUs = e.ts;
1541
+ continue;
1542
+ }
1543
+ if (name === "largestContentfulPaint::Candidate" && typeof e.ts === "number") {
1544
+ if (lcpUs === undefined || e.ts > lcpUs)
1545
+ lcpUs = e.ts;
1546
+ continue;
1547
+ }
1548
+ if ((name === "domContentLoadedEventEnd" || name === "DOMContentLoaded") && typeof e.ts === "number") {
1549
+ if (dclUs === undefined || e.ts > dclUs)
1550
+ dclUs = e.ts;
1551
+ continue;
1552
+ }
1553
+ if ((name === "loadEventEnd" || name === "MarkLoad") && typeof e.ts === "number") {
1554
+ if (loadUs === undefined || e.ts > loadUs)
1555
+ loadUs = e.ts;
1556
+ continue;
1557
+ }
1558
+ if (name === "LayoutShift") {
1559
+ const data = e.args?.data;
1560
+ const hadRecentInput = data?.had_recent_input === true;
1561
+ const score = typeof data?.score === "number" ? data.score : undefined;
1562
+ if (!hadRecentInput && typeof score === "number") {
1563
+ cls += score;
1564
+ layoutShifts += 1;
1565
+ }
1566
+ continue;
1567
+ }
1568
+ if (name === "RunTask" && typeof e.dur === "number") {
1569
+ const durMs = e.dur / 1000;
1570
+ if (durMs > 50) {
1571
+ longTasks += 1;
1572
+ longTasksTotalMs += durMs;
1573
+ totalBlockingTimeMs += durMs - 50;
1574
+ }
1575
+ continue;
1576
+ }
1577
+ }
1578
+ const relMs = (us) => us !== undefined && navigationStartUs !== undefined ? (us - navigationStartUs) / 1000 : undefined;
1579
+ const metrics = {
1580
+ navigationStartUs,
1581
+ fcpMs: relMs(fcpUs),
1582
+ lcpMs: relMs(lcpUs),
1583
+ domContentLoadedMs: relMs(dclUs),
1584
+ loadMs: relMs(loadUs),
1585
+ cls: layoutShifts > 0 ? Number(cls.toFixed(4)) : 0,
1586
+ totalBlockingTimeMs: Number(totalBlockingTimeMs.toFixed(2))
1587
+ };
1588
+ return {
1589
+ metrics,
1590
+ longTasks,
1591
+ longTasksTotalMs: Number(longTasksTotalMs.toFixed(2)),
1592
+ layoutShifts,
1593
+ eventCount: events.length
1594
+ };
1595
+ }
1596
+ async function performanceAnalyzeInsight(args = {}) {
1597
+ if (!args.tracePath) {
1598
+ throw new CdpError("performanceAnalyzeInsight requires { tracePath } pointing at a trace JSON file");
1599
+ }
1600
+ let raw;
1601
+ try {
1602
+ raw = await readFile3(args.tracePath, "utf8");
1603
+ } catch (err) {
1604
+ throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
1605
+ }
1606
+ let parsed;
1607
+ try {
1608
+ parsed = JSON.parse(raw);
1609
+ } catch (err) {
1610
+ throw new CdpError(`trace file '${args.tracePath}' is not valid JSON: ${err.message}`);
1611
+ }
1612
+ const events = Array.isArray(parsed) ? parsed : Array.isArray(parsed.traceEvents) ? parsed.traceEvents : (() => {
1613
+ throw new CdpError(`trace file '${args.tracePath}' has no traceEvents array`);
1614
+ })();
1615
+ return { ...analyzeEvents(events), tracePath: args.tracePath };
1616
+ }
1617
+
1618
+ // src/tools/heap.ts
1619
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
1620
+ import { join as join3 } from "node:path";
1621
+ var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1622
+ function stamp3(targetId) {
1623
+ const short = targetId.slice(0, 8);
1624
+ const iso = new Date().toISOString().replace(/[:.]/g, "-");
1625
+ return `${short}-${iso}`;
1626
+ }
1627
+ async function takeHeapsnapshot(args = {}) {
1628
+ return withPage(args.target, async (conn, target) => {
1629
+ await conn.send("HeapProfiler.enable");
1630
+ const parts = [];
1631
+ const off = conn.on("HeapProfiler.addHeapSnapshotChunk", (params) => {
1632
+ const chunk = params.chunk;
1633
+ if (typeof chunk === "string")
1634
+ parts.push(chunk);
1635
+ });
1636
+ try {
1637
+ await conn.send("HeapProfiler.takeHeapSnapshot", { reportProgress: false, captureNumericValue: false }, { timeoutMs: 120000 });
1638
+ } finally {
1639
+ off();
1640
+ }
1641
+ const snapshot = parts.join("");
1642
+ if (snapshot.length === 0) {
1643
+ throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
1644
+ }
1645
+ await mkdir5(ARTIFACT_DIR4, { recursive: true });
1646
+ const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join3(ARTIFACT_DIR4, args.savePath) : join3(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
1647
+ await writeFile5(path, snapshot, "utf8");
1648
+ const bytes = Buffer.byteLength(snapshot, "utf8");
1649
+ return {
1650
+ path,
1651
+ bytes,
1652
+ chunks: parts.length,
1653
+ target: { id: target.id, url: target.url, title: target.title }
1654
+ };
1655
+ });
1656
+ }
1657
+
1658
+ // src/tools/lighthouse.ts
1659
+ import { spawn } from "node:child_process";
1660
+ import { mkdir as mkdir6, readFile as readFile4, stat as stat2 } from "node:fs/promises";
1661
+ var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1662
+ function cdpPort() {
1663
+ const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
1664
+ const m = base.match(/:(\d+)/);
1665
+ return m?.[1] ? Number(m[1]) : 9222;
1666
+ }
1667
+ function stamp4() {
1668
+ return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
1669
+ }
1670
+ function shortHost(url) {
1671
+ try {
1672
+ const u = new URL(url);
1673
+ const h = u.hostname || u.protocol.replace(/:$/, "");
1674
+ return (h || "page").replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 24);
1675
+ } catch {
1676
+ return "page";
1677
+ }
1678
+ }
1679
+ function runProcess(cmd, args, opts) {
1680
+ return new Promise((resolve) => {
1681
+ let stdout = "";
1682
+ let stderr = "";
1683
+ let settled = false;
1684
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1685
+ const timer = setTimeout(() => {
1686
+ if (settled)
1687
+ return;
1688
+ try {
1689
+ child.kill("SIGKILL");
1690
+ } catch {}
1691
+ }, opts.timeoutMs);
1692
+ child.stdout.on("data", (d) => {
1693
+ stdout += d.toString();
1694
+ });
1695
+ child.stderr.on("data", (d) => {
1696
+ stderr += d.toString();
1697
+ });
1698
+ child.on("error", (err) => {
1699
+ if (settled)
1700
+ return;
1701
+ settled = true;
1702
+ clearTimeout(timer);
1703
+ resolve({ code: null, signal: null, stdout, stderr, spawnError: err });
1704
+ });
1705
+ child.on("close", (code, signal) => {
1706
+ if (settled)
1707
+ return;
1708
+ settled = true;
1709
+ clearTimeout(timer);
1710
+ resolve({ code, signal, stdout, stderr });
1711
+ });
1712
+ });
1713
+ }
1714
+ async function lighthouseAudit(args) {
1715
+ if (!args || typeof args.url !== "string" || args.url.trim() === "") {
1716
+ throw new CdpError("lighthouse_audit requires a non-empty 'url'");
1717
+ }
1718
+ const url = args.url.trim();
1719
+ const timeoutMs = args.timeoutMs ?? 120000;
1720
+ const port = cdpPort();
1721
+ await mkdir6(ARTIFACT_DIR5, { recursive: true });
1722
+ const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
1723
+ const formFactor = args.formFactor ?? "desktop";
1724
+ const lhArgs = [
1725
+ "--yes",
1726
+ "lighthouse",
1727
+ url,
1728
+ `--port=${port}`,
1729
+ "--output=json",
1730
+ `--output-path=${outPath}`,
1731
+ "--quiet",
1732
+ "--chrome-flags=--headless=new"
1733
+ ];
1734
+ if (formFactor === "desktop") {
1735
+ lhArgs.push("--preset=desktop");
1736
+ }
1737
+ if (Array.isArray(args.categories) && args.categories.length > 0) {
1738
+ lhArgs.push(`--only-categories=${args.categories.join(",")}`);
1739
+ }
1740
+ const res = await runProcess("npx", lhArgs, { timeoutMs });
1741
+ if (res.spawnError) {
1742
+ const code = res.spawnError.code;
1743
+ if (code === "ENOENT") {
1744
+ throw new CdpError("lighthouse_audit requires 'npx' (Node.js) on PATH to fetch and run the Lighthouse CLI; 'npx' was not found.");
1745
+ }
1746
+ throw new CdpError(`lighthouse_audit failed to spawn npx/lighthouse: ${res.spawnError.message}`);
1747
+ }
1748
+ let raw;
1749
+ try {
1750
+ raw = await readFile4(outPath, "utf8");
1751
+ } catch {
1752
+ raw = undefined;
1753
+ }
1754
+ if (raw === undefined) {
1755
+ if (res.signal === "SIGKILL") {
1756
+ throw new CdpError(`lighthouse_audit timed out after ${timeoutMs}ms (no report written). stderr: ${truncate(res.stderr)}`);
1757
+ }
1758
+ const hint = /not found|could not determine executable|npm ERR|E404|ERR_MODULE/i.test(res.stderr) ? " The Lighthouse CLI could not be fetched/installed via npx — ensure network access and that 'lighthouse' is installable." : "";
1759
+ throw new CdpError(`lighthouse_audit produced no report (exit code ${res.code}).${hint} stderr: ${truncate(res.stderr)}`);
1760
+ }
1761
+ let report;
1762
+ try {
1763
+ report = JSON.parse(raw);
1764
+ } catch (e) {
1765
+ throw new CdpError(`lighthouse_audit wrote a report that is not valid JSON (${e.message})`);
1766
+ }
1767
+ if (report.runtimeError?.code && report.runtimeError.code !== "NO_ERROR") {
1768
+ throw new CdpError(`lighthouse_audit runtime error [${report.runtimeError.code}]: ${report.runtimeError.message ?? "unknown"}`);
1769
+ }
1770
+ const categories = {};
1771
+ for (const [key, cat] of Object.entries(report.categories ?? {})) {
1772
+ categories[key] = cat?.score ?? null;
1773
+ }
1774
+ if (Object.keys(categories).length === 0) {
1775
+ throw new CdpError(`lighthouse_audit report contained no category scores (exit code ${res.code}). stderr: ${truncate(res.stderr)}`);
1776
+ }
1777
+ const { size } = await stat2(outPath);
1778
+ return {
1779
+ path: outPath,
1780
+ bytes: size,
1781
+ url,
1782
+ finalUrl: report.finalDisplayedUrl ?? report.finalUrl,
1783
+ lighthouseVersion: report.lighthouseVersion,
1784
+ fetchTime: report.fetchTime,
1785
+ categories,
1786
+ warnings: Array.isArray(report.runWarnings) && report.runWarnings.length > 0 ? report.runWarnings : undefined
1787
+ };
1788
+ }
1789
+ function truncate(s, max = 600) {
1790
+ const t = s.trim();
1791
+ return t.length > max ? `${t.slice(0, max)}…` : t;
1792
+ }
1793
+
1794
+ // src/tools/network_mock.ts
1795
+ function escapeRegExpChar(ch) {
1796
+ return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
1797
+ }
1798
+ function globToRegExp(pattern) {
1799
+ let re = "";
1800
+ for (let i = 0;i < pattern.length; i++) {
1801
+ const ch = pattern[i];
1802
+ if (ch === "\\") {
1803
+ const next = pattern[i + 1];
1804
+ if (next !== undefined) {
1805
+ re += escapeRegExpChar(next);
1806
+ i++;
1807
+ } else {
1808
+ re += "\\\\";
1809
+ }
1810
+ } else if (ch === "*") {
1811
+ re += ".*";
1812
+ } else if (ch === "?") {
1813
+ re += ".";
1814
+ } else {
1815
+ re += escapeRegExpChar(ch);
1816
+ }
1817
+ }
1818
+ return new RegExp(`^${re}$`);
1819
+ }
1820
+ function urlMatches(pattern, url) {
1821
+ return globToRegExp(pattern).test(url);
1822
+ }
1823
+ function selectRule(rules, url, method) {
1824
+ return rules.find((r) => urlMatches(r.urlPattern, url) && (!r.method || r.method.toUpperCase() === method.toUpperCase()));
1825
+ }
1826
+ function buildFulfillParams(requestId, rule) {
1827
+ const merged = new Map;
1828
+ merged.set("content-type", { name: "Content-Type", value: rule.contentType ?? "application/json" });
1829
+ for (const [name, value] of Object.entries(rule.headers ?? {})) {
1830
+ merged.set(name.toLowerCase(), { name, value });
1831
+ }
1832
+ return {
1833
+ requestId,
1834
+ responseCode: rule.status ?? 200,
1835
+ responseHeaders: [...merged.values()],
1836
+ body: Buffer.from(rule.body ?? "", "utf8").toString("base64")
1837
+ };
1838
+ }
1839
+ function effectiveAction(rule, rnd) {
1840
+ if (rule.failRate !== undefined && rule.failRate > 0 && rnd < rule.failRate)
1841
+ return "fail";
1842
+ return rule.action;
1843
+ }
1844
+ var sessions = new Map;
1845
+ function delay3(ms) {
1846
+ return new Promise((r) => setTimeout(r, ms));
1847
+ }
1848
+ function registerHandler(session) {
1849
+ session.conn.on("Fetch.requestPaused", (params) => {
1850
+ const p = params;
1851
+ session.chain = session.chain.then(async () => {
1852
+ const rule = selectRule(session.rules, p.request.url, p.request.method);
1853
+ if (!rule) {
1854
+ await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
1855
+ return;
1856
+ }
1857
+ if (rule.delayMs)
1858
+ await delay3(rule.delayMs);
1859
+ const act = effectiveAction(rule, Math.random());
1860
+ if (act === "fulfill") {
1861
+ await session.conn.send("Fetch.fulfillRequest", { ...buildFulfillParams(p.requestId, rule) });
1862
+ } else if (act === "fail") {
1863
+ await session.conn.send("Fetch.failRequest", { requestId: p.requestId, errorReason: rule.errorReason ?? "Failed" });
1864
+ } else {
1865
+ await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
1866
+ }
1867
+ rule.hits++;
1868
+ session.intercepted.push({ url: p.request.url, method: p.request.method, action: act });
1869
+ }).catch((e) => {
1870
+ if (/closed|connection|Target|Inspected/i.test(String(e)))
1871
+ session.dead = true;
1872
+ });
1873
+ });
1874
+ }
1875
+ async function mockRequest(args) {
1876
+ if (!args.urlPattern)
1877
+ throw new CdpError("mock_request requires a { urlPattern }");
1878
+ const rule = {
1879
+ urlPattern: args.urlPattern,
1880
+ action: args.action ?? "fulfill",
1881
+ status: args.status,
1882
+ body: args.body,
1883
+ contentType: args.contentType,
1884
+ headers: args.headers,
1885
+ errorReason: args.errorReason,
1886
+ method: args.method,
1887
+ delayMs: args.delayMs,
1888
+ failRate: args.failRate,
1889
+ hits: 0
1890
+ };
1891
+ const resolved = await resolveTarget(args.target);
1892
+ let session = sessions.get(resolved.id);
1893
+ if (session && !session.dead) {
1894
+ const idx = session.rules.findIndex((r) => r.urlPattern === rule.urlPattern && (r.method ?? "") === (rule.method ?? ""));
1895
+ if (idx >= 0)
1896
+ session.rules[idx] = rule;
1897
+ else
1898
+ session.rules.push(rule);
1899
+ await session.conn.send("Fetch.enable", { patterns: session.rules.map((r) => ({ urlPattern: r.urlPattern })) });
1900
+ } else {
1901
+ const { conn, target } = await openPage(resolved.id);
1902
+ session = { conn, target, rules: [rule], intercepted: [], chain: Promise.resolve(), dead: false };
1903
+ registerHandler(session);
1904
+ await conn.send("Fetch.enable", { patterns: [{ urlPattern: rule.urlPattern }] });
1905
+ sessions.set(target.id, session);
1906
+ }
1907
+ let reloaded = false;
1908
+ if (args.reload) {
1909
+ await session.conn.send("Page.enable");
1910
+ await session.conn.send("Page.reload", { ignoreCache: true });
1911
+ await delay3(800);
1912
+ reloaded = true;
1913
+ }
1914
+ return {
1915
+ target: { id: session.target.id, url: session.target.url, title: session.target.title },
1916
+ pattern: rule.urlPattern,
1917
+ ruleCount: session.rules.length,
1918
+ reloaded
1919
+ };
1920
+ }
1921
+ async function listMocks(_args = {}) {
1922
+ for (const [id, s] of sessions) {
1923
+ if (s.dead) {
1924
+ try {
1925
+ s.conn.close();
1926
+ } catch {}
1927
+ sessions.delete(id);
1928
+ continue;
1929
+ }
1930
+ try {
1931
+ await s.conn.send("Runtime.evaluate", { expression: "1", returnByValue: true }, { timeoutMs: 3000 });
1932
+ } catch {
1933
+ try {
1934
+ s.conn.close();
1935
+ } catch {}
1936
+ sessions.delete(id);
1937
+ }
1938
+ }
1939
+ const mocks = [...sessions.values()].map((s) => ({
1940
+ target: { id: s.target.id, url: s.target.url, title: s.target.title },
1941
+ rules: s.rules.map((r) => ({ urlPattern: r.urlPattern, action: r.action, method: r.method, hits: r.hits })),
1942
+ hits: s.intercepted.length
1943
+ }));
1944
+ return { count: mocks.length, mocks };
1945
+ }
1946
+ async function clearMocks(args = {}) {
1947
+ let ids;
1948
+ if (args.all) {
1949
+ ids = [...sessions.keys()];
1950
+ } else {
1951
+ const resolved = await resolveTarget(args.target);
1952
+ ids = sessions.has(resolved.id) ? [resolved.id] : [];
1953
+ }
1954
+ let cleared = 0;
1955
+ for (const id of ids) {
1956
+ const s = sessions.get(id);
1957
+ if (!s)
1958
+ continue;
1959
+ try {
1960
+ await s.conn.send("Fetch.disable");
1961
+ } catch {}
1962
+ try {
1963
+ s.conn.close();
1964
+ } catch {}
1965
+ sessions.delete(id);
1966
+ cleared++;
1967
+ }
1968
+ return { cleared };
1969
+ }
1970
+
1971
+ // src/index.ts
1972
+ var TOOLS = {
1973
+ list_pages: listPages,
1974
+ new_page: newPage,
1975
+ close_page: closePage,
1976
+ select_page: selectPage,
1977
+ navigate_page: navigatePage,
1978
+ wait_for: waitForText,
1979
+ evaluate_script: evaluateScript,
1980
+ take_snapshot: takeSnapshot,
1981
+ click,
1982
+ hover,
1983
+ drag,
1984
+ fill,
1985
+ fill_form: fillForm,
1986
+ type_text: typeText,
1987
+ press_key: pressKey,
1988
+ upload_file: uploadFile,
1989
+ take_screenshot: takeScreenshot,
1990
+ emulate,
1991
+ resize_page: resizePage,
1992
+ handle_dialog: handleDialog,
1993
+ list_console_messages: listConsoleMessages,
1994
+ get_console_message: getConsoleMessage,
1995
+ list_network_requests: listNetworkRequests,
1996
+ get_network_request: getNetworkRequest,
1997
+ performance_start_trace: performanceStartTrace,
1998
+ performance_stop_trace: performanceStopTrace,
1999
+ performance_analyze_insight: performanceAnalyzeInsight,
2000
+ performance_trace: performanceTrace,
2001
+ take_heapsnapshot: takeHeapsnapshot,
2002
+ lighthouse_audit: lighthouseAudit,
2003
+ mock_request: mockRequest,
2004
+ list_mocks: listMocks,
2005
+ clear_mocks: clearMocks
2006
+ };
2007
+ var TOOL_NAMES = Object.keys(TOOLS);
2008
+
2009
+ // src/mcp.ts
2010
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2011
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2012
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2013
+
2014
+ // src/manifest.ts
2015
+ var MANIFEST = [
2016
+ {
2017
+ name: "list_pages",
2018
+ description: "Enumerate browser page targets via the CDP browser endpoint (GET /json/list). By default returns only page-type tabs; set 'all' to also include workers and background pages. Each entry carries the targetId used as a target selector elsewhere.",
2019
+ inputSchema: {
2020
+ type: "object",
2021
+ properties: {
2022
+ all: {
2023
+ type: "boolean",
2024
+ description: "Include non-page targets (service/shared workers, background pages) when true; otherwise only page-type tabs are listed."
2025
+ }
2026
+ },
2027
+ required: [],
2028
+ additionalProperties: false
2029
+ }
2030
+ },
2031
+ {
2032
+ name: "new_page",
2033
+ description: "Open a new browser tab via Target.createTarget and return {targetId,url}. Defaults to about:blank; navigation is not awaited here (use navigate_page to load and wait).",
2034
+ inputSchema: {
2035
+ type: "object",
2036
+ properties: {
2037
+ url: {
2038
+ type: "string",
2039
+ description: "URL to open in the new tab; defaults to about:blank."
2040
+ }
2041
+ },
2042
+ required: [],
2043
+ additionalProperties: false
2044
+ }
2045
+ },
2046
+ {
2047
+ name: "close_page",
2048
+ description: "Close a page target via Target.closeTarget. Requires an explicit, resolvable target; refuses to guess and errors if the selector matches no target.",
2049
+ inputSchema: {
2050
+ type: "object",
2051
+ properties: {
2052
+ target: {
2053
+ type: "string",
2054
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2055
+ }
2056
+ },
2057
+ required: [
2058
+ "target"
2059
+ ],
2060
+ additionalProperties: false
2061
+ }
2062
+ },
2063
+ {
2064
+ name: "select_page",
2065
+ description: "Activate/focus a page target via Target.activateTarget and persist its bare targetId to the selected-state file (CDP_STATE_DIR/selected). Requires an explicit, resolvable target.",
2066
+ inputSchema: {
2067
+ type: "object",
2068
+ properties: {
2069
+ target: {
2070
+ type: "string",
2071
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2072
+ }
2073
+ },
2074
+ required: [
2075
+ "target"
2076
+ ],
2077
+ additionalProperties: false
2078
+ }
2079
+ },
2080
+ {
2081
+ name: "navigate_page",
2082
+ description: "Navigate a target page to a URL (Page.navigate) OR reload it (reload:true) over raw CDP, then wait for the load milestone with a bounded timeout so a wedged renderer can't hang. Returns {url,frameId,waitedFor} (plus reloaded:true on a reload); waitUntil supports 'load'|'domcontentloaded' only (no 'networkidle'), and there is no auto-snapshot of the new page. Pass reload:true with ignoreCache:true for a hard reload that refetches every subresource (e.g. to pick up a freshly-deployed, non-content-hashed bundle the HTTP cache would serve stale).",
2083
+ inputSchema: {
2084
+ type: "object",
2085
+ properties: {
2086
+ target: {
2087
+ type: "string",
2088
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2089
+ },
2090
+ url: {
2091
+ type: "string",
2092
+ description: "Destination URL to navigate to. Required unless reload:true."
2093
+ },
2094
+ reload: {
2095
+ type: "boolean",
2096
+ description: "Reload the current page (Page.reload) instead of navigating to url. Default false."
2097
+ },
2098
+ ignoreCache: {
2099
+ type: "boolean",
2100
+ description: "On reload, bypass the HTTP cache (hard reload) so subresources are refetched. Ignored unless reload:true. Default false."
2101
+ },
2102
+ waitUntil: {
2103
+ type: "string",
2104
+ enum: [
2105
+ "load",
2106
+ "domcontentloaded"
2107
+ ],
2108
+ description: "Which load milestone to wait for. Defaults to 'load'. No 'networkidle' support."
2109
+ },
2110
+ timeoutMs: {
2111
+ type: "number",
2112
+ description: "Override the navigation timeout in milliseconds."
2113
+ }
2114
+ },
2115
+ additionalProperties: false
2116
+ }
2117
+ },
2118
+ {
2119
+ name: "wait_for",
2120
+ description: "Poll a target page until the given substring appears in document.body.innerText (Runtime.evaluate on a fixed interval), or throw on timeout. Text-substring waiting only — no aria/role/selector or event variants; throws rather than returning {found:false}.",
2121
+ inputSchema: {
2122
+ type: "object",
2123
+ properties: {
2124
+ target: {
2125
+ type: "string",
2126
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2127
+ },
2128
+ text: {
2129
+ type: "string",
2130
+ description: "Substring to wait for in document.body.innerText. Required."
2131
+ },
2132
+ timeoutMs: {
2133
+ type: "number",
2134
+ description: "Total time budget in milliseconds. Defaults to 15000."
2135
+ },
2136
+ pollMs: {
2137
+ type: "number",
2138
+ description: "Poll interval in milliseconds. Defaults to 250."
2139
+ }
2140
+ },
2141
+ required: [
2142
+ "text"
2143
+ ],
2144
+ additionalProperties: false
2145
+ }
2146
+ },
2147
+ {
2148
+ name: "evaluate_script",
2149
+ description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string.",
2150
+ inputSchema: {
2151
+ type: "object",
2152
+ properties: {
2153
+ target: {
2154
+ type: "string",
2155
+ description: "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring."
2156
+ },
2157
+ expression: {
2158
+ type: "string",
2159
+ description: "JavaScript to run. Evaluated as an expression when 'args' is omitted; must be a function literal (arrow or classic) whose parameters receive 'args' when 'args' is provided."
2160
+ },
2161
+ awaitPromise: {
2162
+ type: "boolean",
2163
+ description: "Await the result if it is a Promise (default true)."
2164
+ },
2165
+ args: {
2166
+ type: "array",
2167
+ description: "Positional JSON-serializable arguments to pass to the expression, treating it as a function. No live element/page handle is bound."
2168
+ }
2169
+ },
2170
+ required: [
2171
+ "expression"
2172
+ ],
2173
+ additionalProperties: false
2174
+ }
2175
+ },
2176
+ {
2177
+ name: "take_snapshot",
2178
+ description: "Capture the page's accessibility tree (Accessibility.getFullAXTree) as a compact indented text tree where each line is prefixed with [uid] — the node's CDP backendDOMNodeId. These uids are the stateless element references that every interaction tool (click/hover/fill/etc.) feeds back to resolve a live DOM node (via DOM.resolveNode({ backendNodeId: uid })), so run this first to discover uids.",
2179
+ inputSchema: {
2180
+ type: "object",
2181
+ properties: {
2182
+ target: {
2183
+ type: "string",
2184
+ description: "Target page selector: 'active' (default, = first page-type target) | 'index:N' (Nth page-type target, 0-based) | 'url:<substring>' | 'title:<substring>' | '<32-hex targetId>' (exact target by id)."
2185
+ },
2186
+ interactiveOnly: {
2187
+ type: "boolean",
2188
+ description: "When true, emit only interactive/meaningful nodes flattened into a readable list; default false returns the full hierarchical a11y tree."
2189
+ }
2190
+ },
2191
+ required: [],
2192
+ additionalProperties: false
2193
+ }
2194
+ },
2195
+ {
2196
+ name: "click",
2197
+ description: "Click an element via a synthetic mouse press/release at the element's scrolled-into-view bounding-rect center. Target the element with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector).",
2198
+ inputSchema: {
2199
+ type: "object",
2200
+ properties: {
2201
+ target: {
2202
+ type: "string",
2203
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2204
+ },
2205
+ uid: {
2206
+ type: "number",
2207
+ description: "CDP backendDOMNodeId of the element to click, obtained from take_snapshot. Provide exactly one of uid or selector."
2208
+ },
2209
+ selector: {
2210
+ type: "string",
2211
+ description: "CSS selector for the element to click (resolved via document.querySelector). Provide exactly one of uid or selector."
2212
+ },
2213
+ button: {
2214
+ type: "string",
2215
+ enum: [
2216
+ "left",
2217
+ "right",
2218
+ "middle"
2219
+ ],
2220
+ description: "Mouse button: 'left' (default), 'right', or 'middle'."
2221
+ },
2222
+ clickCount: {
2223
+ type: "number",
2224
+ description: "Number of clicks: 1 = single (default), 2 = double-click."
2225
+ }
2226
+ },
2227
+ required: [],
2228
+ additionalProperties: false
2229
+ }
2230
+ },
2231
+ {
2232
+ name: "hover",
2233
+ description: "Hover the mouse over an element by dispatching a mouseMoved event at its scrolled-into-view center, firing framework hover handlers. Target with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector).",
2234
+ inputSchema: {
2235
+ type: "object",
2236
+ properties: {
2237
+ target: {
2238
+ type: "string",
2239
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2240
+ },
2241
+ uid: {
2242
+ type: "number",
2243
+ description: "CDP backendDOMNodeId of the element to hover, obtained from take_snapshot. Provide exactly one of uid or selector."
2244
+ },
2245
+ selector: {
2246
+ type: "string",
2247
+ description: "CSS selector for the element to hover (resolved via document.querySelector). Provide exactly one of uid or selector."
2248
+ }
2249
+ },
2250
+ required: [],
2251
+ additionalProperties: false
2252
+ }
2253
+ },
2254
+ {
2255
+ name: "drag",
2256
+ description: "Drag from a source element to a destination element via synthetic mouse press/move/release (approximates HTML5 drag-and-drop; not guaranteed for every custom DnD library). 'from' and 'to' each take exactly one of uid (a CDP backendDOMNodeId from take_snapshot) or selector.",
2257
+ inputSchema: {
2258
+ type: "object",
2259
+ properties: {
2260
+ target: {
2261
+ type: "string",
2262
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2263
+ },
2264
+ from: {
2265
+ type: "object",
2266
+ description: "Source element to drag from. Provide exactly one of uid or selector.",
2267
+ properties: {
2268
+ uid: {
2269
+ type: "number",
2270
+ description: "CDP backendDOMNodeId of the source element, obtained from take_snapshot."
2271
+ },
2272
+ selector: {
2273
+ type: "string",
2274
+ description: "CSS selector for the source element (resolved via document.querySelector)."
2275
+ }
2276
+ },
2277
+ additionalProperties: false
2278
+ },
2279
+ to: {
2280
+ type: "object",
2281
+ description: "Destination element to drag to. Provide exactly one of uid or selector.",
2282
+ properties: {
2283
+ uid: {
2284
+ type: "number",
2285
+ description: "CDP backendDOMNodeId of the destination element, obtained from take_snapshot."
2286
+ },
2287
+ selector: {
2288
+ type: "string",
2289
+ description: "CSS selector for the destination element (resolved via document.querySelector)."
2290
+ }
2291
+ },
2292
+ additionalProperties: false
2293
+ }
2294
+ },
2295
+ required: [
2296
+ "from",
2297
+ "to"
2298
+ ],
2299
+ additionalProperties: false
2300
+ }
2301
+ },
2302
+ {
2303
+ name: "fill",
2304
+ description: "Fill an element by focusing it, clearing existing content, then inserting 'value' via Input.insertText (atomic paste-like commit, not per-character keystrokes). Target with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector).",
2305
+ inputSchema: {
2306
+ type: "object",
2307
+ properties: {
2308
+ target: {
2309
+ type: "string",
2310
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2311
+ },
2312
+ uid: {
2313
+ type: "number",
2314
+ description: "CDP backendDOMNodeId of the field to fill, obtained from take_snapshot. Provide exactly one of uid or selector."
2315
+ },
2316
+ selector: {
2317
+ type: "string",
2318
+ description: "CSS selector for the field to fill (resolved via document.querySelector). Provide exactly one of uid or selector."
2319
+ },
2320
+ value: {
2321
+ type: "string",
2322
+ description: "The text value to set; the field is cleared first so this overwrites existing content."
2323
+ }
2324
+ },
2325
+ required: [
2326
+ "value"
2327
+ ],
2328
+ additionalProperties: false
2329
+ }
2330
+ },
2331
+ {
2332
+ name: "fill_form",
2333
+ description: "Fill multiple form fields in one call; each field is focused, cleared, then set via Input.insertText. Each field in the non-empty 'fields' array takes exactly one of uid (a CDP backendDOMNodeId from take_snapshot) or selector, plus its string value.",
2334
+ inputSchema: {
2335
+ type: "object",
2336
+ properties: {
2337
+ target: {
2338
+ type: "string",
2339
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2340
+ },
2341
+ fields: {
2342
+ type: "array",
2343
+ description: "Non-empty array of fields to fill, each with exactly one of uid or selector plus a string value.",
2344
+ items: {
2345
+ type: "object",
2346
+ properties: {
2347
+ uid: {
2348
+ type: "number",
2349
+ description: "CDP backendDOMNodeId of the field, obtained from take_snapshot. Provide exactly one of uid or selector."
2350
+ },
2351
+ selector: {
2352
+ type: "string",
2353
+ description: "CSS selector for the field (resolved via document.querySelector). Provide exactly one of uid or selector."
2354
+ },
2355
+ value: {
2356
+ type: "string",
2357
+ description: "The text value to set; the field is cleared first so this overwrites existing content."
2358
+ }
2359
+ },
2360
+ required: [
2361
+ "value"
2362
+ ],
2363
+ additionalProperties: false
2364
+ }
2365
+ }
2366
+ },
2367
+ required: [
2368
+ "fields"
2369
+ ],
2370
+ additionalProperties: false
2371
+ }
2372
+ },
2373
+ {
2374
+ name: "type_text",
2375
+ description: "Focus an element and append 'text' via Input.insertText without clearing first (closest to typing). Target with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector).",
2376
+ inputSchema: {
2377
+ type: "object",
2378
+ properties: {
2379
+ target: {
2380
+ type: "string",
2381
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2382
+ },
2383
+ uid: {
2384
+ type: "number",
2385
+ description: "CDP backendDOMNodeId of the element to type into, obtained from take_snapshot. Provide exactly one of uid or selector."
2386
+ },
2387
+ selector: {
2388
+ type: "string",
2389
+ description: "CSS selector for the element to type into (resolved via document.querySelector). Provide exactly one of uid or selector."
2390
+ },
2391
+ text: {
2392
+ type: "string",
2393
+ description: "The text to insert; appended to existing content rather than overwriting it."
2394
+ }
2395
+ },
2396
+ required: [
2397
+ "text"
2398
+ ],
2399
+ additionalProperties: false
2400
+ }
2401
+ },
2402
+ {
2403
+ name: "press_key",
2404
+ description: "Dispatch a keyDown/keyUp pair for a single key with optional modifiers. 'key' is a named key (Enter, Tab, Escape, ArrowDown, Backspace, etc.) or a single printable character; the named-key table is curated (no F-keys/numpad/IME).",
2405
+ inputSchema: {
2406
+ type: "object",
2407
+ properties: {
2408
+ target: {
2409
+ type: "string",
2410
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2411
+ },
2412
+ key: {
2413
+ type: "string",
2414
+ description: "Key to press: a named key like 'Enter', 'Tab', 'Escape', 'ArrowDown', 'Backspace', or a single printable character. Required."
2415
+ },
2416
+ modifiers: {
2417
+ type: "array",
2418
+ description: "Optional modifier names held during the press: 'Control'/'Ctrl', 'Shift', 'Alt', 'Meta'/'Cmd'.",
2419
+ items: {
2420
+ type: "string"
2421
+ }
2422
+ }
2423
+ },
2424
+ required: [
2425
+ "key"
2426
+ ],
2427
+ additionalProperties: false
2428
+ }
2429
+ },
2430
+ {
2431
+ name: "upload_file",
2432
+ description: "Attach one or more files to an <input type=file> element via DOM.setFileInputFiles. Target the input with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector'; 'files' is an absolute path or array of absolute paths.",
2433
+ inputSchema: {
2434
+ type: "object",
2435
+ properties: {
2436
+ target: {
2437
+ type: "string",
2438
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2439
+ },
2440
+ uid: {
2441
+ type: "number",
2442
+ description: "CDP backendDOMNodeId of the file input element, obtained from take_snapshot. Provide exactly one of uid or selector."
2443
+ },
2444
+ selector: {
2445
+ type: "string",
2446
+ description: "CSS selector for the file input element (resolved via document.querySelector). Provide exactly one of uid or selector."
2447
+ },
2448
+ files: {
2449
+ type: [
2450
+ "string",
2451
+ "array"
2452
+ ],
2453
+ items: {
2454
+ type: "string"
2455
+ },
2456
+ description: "Absolute path, or array of absolute paths, to the file(s) to attach to the <input type=file>. Accepts a single string or an array of strings."
2457
+ }
2458
+ },
2459
+ required: [
2460
+ "files"
2461
+ ],
2462
+ additionalProperties: false
2463
+ }
2464
+ },
2465
+ {
2466
+ name: "take_screenshot",
2467
+ description: "Capture the viewport (default), the full scrollable page (fullPage), or a single element (uid or selector — exactly one, mutually exclusive) via raw CDP Page.captureScreenshot. Writes a PNG/JPEG under /tmp/cdp-toolkit (override with savePath) and returns {path,bytes,format,target}; raw base64 is only included when returnBase64 is set. quality applies to jpeg only.",
2468
+ inputSchema: {
2469
+ type: "object",
2470
+ properties: {
2471
+ target: {
2472
+ type: "string",
2473
+ description: "Target page selector. undefined or 'active' -> first page target; '<32-hex targetId>' -> exact target by id; 'index:N' -> Nth page target (0-based); 'url:<substring>' -> first page whose url contains substring; 'title:<substring>' -> first page whose title contains substring."
2474
+ },
2475
+ format: {
2476
+ type: "string",
2477
+ enum: [
2478
+ "png",
2479
+ "jpeg"
2480
+ ],
2481
+ description: "Image format. Defaults to png. quality applies only to jpeg."
2482
+ },
2483
+ quality: {
2484
+ type: "number",
2485
+ description: "JPEG quality 0-100 (default 80). Ignored for png."
2486
+ },
2487
+ fullPage: {
2488
+ type: "boolean",
2489
+ description: "Capture the full scrollable content height computed from Page.getLayoutMetrics, not just the viewport."
2490
+ },
2491
+ uid: {
2492
+ type: "number",
2493
+ description: "Element to clip to — a CDP backendDOMNodeId obtained from take_snapshot. Mutually exclusive with selector."
2494
+ },
2495
+ selector: {
2496
+ type: "string",
2497
+ description: "CSS selector to clip the capture to. Mutually exclusive with uid."
2498
+ },
2499
+ savePath: {
2500
+ type: "string",
2501
+ description: "Override the output file path. Default: /tmp/cdp-toolkit/screenshot-<id>-<stamp>.<ext>."
2502
+ },
2503
+ returnBase64: {
2504
+ type: "boolean",
2505
+ description: "Also return the raw base64 image bytes in the result, in addition to writing the file."
2506
+ }
2507
+ },
2508
+ required: [],
2509
+ additionalProperties: false
2510
+ }
2511
+ },
2512
+ {
2513
+ name: "emulate",
2514
+ description: "Apply any subset of Chrome emulation overrides in one call: device metrics (width/height together, plus deviceScaleFactor/mobile), userAgent, cpuThrottlingRate (>=1), emulated media type + mediaFeatures, and networkConditions. Pass clearOverrides:true to reset every override to the browser default (ignores all other fields). Non-metrics overrides are session-scoped and reset when the per-call connection closes; device-metrics overrides persist on the target until cleared or the renderer navigates/reloads.",
2515
+ inputSchema: {
2516
+ type: "object",
2517
+ properties: {
2518
+ target: {
2519
+ type: "string",
2520
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2521
+ },
2522
+ width: {
2523
+ type: "number",
2524
+ description: "Device-metrics viewport width in CSS pixels. Must be supplied together with height."
2525
+ },
2526
+ height: {
2527
+ type: "number",
2528
+ description: "Device-metrics viewport height in CSS pixels. Must be supplied together with width."
2529
+ },
2530
+ deviceScaleFactor: {
2531
+ type: "number",
2532
+ description: "Device pixel ratio (DPR) for the device-metrics override; 0 uses the platform default."
2533
+ },
2534
+ mobile: {
2535
+ type: "boolean",
2536
+ description: "Whether to emulate a mobile device (affects viewport meta handling, scrollbars, etc.) for the device-metrics override."
2537
+ },
2538
+ userAgent: {
2539
+ type: "string",
2540
+ description: "User-Agent string to override via Emulation.setUserAgentOverride."
2541
+ },
2542
+ cpuThrottlingRate: {
2543
+ type: "number",
2544
+ description: "CPU throttling multiplier: 1 = no throttle, 2 = 2x slower, etc. Must be >= 1."
2545
+ },
2546
+ media: {
2547
+ type: "string",
2548
+ description: "Emulated CSS media type: 'screen' | 'print' | '' (clear)."
2549
+ },
2550
+ mediaFeatures: {
2551
+ type: "array",
2552
+ description: "Emulated media features, e.g. [{ name: 'prefers-color-scheme', value: 'dark' }].",
2553
+ items: {
2554
+ type: "object",
2555
+ properties: {
2556
+ name: {
2557
+ type: "string",
2558
+ description: "Media feature name, e.g. 'prefers-color-scheme'."
2559
+ },
2560
+ value: {
2561
+ type: "string",
2562
+ description: "Media feature value, e.g. 'dark'."
2563
+ }
2564
+ },
2565
+ required: [
2566
+ "name",
2567
+ "value"
2568
+ ],
2569
+ additionalProperties: false
2570
+ }
2571
+ },
2572
+ networkConditions: {
2573
+ type: "object",
2574
+ description: "Network condition overrides applied via Network.emulateNetworkConditions.",
2575
+ properties: {
2576
+ offline: {
2577
+ type: "boolean",
2578
+ description: "True to simulate offline."
2579
+ },
2580
+ latency: {
2581
+ type: "number",
2582
+ description: "Additional round-trip latency in milliseconds."
2583
+ },
2584
+ downloadThroughput: {
2585
+ type: "number",
2586
+ description: "Max download throughput in bytes/sec (-1 = no limit)."
2587
+ },
2588
+ uploadThroughput: {
2589
+ type: "number",
2590
+ description: "Max upload throughput in bytes/sec (-1 = no limit)."
2591
+ },
2592
+ connectionType: {
2593
+ type: "string",
2594
+ description: "Connection type: 'none' | 'cellular2g' | 'cellular3g' | 'cellular4g' | 'bluetooth' | 'ethernet' | 'wifi' | 'wimax' | 'other'."
2595
+ }
2596
+ },
2597
+ additionalProperties: false
2598
+ },
2599
+ clearOverrides: {
2600
+ type: "boolean",
2601
+ description: "Reset every override (device metrics, UA, CPU, media, network) to the browser default; ignores all other fields when true."
2602
+ }
2603
+ },
2604
+ required: [],
2605
+ additionalProperties: false
2606
+ }
2607
+ },
2608
+ {
2609
+ name: "resize_page",
2610
+ description: "Set the page's device-metrics width/height (the narrow case of emulate), optionally with deviceScaleFactor and mobile, then verify by reading back window.innerWidth/innerHeight after the override is applied. Requires positive numeric width and height. The device-metrics override persists on the target until cleared or the renderer navigates/reloads.",
2611
+ inputSchema: {
2612
+ type: "object",
2613
+ properties: {
2614
+ target: {
2615
+ type: "string",
2616
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2617
+ },
2618
+ width: {
2619
+ type: "number",
2620
+ description: "Viewport width in CSS pixels (must be positive)."
2621
+ },
2622
+ height: {
2623
+ type: "number",
2624
+ description: "Viewport height in CSS pixels (must be positive)."
2625
+ },
2626
+ deviceScaleFactor: {
2627
+ type: "number",
2628
+ description: "Device pixel ratio (DPR); 0 uses the platform default."
2629
+ },
2630
+ mobile: {
2631
+ type: "boolean",
2632
+ description: "Whether to emulate a mobile device for the device-metrics override."
2633
+ }
2634
+ },
2635
+ required: [
2636
+ "width",
2637
+ "height"
2638
+ ],
2639
+ additionalProperties: false
2640
+ }
2641
+ },
2642
+ {
2643
+ name: "handle_dialog",
2644
+ description: "Wait for the next JavaScript dialog (alert/confirm/prompt/beforeunload) on a page and respond via Page.handleJavaScriptDialog, accepting or dismissing it. Default mode resolves with the first handled dialog or throws on timeout; set autoMs to handle every dialog opening during a fixed window and resolve with the list. The dialog must be triggered out-of-band (e.g. by clicking a button), since a blocking dialog freezes the renderer.",
2645
+ inputSchema: {
2646
+ type: "object",
2647
+ properties: {
2648
+ target: {
2649
+ type: "string",
2650
+ description: "Target page selector: 'active' (or omitted) for the first page-type target | 'index:N' (0-based) | 'url:<substring>' | 'title:<substring>' | a 32-hex <targetId>."
2651
+ },
2652
+ accept: {
2653
+ type: "boolean",
2654
+ description: "Whether to accept (OK) or dismiss (Cancel) the dialog."
2655
+ },
2656
+ promptText: {
2657
+ type: "string",
2658
+ description: "Text to enter for a prompt() dialog when accepting."
2659
+ },
2660
+ timeoutMs: {
2661
+ type: "number",
2662
+ description: "How long to wait for the next dialog in milliseconds (default 15000)."
2663
+ },
2664
+ autoMs: {
2665
+ type: "number",
2666
+ description: "Auto-handle mode: keep handling every dialog that opens for this many milliseconds, then resolve with the list of handled dialogs (an empty list is valid; never throws on 'no dialog')."
2667
+ }
2668
+ },
2669
+ required: [
2670
+ "accept"
2671
+ ],
2672
+ additionalProperties: false
2673
+ }
2674
+ },
2675
+ {
2676
+ name: "list_console_messages",
2677
+ description: "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history.",
2678
+ inputSchema: {
2679
+ type: "object",
2680
+ properties: {
2681
+ target: {
2682
+ type: "string",
2683
+ description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
2684
+ },
2685
+ reload: {
2686
+ type: "boolean",
2687
+ description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
2688
+ },
2689
+ durationMs: {
2690
+ type: "number",
2691
+ description: "Capture window for reload mode, in milliseconds. Default 2500."
2692
+ }
2693
+ },
2694
+ required: [],
2695
+ additionalProperties: false
2696
+ }
2697
+ },
2698
+ {
2699
+ name: "get_console_message",
2700
+ description: "Return a single console entry by zero-based index from the target's existing console buffer. Throws if the index is out of range — run list_console_messages (optionally with reload:true) first to populate the buffer.",
2701
+ inputSchema: {
2702
+ type: "object",
2703
+ properties: {
2704
+ target: {
2705
+ type: "string",
2706
+ description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
2707
+ },
2708
+ index: {
2709
+ type: "number",
2710
+ description: "Zero-based index into the parsed console entries. Default 0."
2711
+ }
2712
+ },
2713
+ required: [],
2714
+ additionalProperties: false
2715
+ }
2716
+ },
2717
+ {
2718
+ name: "list_network_requests",
2719
+ description: "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring.",
2720
+ inputSchema: {
2721
+ type: "object",
2722
+ properties: {
2723
+ target: {
2724
+ type: "string",
2725
+ description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
2726
+ },
2727
+ reload: {
2728
+ type: "boolean",
2729
+ description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
2730
+ },
2731
+ durationMs: {
2732
+ type: "number",
2733
+ description: "Capture window for reload mode, in milliseconds. Default 2500."
2734
+ },
2735
+ filterUrl: {
2736
+ type: "string",
2737
+ description: "Only return requests whose URL contains this substring."
2738
+ }
2739
+ },
2740
+ required: [],
2741
+ additionalProperties: false
2742
+ }
2743
+ },
2744
+ {
2745
+ name: "get_network_request",
2746
+ description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh reload capture and is matched by url ONLY (reload re-mints requestIds, so a carried-over requestId cannot fetch a body — it returns metadata plus bodyUnavailableReason).",
2747
+ inputSchema: {
2748
+ type: "object",
2749
+ properties: {
2750
+ target: {
2751
+ type: "string",
2752
+ description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
2753
+ },
2754
+ requestId: {
2755
+ type: "string",
2756
+ description: "Match by exact requestId (metadata only — cannot fetch a body, since reload re-mints requestIds)."
2757
+ },
2758
+ url: {
2759
+ type: "string",
2760
+ description: "Match by URL substring (first match). Required for body fetch (includeBody), since url is stable across reload."
2761
+ },
2762
+ includeBody: {
2763
+ type: "boolean",
2764
+ description: "Also fetch the response body; drives a fresh reload capture and must be used with the `url` selector."
2765
+ },
2766
+ durationMs: {
2767
+ type: "number",
2768
+ description: "Capture window for the reload-driven body fetch, in milliseconds. Default 2500."
2769
+ }
2770
+ },
2771
+ required: [],
2772
+ additionalProperties: false
2773
+ }
2774
+ },
2775
+ {
2776
+ name: "performance_start_trace",
2777
+ description: "Start a performance trace on the target page over raw CDP (Tracing.start) and park the recording connection in-process. Must be paired with performance_stop_trace WITHIN THE SAME PROCESS; for a robust cross-call trace use performance_trace instead. Throws if a trace is already in progress for the target.",
2778
+ inputSchema: {
2779
+ type: "object",
2780
+ properties: {
2781
+ target: {
2782
+ type: "string",
2783
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2784
+ },
2785
+ categories: {
2786
+ type: "array",
2787
+ items: {
2788
+ type: "string"
2789
+ },
2790
+ description: "Trace categories to include. Defaults to the timeline + user-timing + loading + disabled-by-default timeline tracks that carry LCP/LayoutShift/RunTask."
2791
+ }
2792
+ },
2793
+ required: [],
2794
+ additionalProperties: false
2795
+ }
2796
+ },
2797
+ {
2798
+ name: "performance_stop_trace",
2799
+ description: "Stop the in-process trace started by performance_start_trace, drain buffered Tracing.dataCollected events, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics}. Throws if no live trace exists in this process (e.g. start ran in a different process) — use performance_trace instead. The 'target' arg is accepted only for API symmetry; at most one trace is ever live per process.",
2800
+ inputSchema: {
2801
+ type: "object",
2802
+ properties: {
2803
+ target: {
2804
+ type: "string",
2805
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2806
+ }
2807
+ },
2808
+ required: [],
2809
+ additionalProperties: false
2810
+ }
2811
+ },
2812
+ {
2813
+ name: "performance_analyze_insight",
2814
+ description: "CDP-native approximation of the DevTools insight analyzer: read a trace JSON file (bare array or {traceEvents:[...]}) at the given tracePath and return headline metrics (FCP/LCP/CLS/TBT, long tasks, layout shifts). Requires an explicit tracePath returned by performance_trace/performance_stop_trace — there is no implicit 'latest trace'. Numbers approximate DevTools (no main-thread attribution or frame-scoped LCP).",
2815
+ inputSchema: {
2816
+ type: "object",
2817
+ properties: {
2818
+ tracePath: {
2819
+ type: "string",
2820
+ description: "Filesystem path to a trace JSON file written by performance_trace or performance_stop_trace (bare array OR {traceEvents:[...]}). Required."
2821
+ }
2822
+ },
2823
+ required: [],
2824
+ additionalProperties: false
2825
+ }
2826
+ },
2827
+ {
2828
+ name: "performance_trace",
2829
+ description: "PRIMARY one-shot trace: start tracing, optionally reload or navigate the page, wait durationMs (default 3000), end the trace, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics,target}. Holds one connection open for the whole window, so it is immune to the cross-process limitation of start/stop — the recommended entry point.",
2830
+ inputSchema: {
2831
+ type: "object",
2832
+ properties: {
2833
+ target: {
2834
+ type: "string",
2835
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2836
+ },
2837
+ durationMs: {
2838
+ type: "number",
2839
+ description: "How long to record after the optional reload/navigate, in milliseconds. Default 3000."
2840
+ },
2841
+ reload: {
2842
+ type: "boolean",
2843
+ description: "Reload the page after starting the trace to capture full navigation timing."
2844
+ },
2845
+ navigateTo: {
2846
+ type: "string",
2847
+ description: "Navigate to this URL after starting the trace (alternative to reload)."
2848
+ },
2849
+ categories: {
2850
+ type: "array",
2851
+ items: {
2852
+ type: "string"
2853
+ },
2854
+ description: "Trace categories to include. Defaults to the timeline + user-timing + loading + disabled-by-default timeline tracks that carry LCP/LayoutShift/RunTask."
2855
+ }
2856
+ },
2857
+ required: [],
2858
+ additionalProperties: false
2859
+ }
2860
+ },
2861
+ {
2862
+ name: "take_heapsnapshot",
2863
+ description: "Capture a V8 heap snapshot of the selected page target over raw CDP (HeapProfiler.takeHeapSnapshot, accumulating addHeapSnapshotChunk events) and write it as a .heapsnapshot JSON file loadable by the DevTools Memory panel; returns {path,bytes,chunks,target} only and does not parse/summarize the snapshot. Writes under /tmp/cdp-toolkit (CDP_ARTIFACT_DIR) unless savePath is given.",
2864
+ inputSchema: {
2865
+ type: "object",
2866
+ properties: {
2867
+ target: {
2868
+ type: "string",
2869
+ description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
2870
+ },
2871
+ savePath: {
2872
+ type: "string",
2873
+ description: "Override the output path. An absolute path (starting with /) is used as-is; a relative path is resolved under the artifact dir (/tmp/cdp-toolkit). Defaults to an auto-named take_heapsnapshot-<targetId>-<timestamp>.heapsnapshot file."
2874
+ }
2875
+ },
2876
+ required: [],
2877
+ additionalProperties: false
2878
+ }
2879
+ },
2880
+ {
2881
+ name: "lighthouse_audit",
2882
+ description: "Run a Lighthouse audit against a URL by shelling out to `npx --yes lighthouse` (the toolkit's sole non-CDP tool); Lighthouse attaches to the already-running Chrome on the remote-debugging port and audits its own about:blank tab rather than any live user tab. Writes a JSON report under /tmp/cdp-toolkit and returns {path, bytes} plus per-category scores (0..1 or null).",
2883
+ inputSchema: {
2884
+ type: "object",
2885
+ properties: {
2886
+ url: {
2887
+ type: "string",
2888
+ description: "The URL to audit. Required — never points at a user tab implicitly."
2889
+ },
2890
+ categories: {
2891
+ type: "array",
2892
+ items: {
2893
+ type: "string"
2894
+ },
2895
+ description: 'Lighthouse categories to run (passed as --only-categories). Defaults to the full set, e.g. ["performance"] or ["performance","accessibility","seo"].'
2896
+ },
2897
+ savePath: {
2898
+ type: "string",
2899
+ description: "Override the report output path; defaults to a timestamped file under /tmp/cdp-toolkit."
2900
+ },
2901
+ formFactor: {
2902
+ type: "string",
2903
+ enum: [
2904
+ "desktop",
2905
+ "mobile"
2906
+ ],
2907
+ description: 'Form factor: "desktop" (default, uses --preset=desktop to avoid heavy mobile throttling) or "mobile".'
2908
+ },
2909
+ timeoutMs: {
2910
+ type: "number",
2911
+ description: "Overall budget for the lighthouse process in milliseconds. Default 120000."
2912
+ }
2913
+ },
2914
+ required: [
2915
+ "url"
2916
+ ],
2917
+ additionalProperties: false
2918
+ }
2919
+ },
2920
+ {
2921
+ name: "mock_request",
2922
+ description: "Register a mock rule on a target's persistent fake-backend session (CDP Fetch domain): intercept requests whose URL matches urlPattern and fulfill them with a canned response, fail (abort) them, or continue them — optionally with fault injection (delayMs/failRate). The session survives reloads and navigations and lives until clear_mocks. Call repeatedly to mock several endpoints on the same target. Pass reload:true to apply immediately. Persistent across calls only via the MCP server (not the one-shot CLI).",
2923
+ inputSchema: {
2924
+ type: "object",
2925
+ properties: {
2926
+ target: {
2927
+ type: "string",
2928
+ description: "Target selector: active | index:N | url:<substr> | title:<substr> | <targetId>. Defaults to the active page."
2929
+ },
2930
+ urlPattern: {
2931
+ type: "string",
2932
+ description: `CDP Fetch urlPattern glob: '*' matches any run of characters, '?' exactly one, '\\' escapes. Only matching request URLs are paused. e.g. "*/api/users*".`
2933
+ },
2934
+ action: {
2935
+ type: "string",
2936
+ enum: [
2937
+ "fulfill",
2938
+ "fail",
2939
+ "continue"
2940
+ ],
2941
+ description: 'What to do with a matched request. Default "fulfill".'
2942
+ },
2943
+ status: {
2944
+ type: "number",
2945
+ description: "fulfill: HTTP status code for the canned response. Default 200."
2946
+ },
2947
+ body: {
2948
+ type: "string",
2949
+ description: "fulfill: response body string (base64-encoded for CDP internally)."
2950
+ },
2951
+ contentType: {
2952
+ type: "string",
2953
+ description: 'fulfill: Content-Type header. Default "application/json".'
2954
+ },
2955
+ headers: {
2956
+ type: "object",
2957
+ description: 'fulfill: extra response headers (name->value); override Content-Type case-insensitively. Add "Access-Control-Allow-Origin" for cross-origin fetches.'
2958
+ },
2959
+ errorReason: {
2960
+ type: "string",
2961
+ description: 'fail: CDP Network.ErrorReason, e.g. "Failed" (default), "BlockedByClient", "ConnectionRefused", "TimedOut".'
2962
+ },
2963
+ method: {
2964
+ type: "string",
2965
+ description: 'Only mock requests with this HTTP method (e.g. "POST"); other methods pass through.'
2966
+ },
2967
+ delayMs: {
2968
+ type: "number",
2969
+ description: "Fault injection: artificial latency in ms before responding."
2970
+ },
2971
+ failRate: {
2972
+ type: "number",
2973
+ description: "Fault injection: probability 0..1 of failing a matched request regardless of action (resilience testing)."
2974
+ },
2975
+ reload: {
2976
+ type: "boolean",
2977
+ description: "Reload the target after arming so the mock immediately catches traffic."
2978
+ }
2979
+ },
2980
+ required: [
2981
+ "urlPattern"
2982
+ ],
2983
+ additionalProperties: false
2984
+ }
2985
+ },
2986
+ {
2987
+ name: "list_mocks",
2988
+ description: "List active fake-backend sessions (one per target) with their rules and hit counts. Prunes sessions whose tab has closed. Use to see what is currently being mocked.",
2989
+ inputSchema: {
2990
+ type: "object",
2991
+ properties: {
2992
+ target: {
2993
+ type: "string",
2994
+ description: "Unused filter placeholder; list_mocks returns all active sessions."
2995
+ }
2996
+ },
2997
+ required: [],
2998
+ additionalProperties: false
2999
+ }
3000
+ },
3001
+ {
3002
+ name: "clear_mocks",
3003
+ description: "Tear down fake-backend sessions (Fetch.disable + close the connection). Clears the resolved target's session by default, or every active session with all:true.",
3004
+ inputSchema: {
3005
+ type: "object",
3006
+ properties: {
3007
+ target: {
3008
+ type: "string",
3009
+ description: "Target selector whose mock session to clear. Defaults to the active page. Ignored when all:true."
3010
+ },
3011
+ all: {
3012
+ type: "boolean",
3013
+ description: "Clear every active mock session instead of just the resolved target's."
3014
+ }
3015
+ },
3016
+ required: [],
3017
+ additionalProperties: false
3018
+ }
3019
+ }
3020
+ ];
3021
+
3022
+ // src/mcp.ts
3023
+ var VERSION = "1.0.0";
3024
+ var dispatch = TOOLS;
3025
+ function auditCoverage() {
3026
+ const manifestNames = new Set(MANIFEST.map((s) => s.name));
3027
+ const registryNames = new Set(TOOL_NAMES);
3028
+ const missingSchema = [...registryNames].filter((n) => !manifestNames.has(n));
3029
+ const orphanSchema = [...manifestNames].filter((n) => !registryNames.has(n));
3030
+ if (missingSchema.length)
3031
+ console.error(`[cdp-toolkit] WARN: tools without a manifest schema: ${missingSchema.join(", ")}`);
3032
+ if (orphanSchema.length)
3033
+ console.error(`[cdp-toolkit] WARN: manifest schemas with no registered tool: ${orphanSchema.join(", ")}`);
3034
+ }
3035
+ var server = new Server({ name: "cdp-toolkit", version: VERSION }, { capabilities: { tools: {} } });
3036
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3037
+ tools: MANIFEST.map((s) => ({ name: s.name, description: s.description, inputSchema: s.inputSchema }))
3038
+ }));
3039
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3040
+ const { name } = request.params;
3041
+ const args = request.params.arguments ?? {};
3042
+ const fn = dispatch[name];
3043
+ if (!fn) {
3044
+ return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
3045
+ }
3046
+ try {
3047
+ const result = await fn(args);
3048
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
3049
+ } catch (err) {
3050
+ const message = err instanceof Error ? err.message : String(err);
3051
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
3052
+ }
3053
+ });
3054
+ auditCoverage();
3055
+ await server.connect(new StdioServerTransport);
3056
+ console.error(`[cdp-toolkit] MCP server v${VERSION} ready — ${MANIFEST.length} tools, CDP_BASE=${BASE}`);