@mehmoodqureshi/chrome-mcp 0.6.7 → 0.7.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.
Files changed (36) hide show
  1. package/README.md +116 -0
  2. package/dist/shared/observers.d.ts +110 -0
  3. package/dist/shared/observers.js +127 -0
  4. package/dist/shared/page-fns.d.ts +46 -0
  5. package/dist/shared/page-fns.js +292 -0
  6. package/dist/shared/policy.d.ts +9 -0
  7. package/dist/shared/policy.js +16 -0
  8. package/dist/shared/protocol.d.ts +11 -2
  9. package/dist/shared/protocol.js +3 -0
  10. package/dist/shared/snapshot.d.ts +2 -0
  11. package/dist/shared/snapshot.js +8 -1
  12. package/dist/src/bridge/workspace.d.ts +6 -0
  13. package/dist/src/bridge/workspace.js +20 -0
  14. package/dist/src/config.js +31 -0
  15. package/dist/src/executor/extension-executor.d.ts +27 -13
  16. package/dist/src/executor/extension-executor.js +53 -12
  17. package/dist/src/executor/stub-executor.d.ts +19 -1
  18. package/dist/src/executor/stub-executor.js +22 -6
  19. package/dist/src/executor/types.d.ts +72 -13
  20. package/dist/src/mcp/audit.d.ts +39 -0
  21. package/dist/src/mcp/audit.js +60 -0
  22. package/dist/src/mcp/helpers.d.ts +4 -4
  23. package/dist/src/mcp/helpers.js +9 -4
  24. package/dist/src/mcp/locate.d.ts +45 -0
  25. package/dist/src/mcp/locate.js +105 -0
  26. package/dist/src/mcp/redact.d.ts +48 -0
  27. package/dist/src/mcp/redact.js +92 -0
  28. package/dist/src/mcp/snapdiff.d.ts +43 -0
  29. package/dist/src/mcp/snapdiff.js +92 -0
  30. package/dist/src/mcp/tools.js +429 -43
  31. package/dist/src/security/policy.d.ts +5 -0
  32. package/dist/src/security/policy.js +6 -0
  33. package/docs/BLUEPRINT.md +15 -1
  34. package/extension-dist/background.js +617 -214
  35. package/extension-dist/page-hook.js +215 -0
  36. 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
- async function execInTab(tabId, func, args = [], world) {
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: { tabId },
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 found = await execInTab(
543
- tabId,
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 execInTab(
570
- tabId,
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 execInTab(
590
- tabId,
591
- (s) => {
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 true;
888
+ return "ok";
607
889
  }
608
- async function measurePage(tabId, selector) {
609
- return execInTab(
610
- tabId,
611
- (sel) => {
612
- const d = document.documentElement;
613
- const dims = {
614
- w: window.innerWidth,
615
- h: window.innerHeight,
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 text = await execInTab(
793
- id,
794
- (sel) => {
795
- const el = sel ? document.querySelector(sel) : document.body;
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 outer = cmd.params.outer === true;
805
- const html = await execInTab(
1127
+ const frameIds = await this.frames(cmd, id);
1128
+ const out = await execOp(
806
1129
  id,
807
- (sel, isOuter) => {
808
- const el = sel ? document.querySelector(sel) : document.documentElement;
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
- return { html: html ?? "" };
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 matched = await execInTab(
835
- id,
836
- (s, vals) => {
837
- const el = document.querySelector(s);
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 res = await execInTab(
882
- id,
883
- (o, k, v, s) => {
884
- const store = s ? window.sessionStorage : window.localStorage;
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
- if (!await waitForSelector(id, sel)) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
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
- if (!await trustedClick(id, sel)) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
916
- return { ok: true };
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 found = await execInTab(
919
- id,
920
- (s) => {
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)) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${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)) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
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 found = await execInTab(
950
- id,
951
- (s, value, doClear) => {
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 waitForSelector(id, sel);
986
- await execInTab(
987
- id,
988
- (s) => {
989
- const el = document.querySelector(s);
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 execInTab(
1263
+ const frameIds = await this.frames(cmd, id);
1264
+ await execOp(
1000
1265
  id,
1001
- (x, y, dx, dy) => {
1002
- if (x != null || y != null) window.scrollTo(x ?? 0, y ?? 0);
1003
- else window.scrollBy(dx ?? 0, dy ?? 0);
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
- [cmd.params.x ?? null, cmd.params.y ?? null, cmd.params.deltaX ?? null, cmd.params.deltaY ?? null]
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 matched = await execInTab(
1323
+ const out = await execOp(
1052
1324
  id,
1053
- (sel, text, gone, timeoutMs, interval) => new Promise((resolve) => {
1054
- const deadline = Date.now() + timeoutMs;
1055
- const hit = () => {
1056
- let present;
1057
- if (sel) present = !!document.querySelector(sel);
1058
- else if (text) present = (document.body?.innerText ?? "").includes(text);
1059
- else present = true;
1060
- return gone ? !present : present;
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)) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${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
  });