@mehmoodqureshi/chrome-mcp 0.6.7 → 0.7.1
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/README.md +116 -0
- package/dist/shared/observers.d.ts +110 -0
- package/dist/shared/observers.js +127 -0
- package/dist/shared/page-fns.d.ts +46 -0
- package/dist/shared/page-fns.js +292 -0
- package/dist/shared/policy.d.ts +9 -0
- package/dist/shared/policy.js +16 -0
- package/dist/shared/protocol.d.ts +11 -2
- package/dist/shared/protocol.js +3 -0
- package/dist/shared/snapshot.d.ts +2 -0
- package/dist/shared/snapshot.js +8 -1
- package/dist/src/bridge/workspace.d.ts +6 -0
- package/dist/src/bridge/workspace.js +26 -7
- package/dist/src/config.js +31 -0
- package/dist/src/executor/extension-executor.d.ts +27 -13
- package/dist/src/executor/extension-executor.js +53 -12
- package/dist/src/executor/stub-executor.d.ts +19 -1
- package/dist/src/executor/stub-executor.js +22 -6
- package/dist/src/executor/types.d.ts +72 -13
- package/dist/src/mcp/audit.d.ts +39 -0
- package/dist/src/mcp/audit.js +60 -0
- package/dist/src/mcp/helpers.d.ts +4 -4
- package/dist/src/mcp/helpers.js +9 -4
- package/dist/src/mcp/locate.d.ts +45 -0
- package/dist/src/mcp/locate.js +105 -0
- package/dist/src/mcp/redact.d.ts +48 -0
- package/dist/src/mcp/redact.js +92 -0
- package/dist/src/mcp/snapdiff.d.ts +43 -0
- package/dist/src/mcp/snapdiff.js +92 -0
- package/dist/src/mcp/tools.js +429 -43
- package/dist/src/security/policy.d.ts +5 -0
- package/dist/src/security/policy.js +6 -0
- package/docs/BLUEPRINT.md +15 -1
- package/extension-dist/background.js +617 -214
- package/extension-dist/page-hook.js +215 -0
- package/package.json +1 -1
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"wait_for",
|
|
29
29
|
"download_file",
|
|
30
30
|
"upload_file",
|
|
31
|
+
"frames_list",
|
|
32
|
+
"observers",
|
|
33
|
+
"print_pdf",
|
|
31
34
|
"ping_probe"
|
|
32
35
|
];
|
|
33
36
|
|
|
@@ -128,7 +131,13 @@
|
|
|
128
131
|
"get_text",
|
|
129
132
|
"get_html",
|
|
130
133
|
"screenshot",
|
|
131
|
-
"wait_for"
|
|
134
|
+
"wait_for",
|
|
135
|
+
// A frame list leaks the URLs a page embeds; console/network buffers are the
|
|
136
|
+
// page's own traffic and error text; a PDF is a screenshot by another name.
|
|
137
|
+
// All three are page CONTENT and gate exactly like the reads above.
|
|
138
|
+
"frames_list",
|
|
139
|
+
"observers",
|
|
140
|
+
"print_pdf"
|
|
132
141
|
]);
|
|
133
142
|
var MUTATE_CONTENT = /* @__PURE__ */ new Set([
|
|
134
143
|
"click",
|
|
@@ -193,6 +202,12 @@
|
|
|
193
202
|
if (method === "download_file" && !policy.allowDownloads) {
|
|
194
203
|
return { ok: false, reason: "downloads are disabled. Pass --enable-downloads or set allowDownloads." };
|
|
195
204
|
}
|
|
205
|
+
if (method === "observers" && !policy.allowObservers) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
reason: "the in-page observers (console_logs / network_log / dialogs) are disabled. They patch console, fetch, XMLHttpRequest and the dialog functions on every allowlisted page in your real browser, so they are opt-in: pass --enable-observers or set allowObservers."
|
|
209
|
+
};
|
|
210
|
+
}
|
|
196
211
|
if (method === "upload_file" && !policy.allowUploads) {
|
|
197
212
|
return {
|
|
198
213
|
ok: false,
|
|
@@ -366,8 +381,10 @@
|
|
|
366
381
|
const ref = `e${++n}`;
|
|
367
382
|
el.setAttribute("data-mcp-ref", ref);
|
|
368
383
|
const node = { ref, role: roleOf(el), name: accName(el), tag: el.tagName.toLowerCase() };
|
|
384
|
+
const isSecret = el.tagName === "INPUT" && el.type === "password";
|
|
385
|
+
if (isSecret) node.secret = true;
|
|
369
386
|
const v = el.value;
|
|
370
|
-
if (typeof v === "string" && v) node.value = v.slice(0, 200);
|
|
387
|
+
if (!isSecret && typeof v === "string" && v) node.value = v.slice(0, 200);
|
|
371
388
|
if (el.disabled) node.disabled = true;
|
|
372
389
|
if (el.checked) node.checked = true;
|
|
373
390
|
nodes.push(node);
|
|
@@ -375,6 +392,289 @@
|
|
|
375
392
|
return { url: location.href, title: document.title, nodes, truncated: els.length > nodes.length };
|
|
376
393
|
}
|
|
377
394
|
|
|
395
|
+
// shared/page-fns.ts
|
|
396
|
+
function pageOp(a) {
|
|
397
|
+
const deepQuery = (sel2) => {
|
|
398
|
+
const roots = [document];
|
|
399
|
+
const seen = /* @__PURE__ */ new Set();
|
|
400
|
+
while (roots.length > 0) {
|
|
401
|
+
const root = roots.shift();
|
|
402
|
+
if (seen.has(root)) continue;
|
|
403
|
+
seen.add(root);
|
|
404
|
+
let hit = null;
|
|
405
|
+
try {
|
|
406
|
+
hit = root.querySelector(sel2);
|
|
407
|
+
} catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
if (hit) return hit;
|
|
411
|
+
let hosts = [];
|
|
412
|
+
try {
|
|
413
|
+
hosts = Array.from(root.querySelectorAll("*"));
|
|
414
|
+
} catch {
|
|
415
|
+
hosts = [];
|
|
416
|
+
}
|
|
417
|
+
for (const host of hosts) {
|
|
418
|
+
const sr = host.shadowRoot;
|
|
419
|
+
if (sr && !seen.has(sr)) roots.push(sr);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
};
|
|
424
|
+
const frameOffset = () => {
|
|
425
|
+
let dx = 0;
|
|
426
|
+
let dy = 0;
|
|
427
|
+
let win = window;
|
|
428
|
+
try {
|
|
429
|
+
while (win.parent && win.parent !== win) {
|
|
430
|
+
const fe = win.frameElement;
|
|
431
|
+
if (!fe) return { dx, dy, exact: false, scrollX: window.scrollX, scrollY: window.scrollY };
|
|
432
|
+
const r = fe.getBoundingClientRect();
|
|
433
|
+
dx += r.left;
|
|
434
|
+
dy += r.top;
|
|
435
|
+
win = win.parent;
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
return { dx, dy, exact: false, scrollX: window.scrollX, scrollY: window.scrollY };
|
|
439
|
+
}
|
|
440
|
+
return { dx, dy, exact: true, scrollX: win.scrollX, scrollY: win.scrollY };
|
|
441
|
+
};
|
|
442
|
+
const sel = typeof a.selector === "string" && a.selector.length > 0 ? a.selector : null;
|
|
443
|
+
const el = sel ? deepQuery(sel) : null;
|
|
444
|
+
const missing = sel !== null && el === null;
|
|
445
|
+
const setValue = (node, next) => {
|
|
446
|
+
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), "value")?.set;
|
|
447
|
+
if (setter) setter.call(node, next);
|
|
448
|
+
else node.value = next;
|
|
449
|
+
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
450
|
+
};
|
|
451
|
+
switch (a.op) {
|
|
452
|
+
case "probe":
|
|
453
|
+
return { found: true, url: location.href, title: document.title };
|
|
454
|
+
case "text": {
|
|
455
|
+
if (missing) return { found: false };
|
|
456
|
+
const root = el ?? document.body;
|
|
457
|
+
return { found: true, text: root ? root.innerText ?? "" : "" };
|
|
458
|
+
}
|
|
459
|
+
case "html": {
|
|
460
|
+
if (missing) return { found: false };
|
|
461
|
+
const root = el ?? document.documentElement;
|
|
462
|
+
if (!root) return { found: true, html: "" };
|
|
463
|
+
const outer = a.outer === true || !sel;
|
|
464
|
+
return { found: true, html: outer ? root.outerHTML : root.innerHTML };
|
|
465
|
+
}
|
|
466
|
+
case "click": {
|
|
467
|
+
if (!el) return { found: false };
|
|
468
|
+
el.scrollIntoView({ block: "center" });
|
|
469
|
+
el.click();
|
|
470
|
+
return { found: true };
|
|
471
|
+
}
|
|
472
|
+
case "type": {
|
|
473
|
+
const node = el;
|
|
474
|
+
if (!node) return { found: false };
|
|
475
|
+
node.focus();
|
|
476
|
+
const next = (a.clear ? "" : node.value ?? "") + (a.text ?? "");
|
|
477
|
+
setValue(node, next);
|
|
478
|
+
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
479
|
+
return { found: true };
|
|
480
|
+
}
|
|
481
|
+
case "focus": {
|
|
482
|
+
const node = el;
|
|
483
|
+
if (!node) return { found: false };
|
|
484
|
+
node.focus();
|
|
485
|
+
if (a.clear) setValue(node, "");
|
|
486
|
+
return { found: true };
|
|
487
|
+
}
|
|
488
|
+
case "point": {
|
|
489
|
+
if (!el) return { found: false };
|
|
490
|
+
el.scrollIntoView({ block: "center", inline: "center" });
|
|
491
|
+
const r = el.getBoundingClientRect();
|
|
492
|
+
const off = frameOffset();
|
|
493
|
+
return {
|
|
494
|
+
found: true,
|
|
495
|
+
x: r.left + r.width / 2 + off.dx,
|
|
496
|
+
y: r.top + r.height / 2 + off.dy,
|
|
497
|
+
// false => this frame's coordinates cannot be mapped to the top viewport.
|
|
498
|
+
exact: off.exact
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
case "hover": {
|
|
502
|
+
if (!el) return { found: false };
|
|
503
|
+
el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
504
|
+
el.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
|
505
|
+
return { found: true };
|
|
506
|
+
}
|
|
507
|
+
case "select": {
|
|
508
|
+
const node = el;
|
|
509
|
+
if (!node || !node.options) return { found: false };
|
|
510
|
+
const want = new Set(a.values ?? []);
|
|
511
|
+
let matched = false;
|
|
512
|
+
for (const opt of Array.from(node.options)) {
|
|
513
|
+
const on = want.has(opt.value) || want.has(opt.label) || want.has(opt.text);
|
|
514
|
+
opt.selected = on;
|
|
515
|
+
if (on) matched = true;
|
|
516
|
+
}
|
|
517
|
+
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
518
|
+
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
519
|
+
return { found: true, matched };
|
|
520
|
+
}
|
|
521
|
+
case "measure": {
|
|
522
|
+
const d = document.documentElement;
|
|
523
|
+
const dims = {
|
|
524
|
+
w: window.innerWidth,
|
|
525
|
+
h: window.innerHeight,
|
|
526
|
+
fullW: Math.max(d.scrollWidth, d.clientWidth),
|
|
527
|
+
fullH: Math.max(d.scrollHeight, d.clientHeight)
|
|
528
|
+
};
|
|
529
|
+
if (!sel) return { found: true, dims, element: null, missing: false };
|
|
530
|
+
if (!el) return { found: true, dims, element: null, missing: true };
|
|
531
|
+
el.scrollIntoView({ block: "center", inline: "center" });
|
|
532
|
+
const r = el.getBoundingClientRect();
|
|
533
|
+
const off = frameOffset();
|
|
534
|
+
return {
|
|
535
|
+
found: true,
|
|
536
|
+
dims,
|
|
537
|
+
element: {
|
|
538
|
+
x: r.left + off.dx + off.scrollX,
|
|
539
|
+
y: r.top + off.dy + off.scrollY,
|
|
540
|
+
w: r.width,
|
|
541
|
+
h: r.height
|
|
542
|
+
},
|
|
543
|
+
missing: false,
|
|
544
|
+
exact: off.exact
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
case "scroll": {
|
|
548
|
+
if (el) el.scrollIntoView({ block: "center" });
|
|
549
|
+
else if (a.x != null || a.y != null) window.scrollTo(a.x ?? 0, a.y ?? 0);
|
|
550
|
+
else window.scrollBy(a.deltaX ?? 0, a.deltaY ?? 0);
|
|
551
|
+
return { found: !missing };
|
|
552
|
+
}
|
|
553
|
+
case "storage": {
|
|
554
|
+
const store = a.session ? window.sessionStorage : window.localStorage;
|
|
555
|
+
const op = a.storageOp;
|
|
556
|
+
if (op === "set") {
|
|
557
|
+
store.setItem(String(a.key), String(a.value ?? ""));
|
|
558
|
+
return { found: true, ok: true };
|
|
559
|
+
}
|
|
560
|
+
if (op === "remove") {
|
|
561
|
+
store.removeItem(String(a.key));
|
|
562
|
+
return { found: true, ok: true };
|
|
563
|
+
}
|
|
564
|
+
if (op === "clear") {
|
|
565
|
+
store.clear();
|
|
566
|
+
return { found: true, ok: true };
|
|
567
|
+
}
|
|
568
|
+
if (a.key) return { found: true, ok: true, value: store.getItem(a.key) };
|
|
569
|
+
const entries = {};
|
|
570
|
+
for (let i = 0; i < store.length; i++) {
|
|
571
|
+
const k = store.key(i);
|
|
572
|
+
if (k) entries[k] = store.getItem(k) ?? "";
|
|
573
|
+
}
|
|
574
|
+
return { found: true, ok: true, entries };
|
|
575
|
+
}
|
|
576
|
+
// -- the two polling ops: one injection that resolves in-page, rather than
|
|
577
|
+
// one executeScript round-trip per tick --
|
|
578
|
+
case "waitSelector": {
|
|
579
|
+
const deadline = Date.now() + (a.timeoutMs ?? 5e3);
|
|
580
|
+
const every = a.interval ?? 120;
|
|
581
|
+
return new Promise((resolve) => {
|
|
582
|
+
const tick = () => {
|
|
583
|
+
if (sel && deepQuery(sel)) return resolve({ found: true });
|
|
584
|
+
if (Date.now() > deadline) return resolve({ found: false });
|
|
585
|
+
setTimeout(tick, every);
|
|
586
|
+
};
|
|
587
|
+
tick();
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
case "waitFor": {
|
|
591
|
+
const deadline = Date.now() + (a.timeoutMs ?? 3e4);
|
|
592
|
+
const every = a.interval ?? 150;
|
|
593
|
+
const want = typeof a.textContains === "string" && a.textContains.length > 0 ? a.textContains : null;
|
|
594
|
+
const gone = a.gone === true;
|
|
595
|
+
return new Promise((resolve) => {
|
|
596
|
+
const hit = () => {
|
|
597
|
+
let present;
|
|
598
|
+
if (sel) present = !!deepQuery(sel);
|
|
599
|
+
else if (want) present = (document.body?.innerText ?? "").includes(want);
|
|
600
|
+
else present = true;
|
|
601
|
+
return gone ? !present : present;
|
|
602
|
+
};
|
|
603
|
+
const tick = () => {
|
|
604
|
+
if (hit()) return resolve({ found: true, matched: true });
|
|
605
|
+
if (Date.now() > deadline) return resolve({ found: true, matched: false });
|
|
606
|
+
setTimeout(tick, every);
|
|
607
|
+
};
|
|
608
|
+
tick();
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
default:
|
|
612
|
+
return { found: false, error: `unknown page op: ${String(a.op)}` };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// shared/observers.ts
|
|
617
|
+
var OBSERVER_GLOBAL = "__chromeMcpObservers";
|
|
618
|
+
function readObservers(globalName, opts) {
|
|
619
|
+
const state = window[globalName];
|
|
620
|
+
if (!state) return { installed: false };
|
|
621
|
+
if (opts.setPolicy === "dismiss" || opts.setPolicy === "accept") state.dialogPolicy = opts.setPolicy;
|
|
622
|
+
if (typeof opts.promptText === "string") state.promptText = opts.promptText;
|
|
623
|
+
const since = typeof opts.sinceSeq === "number" ? opts.sinceSeq : 0;
|
|
624
|
+
const limit = typeof opts.limit === "number" && opts.limit > 0 ? opts.limit : 200;
|
|
625
|
+
const take = (buf) => {
|
|
626
|
+
const picked = buf.filter((e) => e.seq > since);
|
|
627
|
+
return picked.length > limit ? picked.slice(picked.length - limit) : picked;
|
|
628
|
+
};
|
|
629
|
+
if (opts.network && opts.includeResources) {
|
|
630
|
+
try {
|
|
631
|
+
const entries = performance.getEntriesByType("resource");
|
|
632
|
+
for (let i = state.resourceCursor; i < entries.length; i++) {
|
|
633
|
+
const r = entries[i];
|
|
634
|
+
state.network.push({
|
|
635
|
+
seq: ++state.seq,
|
|
636
|
+
ts: Math.round(performance.timeOrigin + r.startTime),
|
|
637
|
+
via: "resource",
|
|
638
|
+
method: "GET",
|
|
639
|
+
url: r.name.length > 2e3 ? `${r.name.slice(0, 2e3)}\u2026` : r.name,
|
|
640
|
+
durationMs: Math.round(r.duration),
|
|
641
|
+
initiator: r.initiatorType,
|
|
642
|
+
bytes: r.transferSize || void 0
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
state.resourceCursor = entries.length;
|
|
646
|
+
} catch {
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const out = {
|
|
650
|
+
installed: true,
|
|
651
|
+
hookVersion: state.v,
|
|
652
|
+
dialogPolicy: state.dialogPolicy,
|
|
653
|
+
dropped: state.dropped
|
|
654
|
+
};
|
|
655
|
+
if (opts.console) out.console = take(state.console);
|
|
656
|
+
if (opts.network) out.network = take(state.network);
|
|
657
|
+
if (opts.dialogs) out.dialogs = take(state.dialogs);
|
|
658
|
+
if (opts.clear) {
|
|
659
|
+
if (opts.console) state.console.length = 0;
|
|
660
|
+
if (opts.network) state.network.length = 0;
|
|
661
|
+
if (opts.dialogs) state.dialogs.length = 0;
|
|
662
|
+
state.dropped = false;
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
function observerMatches(policy) {
|
|
667
|
+
if (policy.allowObservers !== true) return [];
|
|
668
|
+
const out = /* @__PURE__ */ new Set();
|
|
669
|
+
for (const raw of policy.allowDomains ?? []) {
|
|
670
|
+
const p = normalizeDomainPattern(raw);
|
|
671
|
+
if (!p) continue;
|
|
672
|
+
if (p === "*") return ["<all_urls>"];
|
|
673
|
+
out.add(`*://${p}/*`);
|
|
674
|
+
}
|
|
675
|
+
return [...out];
|
|
676
|
+
}
|
|
677
|
+
|
|
378
678
|
// shared/screenshot.ts
|
|
379
679
|
var MAX_CAPTURE_PX = 16384;
|
|
380
680
|
function planScreenshot(dims, opts = {}) {
|
|
@@ -510,15 +810,32 @@
|
|
|
510
810
|
return "";
|
|
511
811
|
}
|
|
512
812
|
}
|
|
513
|
-
|
|
813
|
+
function scriptTarget(tabId, frameIds) {
|
|
814
|
+
return frameIds && frameIds.length > 0 ? { tabId, frameIds } : { tabId };
|
|
815
|
+
}
|
|
816
|
+
async function execInTab(tabId, func, args = [], world, frameIds) {
|
|
514
817
|
const [res] = await chrome.scripting.executeScript({
|
|
515
|
-
target:
|
|
818
|
+
target: scriptTarget(tabId, frameIds),
|
|
516
819
|
func,
|
|
517
820
|
args,
|
|
518
821
|
world
|
|
519
822
|
});
|
|
520
823
|
return res?.result;
|
|
521
824
|
}
|
|
825
|
+
async function execOp(tabId, args, frameIds, world) {
|
|
826
|
+
const results = await chrome.scripting.executeScript({
|
|
827
|
+
target: scriptTarget(tabId, frameIds),
|
|
828
|
+
func: pageOp,
|
|
829
|
+
args: [args],
|
|
830
|
+
world
|
|
831
|
+
});
|
|
832
|
+
const outcomes = results.map((r) => ({
|
|
833
|
+
...r.result ?? { found: false },
|
|
834
|
+
frameId: r.frameId ?? 0
|
|
835
|
+
}));
|
|
836
|
+
if (outcomes.length === 0) throw new CmdError("TARGET_GONE", "the page returned no result for this command");
|
|
837
|
+
return outcomes.find((o) => o.found === true) ?? outcomes[0];
|
|
838
|
+
}
|
|
522
839
|
async function waitComplete(tabId, timeoutMs = 3e4) {
|
|
523
840
|
const start = Date.now();
|
|
524
841
|
for (; ; ) {
|
|
@@ -538,21 +855,9 @@
|
|
|
538
855
|
function selectorOf(cmd) {
|
|
539
856
|
return resolveSelector(cmd);
|
|
540
857
|
}
|
|
541
|
-
async function waitForSelector(tabId, selector, timeoutMs = 5e3) {
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
(s, timeout, interval) => new Promise((resolve) => {
|
|
545
|
-
const deadline = Date.now() + timeout;
|
|
546
|
-
const tick = () => {
|
|
547
|
-
if (document.querySelector(s)) return resolve(true);
|
|
548
|
-
if (Date.now() > deadline) return resolve(false);
|
|
549
|
-
setTimeout(tick, interval);
|
|
550
|
-
};
|
|
551
|
-
tick();
|
|
552
|
-
}),
|
|
553
|
-
[selector, timeoutMs, 120]
|
|
554
|
-
);
|
|
555
|
-
return found === true;
|
|
858
|
+
async function waitForSelector(tabId, selector, frameIds, timeoutMs = 5e3) {
|
|
859
|
+
const out = await execOp(tabId, { op: "waitSelector", selector, timeoutMs, interval: 120 }, frameIds);
|
|
860
|
+
return out.found === true;
|
|
556
861
|
}
|
|
557
862
|
async function withDebugger(tabId, fn) {
|
|
558
863
|
return locks.run(`dbg:${tabId}`, async () => {
|
|
@@ -565,69 +870,34 @@
|
|
|
565
870
|
}
|
|
566
871
|
});
|
|
567
872
|
}
|
|
568
|
-
async function trustedType(tabId, selector, text, clear) {
|
|
569
|
-
const focused = await
|
|
570
|
-
|
|
571
|
-
(s, doClear) => {
|
|
572
|
-
const el = document.querySelector(s);
|
|
573
|
-
if (!el) return false;
|
|
574
|
-
el.focus();
|
|
575
|
-
if (doClear) {
|
|
576
|
-
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value")?.set;
|
|
577
|
-
setter ? setter.call(el, "") : el.value = "";
|
|
578
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
579
|
-
}
|
|
580
|
-
return true;
|
|
581
|
-
},
|
|
582
|
-
[selector, clear]
|
|
583
|
-
);
|
|
584
|
-
if (!focused) return false;
|
|
873
|
+
async function trustedType(tabId, selector, text, clear, frameIds) {
|
|
874
|
+
const focused = await execOp(tabId, { op: "focus", selector, clear }, frameIds);
|
|
875
|
+
if (!focused.found) return false;
|
|
585
876
|
await withDebugger(tabId, (t) => chrome.debugger.sendCommand(t, "Input.insertText", { text }));
|
|
586
877
|
return true;
|
|
587
878
|
}
|
|
588
|
-
async function trustedClick(tabId, selector) {
|
|
589
|
-
const pt = await
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const el = document.querySelector(s);
|
|
593
|
-
if (!el) return null;
|
|
594
|
-
el.scrollIntoView({ block: "center", inline: "center" });
|
|
595
|
-
const r = el.getBoundingClientRect();
|
|
596
|
-
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
597
|
-
},
|
|
598
|
-
[selector]
|
|
599
|
-
);
|
|
600
|
-
if (!pt) return false;
|
|
879
|
+
async function trustedClick(tabId, selector, frameIds) {
|
|
880
|
+
const pt = await execOp(tabId, { op: "point", selector }, frameIds);
|
|
881
|
+
if (!pt.found) return "missing";
|
|
882
|
+
if (pt.exact === false) return "inexact";
|
|
601
883
|
await withDebugger(tabId, async (t) => {
|
|
602
884
|
const base = { x: pt.x, y: pt.y, button: "left", clickCount: 1 };
|
|
603
885
|
await chrome.debugger.sendCommand(t, "Input.dispatchMouseEvent", { type: "mousePressed", buttons: 1, ...base });
|
|
604
886
|
await chrome.debugger.sendCommand(t, "Input.dispatchMouseEvent", { type: "mouseReleased", buttons: 0, ...base });
|
|
605
887
|
});
|
|
606
|
-
return
|
|
888
|
+
return "ok";
|
|
607
889
|
}
|
|
608
|
-
async function measurePage(tabId, selector) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
fullW: Math.max(d.scrollWidth, d.clientWidth),
|
|
617
|
-
fullH: Math.max(d.scrollHeight, d.clientHeight)
|
|
618
|
-
};
|
|
619
|
-
if (!sel) return { dims, element: null, missing: false };
|
|
620
|
-
const el = document.querySelector(sel);
|
|
621
|
-
if (!el) return { dims, element: null, missing: true };
|
|
622
|
-
el.scrollIntoView({ block: "center", inline: "center" });
|
|
623
|
-
const r = el.getBoundingClientRect();
|
|
624
|
-
return { dims, element: { x: r.left + window.scrollX, y: r.top + window.scrollY, w: r.width, h: r.height }, missing: false };
|
|
625
|
-
},
|
|
626
|
-
[selector ?? null]
|
|
627
|
-
);
|
|
890
|
+
async function measurePage(tabId, selector, frameIds) {
|
|
891
|
+
const found = await execOp(tabId, { op: "measure", selector: selector ?? null }, frameIds);
|
|
892
|
+
const top = selector && frameIds && frameIds.length > 0 ? await execOp(tabId, { op: "measure", selector: null }) : found;
|
|
893
|
+
return {
|
|
894
|
+
dims: top.dims,
|
|
895
|
+
element: found.element ?? null,
|
|
896
|
+
missing: found.missing === true
|
|
897
|
+
};
|
|
628
898
|
}
|
|
629
|
-
async function screenshotViaDebugger(tabId, fullPage, selector) {
|
|
630
|
-
const measured = await measurePage(tabId, selector);
|
|
899
|
+
async function screenshotViaDebugger(tabId, fullPage, selector, frameIds) {
|
|
900
|
+
const measured = await measurePage(tabId, selector, frameIds);
|
|
631
901
|
if (!measured) throw new CmdError("CDP_ERROR", "could not read page dimensions");
|
|
632
902
|
if (selector && measured.missing) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${selector}`);
|
|
633
903
|
const plan = planScreenshot(measured.dims, { fullPage, element: measured.element });
|
|
@@ -705,9 +975,67 @@
|
|
|
705
975
|
"wait_for",
|
|
706
976
|
"download_file",
|
|
707
977
|
"upload_file",
|
|
708
|
-
"ping_probe"
|
|
978
|
+
"ping_probe",
|
|
979
|
+
"frames_list",
|
|
980
|
+
"observers",
|
|
981
|
+
"print_pdf"
|
|
709
982
|
]);
|
|
710
983
|
var ChromeExecutor = class {
|
|
984
|
+
/**
|
|
985
|
+
* The live wire policy, so frame-scoped commands can be gated against the
|
|
986
|
+
* FRAME's origin rather than the tab's. The router gates the tab; only this
|
|
987
|
+
* side knows which frames an injection would actually reach, and an
|
|
988
|
+
* allowlisted page embedding a third-party iframe is the ordinary case, not an
|
|
989
|
+
* exotic one.
|
|
990
|
+
*/
|
|
991
|
+
constructor(getPolicy = () => null) {
|
|
992
|
+
this.getPolicy = getPolicy;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Resolve `frameId` / `allFrames` params into the frame ids an injection may
|
|
996
|
+
* touch, dropping any whose own URL the policy does not allow for this method.
|
|
997
|
+
* `undefined` means the top frame only (the default, and every call that
|
|
998
|
+
* predates frame support).
|
|
999
|
+
*
|
|
1000
|
+
* The URL probe is a separate round-trip on purpose: filtering AFTER running
|
|
1001
|
+
* the command would mean a mutation had already fired inside a frame nobody
|
|
1002
|
+
* authorized.
|
|
1003
|
+
*/
|
|
1004
|
+
async frames(cmd, tabId) {
|
|
1005
|
+
const explicit = typeof cmd.params.frameId === "number" ? cmd.params.frameId : void 0;
|
|
1006
|
+
const all = cmd.params.allFrames === true;
|
|
1007
|
+
if (explicit === void 0 && !all) return void 0;
|
|
1008
|
+
const probes = await chrome.scripting.executeScript({
|
|
1009
|
+
target: explicit !== void 0 ? { tabId, frameIds: [explicit] } : { tabId, allFrames: true },
|
|
1010
|
+
func: pageOp,
|
|
1011
|
+
args: [{ op: "probe" }]
|
|
1012
|
+
}).catch(() => []);
|
|
1013
|
+
const seen = probes.map((r) => ({
|
|
1014
|
+
frameId: r.frameId ?? 0,
|
|
1015
|
+
url: String(r.result?.url ?? "")
|
|
1016
|
+
}));
|
|
1017
|
+
if (seen.length === 0) {
|
|
1018
|
+
throw new CmdError(
|
|
1019
|
+
"FRAME_NOT_FOUND",
|
|
1020
|
+
explicit !== void 0 ? `frame ${explicit} is not in this tab (or the extension cannot inject into it) \u2014 call frames_list` : "no injectable frames in this tab"
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
const policy = this.getPolicy();
|
|
1024
|
+
if (!policy) throw new CmdError("POLICY_DENIED", "no policy has arrived yet, so no frame is authorized");
|
|
1025
|
+
const allowed = [];
|
|
1026
|
+
const denied = [];
|
|
1027
|
+
for (const f of seen) {
|
|
1028
|
+
if (evaluatePolicy(f.url, cmd.method, policy).ok) allowed.push(f.frameId);
|
|
1029
|
+
else denied.push(f.url || `frame ${f.frameId}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (allowed.length === 0) {
|
|
1032
|
+
throw new CmdError(
|
|
1033
|
+
"POLICY_DENIED",
|
|
1034
|
+
`no targeted frame is on the allowed-sites list (skipped: ${denied.slice(0, 5).join(", ")})`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
return allowed;
|
|
1038
|
+
}
|
|
711
1039
|
async run(cmd) {
|
|
712
1040
|
switch (cmd.method) {
|
|
713
1041
|
case "ping_probe":
|
|
@@ -789,39 +1117,34 @@
|
|
|
789
1117
|
// -- reads (isolated world; CSP-safe) --
|
|
790
1118
|
case "get_text": {
|
|
791
1119
|
const id = await targetTab(cmd);
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
return el ? el.innerText : "";
|
|
797
|
-
},
|
|
798
|
-
[selectorOf(cmd) ?? null]
|
|
799
|
-
);
|
|
800
|
-
return { text: text ?? "" };
|
|
1120
|
+
const frameIds = await this.frames(cmd, id);
|
|
1121
|
+
const out = await execOp(id, { op: "text", selector: selectorOf(cmd) ?? null }, frameIds);
|
|
1122
|
+
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${selectorOf(cmd)}`);
|
|
1123
|
+
return { text: String(out.text ?? ""), frameId: out.frameId };
|
|
801
1124
|
}
|
|
802
1125
|
case "get_html": {
|
|
803
1126
|
const id = await targetTab(cmd);
|
|
804
|
-
const
|
|
805
|
-
const
|
|
1127
|
+
const frameIds = await this.frames(cmd, id);
|
|
1128
|
+
const out = await execOp(
|
|
806
1129
|
id,
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
if (!el) return "";
|
|
810
|
-
return isOuter || !sel ? el.outerHTML : el.innerHTML;
|
|
811
|
-
},
|
|
812
|
-
[selectorOf(cmd) ?? null, outer]
|
|
1130
|
+
{ op: "html", selector: selectorOf(cmd) ?? null, outer: cmd.params.outer === true },
|
|
1131
|
+
frameIds
|
|
813
1132
|
);
|
|
814
|
-
|
|
1133
|
+
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${selectorOf(cmd)}`);
|
|
1134
|
+
return { html: String(out.html ?? ""), frameId: out.frameId };
|
|
815
1135
|
}
|
|
816
1136
|
// -- accessibility snapshot (tags elements with data-mcp-ref so refs work) --
|
|
817
1137
|
case "snapshot": {
|
|
818
1138
|
const id = await targetTab(cmd);
|
|
819
1139
|
const interactiveOnly = cmd.params.interactiveOnly !== false;
|
|
820
1140
|
const max = typeof cmd.params.max === "number" ? cmd.params.max : 200;
|
|
1141
|
+
const frameIds = await this.frames(cmd, id);
|
|
821
1142
|
const raw = await execInTab(
|
|
822
1143
|
id,
|
|
823
1144
|
collectSnapshot,
|
|
824
|
-
[interactiveOnly, max]
|
|
1145
|
+
[interactiveOnly, max],
|
|
1146
|
+
void 0,
|
|
1147
|
+
frameIds
|
|
825
1148
|
);
|
|
826
1149
|
return raw ?? { url: "", title: "", nodes: [], truncated: false };
|
|
827
1150
|
}
|
|
@@ -829,28 +1152,13 @@
|
|
|
829
1152
|
case "select_option": {
|
|
830
1153
|
const id = await targetTab(cmd);
|
|
831
1154
|
const sel = requireSelector(cmd);
|
|
1155
|
+
const frameIds = await this.frames(cmd, id);
|
|
832
1156
|
const values = Array.isArray(cmd.params.values) ? cmd.params.values.map(String) : [];
|
|
833
|
-
await waitForSelector(id, sel);
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (!el || !el.options) return false;
|
|
839
|
-
const set = new Set(vals);
|
|
840
|
-
let hit = false;
|
|
841
|
-
for (const opt of Array.from(el.options)) {
|
|
842
|
-
const on = set.has(opt.value) || set.has(opt.label) || set.has(opt.text);
|
|
843
|
-
opt.selected = on;
|
|
844
|
-
if (on) hit = true;
|
|
845
|
-
}
|
|
846
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
847
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
848
|
-
return hit;
|
|
849
|
-
},
|
|
850
|
-
[sel, values]
|
|
851
|
-
);
|
|
852
|
-
if (!matched) throw new CmdError("SELECTOR_NOT_FOUND", `no <select> option matched for ${sel}`);
|
|
853
|
-
return { ok: true };
|
|
1157
|
+
await waitForSelector(id, sel, frameIds);
|
|
1158
|
+
const out = await execOp(id, { op: "select", selector: sel, values }, frameIds);
|
|
1159
|
+
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1160
|
+
if (out.matched !== true) throw new CmdError("SELECTOR_NOT_FOUND", `no <select> option matched for ${sel}`);
|
|
1161
|
+
return { ok: true, frameId: out.frameId };
|
|
854
1162
|
}
|
|
855
1163
|
// -- cookies for the tab's URL (chrome.cookies; needs "cookies" permission) --
|
|
856
1164
|
case "get_cookies": {
|
|
@@ -878,65 +1186,41 @@
|
|
|
878
1186
|
const key = typeof cmd.params.key === "string" ? cmd.params.key : null;
|
|
879
1187
|
const value = typeof cmd.params.value === "string" ? cmd.params.value : null;
|
|
880
1188
|
const session = cmd.params.session === true;
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (o === "set") {
|
|
886
|
-
store.setItem(String(k), String(v ?? ""));
|
|
887
|
-
return { ok: true };
|
|
888
|
-
}
|
|
889
|
-
if (o === "remove") {
|
|
890
|
-
store.removeItem(String(k));
|
|
891
|
-
return { ok: true };
|
|
892
|
-
}
|
|
893
|
-
if (o === "clear") {
|
|
894
|
-
store.clear();
|
|
895
|
-
return { ok: true };
|
|
896
|
-
}
|
|
897
|
-
if (k) return { ok: true, value: store.getItem(k) };
|
|
898
|
-
const entries = {};
|
|
899
|
-
for (let i = 0; i < store.length; i++) {
|
|
900
|
-
const kk = store.key(i);
|
|
901
|
-
if (kk) entries[kk] = store.getItem(kk) ?? "";
|
|
902
|
-
}
|
|
903
|
-
return { ok: true, entries };
|
|
904
|
-
},
|
|
905
|
-
[op, key, value, session]
|
|
906
|
-
);
|
|
907
|
-
return res ?? { ok: false };
|
|
1189
|
+
const frameIds = await this.frames(cmd, id);
|
|
1190
|
+
const out = await execOp(id, { op: "storage", storageOp: op, key, value, session }, frameIds);
|
|
1191
|
+
const { found: _found, frameId: _frameId, ...rest } = out;
|
|
1192
|
+
return Object.keys(rest).length > 0 ? rest : { ok: false };
|
|
908
1193
|
}
|
|
909
1194
|
// -- interaction (synthetic events in the isolated world) --
|
|
910
1195
|
case "click": {
|
|
911
1196
|
const id = await targetTab(cmd);
|
|
912
1197
|
const sel = requireSelector(cmd);
|
|
913
|
-
|
|
1198
|
+
const frameIds = await this.frames(cmd, id);
|
|
1199
|
+
if (!await waitForSelector(id, sel, frameIds)) {
|
|
1200
|
+
throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1201
|
+
}
|
|
914
1202
|
if (cmd.params.trusted === true) {
|
|
915
|
-
|
|
916
|
-
|
|
1203
|
+
const verdict = await trustedClick(id, sel, frameIds);
|
|
1204
|
+
if (verdict === "missing") throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1205
|
+
if (verdict === "ok") return { ok: true };
|
|
917
1206
|
}
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const el = document.querySelector(s);
|
|
922
|
-
if (!el) return false;
|
|
923
|
-
el.scrollIntoView({ block: "center" });
|
|
924
|
-
el.click();
|
|
925
|
-
return true;
|
|
926
|
-
},
|
|
927
|
-
[sel]
|
|
928
|
-
);
|
|
929
|
-
if (!found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
930
|
-
return { ok: true };
|
|
1207
|
+
const out = await execOp(id, { op: "click", selector: sel }, frameIds);
|
|
1208
|
+
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1209
|
+
return { ok: true, frameId: out.frameId, ...cmd.params.trusted === true ? { trusted: false } : {} };
|
|
931
1210
|
}
|
|
932
1211
|
case "type": {
|
|
933
1212
|
const id = await targetTab(cmd);
|
|
934
1213
|
const sel = requireSelector(cmd);
|
|
1214
|
+
const frameIds = await this.frames(cmd, id);
|
|
935
1215
|
const text = String(cmd.params.text ?? "");
|
|
936
1216
|
const clear = cmd.params.clear === true;
|
|
937
|
-
if (!await waitForSelector(id, sel))
|
|
1217
|
+
if (!await waitForSelector(id, sel, frameIds)) {
|
|
1218
|
+
throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1219
|
+
}
|
|
938
1220
|
if (cmd.params.trusted === true) {
|
|
939
|
-
if (!await trustedType(id, sel, text, clear))
|
|
1221
|
+
if (!await trustedType(id, sel, text, clear, frameIds)) {
|
|
1222
|
+
throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1223
|
+
}
|
|
940
1224
|
if (cmd.params.pressEnter === true) {
|
|
941
1225
|
await withDebugger(id, async (t) => {
|
|
942
1226
|
for (const type of ["keyDown", "keyUp"]) {
|
|
@@ -946,23 +1230,9 @@
|
|
|
946
1230
|
}
|
|
947
1231
|
return { ok: true };
|
|
948
1232
|
}
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const el = document.querySelector(s);
|
|
953
|
-
if (!el) return false;
|
|
954
|
-
el.focus();
|
|
955
|
-
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value")?.set;
|
|
956
|
-
const next = (doClear ? "" : el.value ?? "") + value;
|
|
957
|
-
setter ? setter.call(el, next) : el.value = next;
|
|
958
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
959
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
960
|
-
return true;
|
|
961
|
-
},
|
|
962
|
-
[sel, text, clear]
|
|
963
|
-
);
|
|
964
|
-
if (!found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
965
|
-
return { ok: true };
|
|
1233
|
+
const out = await execOp(id, { op: "type", selector: sel, text, clear }, frameIds);
|
|
1234
|
+
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1235
|
+
return { ok: true, frameId: out.frameId };
|
|
966
1236
|
}
|
|
967
1237
|
case "press": {
|
|
968
1238
|
const id = await targetTab(cmd);
|
|
@@ -982,27 +1252,26 @@
|
|
|
982
1252
|
case "hover": {
|
|
983
1253
|
const id = await targetTab(cmd);
|
|
984
1254
|
const sel = requireSelector(cmd);
|
|
985
|
-
await
|
|
986
|
-
await
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
el?.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
991
|
-
el?.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
|
992
|
-
},
|
|
993
|
-
[sel]
|
|
994
|
-
);
|
|
995
|
-
return { ok: true };
|
|
1255
|
+
const frameIds = await this.frames(cmd, id);
|
|
1256
|
+
await waitForSelector(id, sel, frameIds);
|
|
1257
|
+
const out = await execOp(id, { op: "hover", selector: sel }, frameIds);
|
|
1258
|
+
if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1259
|
+
return { ok: true, frameId: out.frameId };
|
|
996
1260
|
}
|
|
997
1261
|
case "scroll": {
|
|
998
1262
|
const id = await targetTab(cmd);
|
|
999
|
-
await
|
|
1263
|
+
const frameIds = await this.frames(cmd, id);
|
|
1264
|
+
await execOp(
|
|
1000
1265
|
id,
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1266
|
+
{
|
|
1267
|
+
op: "scroll",
|
|
1268
|
+
selector: selectorOf(cmd) ?? null,
|
|
1269
|
+
x: cmd.params.x ?? null,
|
|
1270
|
+
y: cmd.params.y ?? null,
|
|
1271
|
+
deltaX: cmd.params.deltaX ?? null,
|
|
1272
|
+
deltaY: cmd.params.deltaY ?? null
|
|
1004
1273
|
},
|
|
1005
|
-
|
|
1274
|
+
frameIds
|
|
1006
1275
|
);
|
|
1007
1276
|
return { ok: true };
|
|
1008
1277
|
}
|
|
@@ -1015,8 +1284,9 @@
|
|
|
1015
1284
|
const id = await targetTab(cmd);
|
|
1016
1285
|
const fullPage = cmd.params.fullPage === true;
|
|
1017
1286
|
const selector = selectorOf(cmd);
|
|
1287
|
+
const frameIds = await this.frames(cmd, id);
|
|
1018
1288
|
try {
|
|
1019
|
-
return await screenshotViaDebugger(id, fullPage, selector);
|
|
1289
|
+
return await screenshotViaDebugger(id, fullPage, selector, frameIds);
|
|
1020
1290
|
} catch (err) {
|
|
1021
1291
|
if (err instanceof CmdError && err.code === "SELECTOR_NOT_FOUND") throw err;
|
|
1022
1292
|
return await screenshotViaVisibleTab(id, fullPage);
|
|
@@ -1037,7 +1307,8 @@
|
|
|
1037
1307
|
}
|
|
1038
1308
|
},
|
|
1039
1309
|
[expr],
|
|
1040
|
-
"MAIN"
|
|
1310
|
+
"MAIN",
|
|
1311
|
+
await this.frames(cmd, id)
|
|
1041
1312
|
);
|
|
1042
1313
|
return result ?? { ok: false, error: "no result" };
|
|
1043
1314
|
}
|
|
@@ -1046,29 +1317,22 @@
|
|
|
1046
1317
|
// passes, rather than one executeScript round-trip per tick.
|
|
1047
1318
|
case "wait_for": {
|
|
1048
1319
|
const id = await targetTab(cmd);
|
|
1320
|
+
const frameIds = await this.frames(cmd, id);
|
|
1049
1321
|
const timeout = typeof cmd.params.timeoutMs === "number" ? cmd.params.timeoutMs : 3e4;
|
|
1050
1322
|
const start = Date.now();
|
|
1051
|
-
const
|
|
1323
|
+
const out = await execOp(
|
|
1052
1324
|
id,
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const tick = () => {
|
|
1063
|
-
if (hit()) return resolve(true);
|
|
1064
|
-
if (Date.now() > deadline) return resolve(false);
|
|
1065
|
-
setTimeout(tick, interval);
|
|
1066
|
-
};
|
|
1067
|
-
tick();
|
|
1068
|
-
}),
|
|
1069
|
-
[cmd.params.selector ?? null, cmd.params.textContains ?? null, cmd.params.gone === true, timeout, 150]
|
|
1325
|
+
{
|
|
1326
|
+
op: "waitFor",
|
|
1327
|
+
selector: cmd.params.selector ?? null,
|
|
1328
|
+
textContains: cmd.params.textContains ?? null,
|
|
1329
|
+
gone: cmd.params.gone === true,
|
|
1330
|
+
timeoutMs: timeout,
|
|
1331
|
+
interval: 150
|
|
1332
|
+
},
|
|
1333
|
+
frameIds
|
|
1070
1334
|
);
|
|
1071
|
-
return { matched: matched === true, waitedMs: Date.now() - start };
|
|
1335
|
+
return { matched: out.matched === true, waitedMs: Date.now() - start };
|
|
1072
1336
|
}
|
|
1073
1337
|
// -- download (saved to the user's Downloads dir; the server then moves it
|
|
1074
1338
|
// into the active task's downloads/). We wait for completion and report
|
|
@@ -1097,7 +1361,9 @@
|
|
|
1097
1361
|
const sel = requireSelector(cmd);
|
|
1098
1362
|
const files = Array.isArray(cmd.params.files) ? cmd.params.files.map(String) : [];
|
|
1099
1363
|
if (files.length === 0) throw new CmdError("BAD_ARGS", 'upload_file requires a non-empty "files" array');
|
|
1100
|
-
if (!await waitForSelector(id, sel
|
|
1364
|
+
if (!await waitForSelector(id, sel, void 0)) {
|
|
1365
|
+
throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
|
|
1366
|
+
}
|
|
1101
1367
|
await withDebugger(id, async (t) => {
|
|
1102
1368
|
const doc = await chrome.debugger.sendCommand(t, "DOM.getDocument", { depth: 0 });
|
|
1103
1369
|
const rootId = doc.root?.nodeId;
|
|
@@ -1108,6 +1374,112 @@
|
|
|
1108
1374
|
});
|
|
1109
1375
|
return { ok: true };
|
|
1110
1376
|
}
|
|
1377
|
+
// -- frames: the ids an injection can target, plus each frame's URL.
|
|
1378
|
+
// Read via a probe injection rather than chrome.webNavigation so no
|
|
1379
|
+
// extra host-level permission is needed to see them. --
|
|
1380
|
+
case "frames_list": {
|
|
1381
|
+
const id = await targetTab(cmd);
|
|
1382
|
+
const probes = await chrome.scripting.executeScript({
|
|
1383
|
+
target: { tabId: id, allFrames: true },
|
|
1384
|
+
func: pageOp,
|
|
1385
|
+
args: [{ op: "probe" }]
|
|
1386
|
+
}).catch(() => []);
|
|
1387
|
+
const frames = probes.map((r) => {
|
|
1388
|
+
const res = r.result ?? {};
|
|
1389
|
+
return {
|
|
1390
|
+
frameId: r.frameId ?? 0,
|
|
1391
|
+
top: (r.frameId ?? 0) === 0,
|
|
1392
|
+
url: res.url ?? "",
|
|
1393
|
+
title: res.title ?? ""
|
|
1394
|
+
};
|
|
1395
|
+
});
|
|
1396
|
+
return { frames, count: frames.length };
|
|
1397
|
+
}
|
|
1398
|
+
// -- observers: read (and configure) the in-page console/network/dialog
|
|
1399
|
+
// buffers. The hook is normally registered at document_start; a page
|
|
1400
|
+
// that loaded before registration gets it injected here, and the caller
|
|
1401
|
+
// is told capture only starts from that moment. --
|
|
1402
|
+
case "observers": {
|
|
1403
|
+
const id = await targetTab(cmd);
|
|
1404
|
+
const frameIds = await this.frames(cmd, id);
|
|
1405
|
+
const opts = {
|
|
1406
|
+
console: cmd.params.console === true,
|
|
1407
|
+
network: cmd.params.network === true,
|
|
1408
|
+
dialogs: cmd.params.dialogs === true,
|
|
1409
|
+
sinceSeq: typeof cmd.params.sinceSeq === "number" ? cmd.params.sinceSeq : 0,
|
|
1410
|
+
limit: typeof cmd.params.limit === "number" ? cmd.params.limit : 200,
|
|
1411
|
+
clear: cmd.params.clear === true,
|
|
1412
|
+
setPolicy: typeof cmd.params.setPolicy === "string" ? cmd.params.setPolicy : null,
|
|
1413
|
+
promptText: typeof cmd.params.promptText === "string" ? cmd.params.promptText : null,
|
|
1414
|
+
includeResources: cmd.params.includeResources === true
|
|
1415
|
+
};
|
|
1416
|
+
const read = async () => {
|
|
1417
|
+
const results = await chrome.scripting.executeScript({
|
|
1418
|
+
target: scriptTarget(id, frameIds),
|
|
1419
|
+
func: readObservers,
|
|
1420
|
+
args: [OBSERVER_GLOBAL, opts],
|
|
1421
|
+
world: "MAIN"
|
|
1422
|
+
});
|
|
1423
|
+
return results.map((r) => ({
|
|
1424
|
+
frameId: r.frameId ?? 0,
|
|
1425
|
+
out: r.result ?? { installed: false }
|
|
1426
|
+
}));
|
|
1427
|
+
};
|
|
1428
|
+
let frames = await read();
|
|
1429
|
+
let justInstalled = false;
|
|
1430
|
+
if (!frames.some((f) => f.out.installed === true)) {
|
|
1431
|
+
await chrome.scripting.executeScript({ target: scriptTarget(id, frameIds), files: ["page-hook.js"], world: "MAIN" }).catch(() => void 0);
|
|
1432
|
+
frames = await read();
|
|
1433
|
+
justInstalled = frames.some((f) => f.out.installed === true);
|
|
1434
|
+
}
|
|
1435
|
+
const live = frames.filter((f) => f.out.installed === true);
|
|
1436
|
+
if (live.length === 0) return { installed: false };
|
|
1437
|
+
const merge = (key) => {
|
|
1438
|
+
const all = [];
|
|
1439
|
+
for (const f of live) {
|
|
1440
|
+
for (const e of f.out[key] ?? []) {
|
|
1441
|
+
all.push(f.frameId === 0 ? e : { ...e, frameId: f.frameId });
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return all.sort((a, b) => Number(a.ts ?? 0) - Number(b.ts ?? 0));
|
|
1445
|
+
};
|
|
1446
|
+
const top = live.find((f) => f.frameId === 0) ?? live[0];
|
|
1447
|
+
return {
|
|
1448
|
+
installed: true,
|
|
1449
|
+
hookVersion: top.out.hookVersion,
|
|
1450
|
+
dialogPolicy: top.out.dialogPolicy,
|
|
1451
|
+
dropped: live.some((f) => f.out.dropped === true),
|
|
1452
|
+
...opts.console ? { console: merge("console") } : {},
|
|
1453
|
+
...opts.network ? { network: merge("network") } : {},
|
|
1454
|
+
...opts.dialogs ? { dialogs: merge("dialogs") } : {},
|
|
1455
|
+
...live.length > 1 ? { frames: live.length } : {},
|
|
1456
|
+
...justInstalled ? {
|
|
1457
|
+
justInstalled: true,
|
|
1458
|
+
note: "the observer hook was installed just now, so these buffers start empty - reload the page to capture what happens during load"
|
|
1459
|
+
} : {}
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
// -- print_pdf: Page.printToPDF over the same one-op debugger attach the
|
|
1463
|
+
// screenshot path uses. Returns base64; the server writes the file. --
|
|
1464
|
+
case "print_pdf": {
|
|
1465
|
+
const id = await targetTab(cmd);
|
|
1466
|
+
const params = {
|
|
1467
|
+
printBackground: cmd.params.printBackground !== false,
|
|
1468
|
+
landscape: cmd.params.landscape === true,
|
|
1469
|
+
preferCSSPageSize: cmd.params.preferCSSPageSize === true
|
|
1470
|
+
};
|
|
1471
|
+
if (typeof cmd.params.scale === "number") params.scale = cmd.params.scale;
|
|
1472
|
+
if (typeof cmd.params.paperWidth === "number") params.paperWidth = cmd.params.paperWidth;
|
|
1473
|
+
if (typeof cmd.params.paperHeight === "number") params.paperHeight = cmd.params.paperHeight;
|
|
1474
|
+
if (typeof cmd.params.pageRanges === "string") params.pageRanges = cmd.params.pageRanges;
|
|
1475
|
+
const data = await withDebugger(id, async (t2) => {
|
|
1476
|
+
const res = await chrome.debugger.sendCommand(t2, "Page.printToPDF", params);
|
|
1477
|
+
return res.data ?? "";
|
|
1478
|
+
});
|
|
1479
|
+
if (!data) throw new CmdError("CDP_ERROR", "Chrome returned an empty PDF");
|
|
1480
|
+
const t = await chrome.tabs.get(id);
|
|
1481
|
+
return { dataBase64: data, mimeType: "application/pdf", url: t.url ?? "", title: t.title ?? "" };
|
|
1482
|
+
}
|
|
1111
1483
|
default:
|
|
1112
1484
|
throw new CmdError("UNKNOWN_METHOD", `unhandled method: ${cmd.method}`);
|
|
1113
1485
|
}
|
|
@@ -1164,15 +1536,46 @@
|
|
|
1164
1536
|
}
|
|
1165
1537
|
};
|
|
1166
1538
|
|
|
1539
|
+
// extension/src/sw/observers.ts
|
|
1540
|
+
var SCRIPT_ID = "chrome-mcp-observers";
|
|
1541
|
+
async function syncObserverScript(policy, log) {
|
|
1542
|
+
try {
|
|
1543
|
+
const existing = await chrome.scripting.getRegisteredContentScripts({ ids: [SCRIPT_ID] }).catch(() => []);
|
|
1544
|
+
const wanted = policy ? observerMatches(policy) : [];
|
|
1545
|
+
if (wanted.length === 0) {
|
|
1546
|
+
if (existing.length > 0) {
|
|
1547
|
+
await chrome.scripting.unregisterContentScripts({ ids: [SCRIPT_ID] });
|
|
1548
|
+
log("observers: unregistered (disabled by policy)");
|
|
1549
|
+
}
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const spec = {
|
|
1553
|
+
id: SCRIPT_ID,
|
|
1554
|
+
js: ["page-hook.js"],
|
|
1555
|
+
matches: wanted,
|
|
1556
|
+
runAt: "document_start",
|
|
1557
|
+
allFrames: true,
|
|
1558
|
+
world: "MAIN",
|
|
1559
|
+
persistAcrossSessions: false
|
|
1560
|
+
};
|
|
1561
|
+
if (existing.length > 0) await chrome.scripting.updateContentScripts([spec]);
|
|
1562
|
+
else await chrome.scripting.registerContentScripts([spec]);
|
|
1563
|
+
log(`observers: registered for ${wanted.join(", ")}`);
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
log(`observers: registration failed (${err instanceof Error ? err.message : String(err)})`);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1167
1569
|
// extension/src/sw/background.ts
|
|
1168
1570
|
var KEEPALIVE_ALARM = "chrome-mcp-keepalive";
|
|
1169
1571
|
var currentPolicy = null;
|
|
1170
|
-
var executor = new ChromeExecutor();
|
|
1572
|
+
var executor = new ChromeExecutor(() => currentPolicy);
|
|
1171
1573
|
var ws = new WsClient({
|
|
1172
1574
|
onCommand: (cmd) => void router.dispatch(cmd),
|
|
1173
1575
|
onState: (state) => void persistState(state),
|
|
1174
1576
|
onPolicy: (policy) => {
|
|
1175
1577
|
currentPolicy = policy;
|
|
1578
|
+
void syncObserverScript(policy, (m) => console.debug("[chrome-mcp]", m));
|
|
1176
1579
|
},
|
|
1177
1580
|
log: (m) => console.debug("[chrome-mcp]", m)
|
|
1178
1581
|
});
|