@mehmoodqureshi/chrome-mcp 0.9.2 → 0.9.4

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.
@@ -285,29 +285,31 @@
285
285
  }
286
286
 
287
287
  // shared/snapshot.ts
288
- function collectSnapshot(interactiveOnly = true, max = 200) {
288
+ function collectSnapshot(interactiveOnly = true, max = 200, locator = null) {
289
289
  const INTERACTIVE = "a[href],button,input,select,textarea,[role=button],[role=link],[role=tab],[role=checkbox],[role=radio],[role=menuitem],[role=option],[role=switch],[contenteditable=true],[onclick]";
290
290
  const LANDMARK = "h1,h2,h3,[role=heading],nav,main,header,footer,[role=navigation]";
291
291
  const sel = interactiveOnly ? INTERACTIVE : `${INTERACTIVE},${LANDMARK}`;
292
292
  const visible = (el) => {
293
- const r = el.getBoundingClientRect();
294
- if (r.width === 0 && r.height === 0) return false;
295
- const s = window.getComputedStyle(el);
296
- if (s.visibility === "hidden" || s.display === "none") return false;
297
- const cv = el.checkVisibility;
298
- if (typeof cv === "function") {
293
+ const h = el;
294
+ if (typeof h.checkVisibility === "function") {
299
295
  try {
300
- return cv.call(el, { checkOpacity: false, checkVisibilityCSS: true });
296
+ if (!h.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true })) return false;
297
+ const r2 = h.getBoundingClientRect();
298
+ return r2.width !== 0 || r2.height !== 0;
301
299
  } catch {
302
300
  }
303
301
  }
302
+ const r = h.getBoundingClientRect();
303
+ if (r.width === 0 && r.height === 0) return false;
304
+ const s = window.getComputedStyle(h);
305
+ if (s.visibility === "hidden" || s.display === "none") return false;
304
306
  let p = el.parentElement;
305
307
  while (p) {
306
308
  const ps = window.getComputedStyle(p);
307
309
  if (ps.display === "none" || ps.visibility === "hidden") return false;
308
310
  p = p.parentElement;
309
311
  }
310
- if (el.offsetParent === null && s.position !== "fixed") return false;
312
+ if (h.offsetParent === null && s.position !== "fixed") return false;
311
313
  return true;
312
314
  };
313
315
  const accName = (el) => {
@@ -350,46 +352,98 @@
350
352
  if (candidates.length >= max) return;
351
353
  let matched;
352
354
  try {
353
- matched = Array.from(root.querySelectorAll(sel));
355
+ matched = root.querySelectorAll(sel);
354
356
  } catch {
355
357
  matched = [];
356
358
  }
357
- for (const el of matched) {
359
+ for (let i = 0; i < matched.length; i++) {
358
360
  if (candidates.length >= max) break;
361
+ const el = matched[i];
359
362
  if (seen.has(el)) continue;
360
363
  seen.add(el);
361
364
  candidates.push(el);
362
365
  }
363
366
  let hosts;
364
367
  try {
365
- hosts = Array.from(root.querySelectorAll("*"));
368
+ hosts = root.querySelectorAll("*");
366
369
  } catch {
367
370
  hosts = [];
368
371
  }
369
- for (const host of hosts) {
372
+ for (let i = 0; i < hosts.length; i++) {
370
373
  if (candidates.length >= max) break;
371
- const sr = host.shadowRoot;
374
+ const sr = hosts[i].shadowRoot;
372
375
  if (sr) collect(sr);
373
376
  }
374
377
  };
375
378
  collect(document);
376
- const els = candidates.filter(visible);
377
- const nodes = [];
378
- let n = 0;
379
- for (const el of els) {
380
- if (nodes.length >= max) break;
381
- const ref = `e${++n}`;
382
- el.setAttribute("data-mcp-ref", ref);
383
- const node = { ref, role: roleOf(el), name: accName(el), tag: el.tagName.toLowerCase() };
379
+ const read = (el) => {
380
+ const node = { role: roleOf(el), name: accName(el), tag: el.tagName.toLowerCase() };
384
381
  const isSecret = el.tagName === "INPUT" && el.type === "password";
385
382
  if (isSecret) node.secret = true;
386
383
  const v = el.value;
387
384
  if (!isSecret && typeof v === "string" && v) node.value = v.slice(0, 200);
388
385
  if (el.disabled) node.disabled = true;
389
386
  if (el.checked) node.checked = true;
390
- nodes.push(node);
387
+ return { el, node };
388
+ };
389
+ const els = candidates.filter(visible);
390
+ let picked;
391
+ let truncated = false;
392
+ let nearby;
393
+ let refPrefix = "e";
394
+ if (locator && (locator.role !== void 0 || locator.name !== void 0)) {
395
+ const norm = (x) => x.replace(/\s+/g, " ").trim().toLowerCase();
396
+ const wantRole = locator.role !== void 0 ? norm(locator.role) : null;
397
+ const wantName = locator.name;
398
+ const score = (role, name) => {
399
+ if (wantRole !== null && norm(role) !== wantRole) return 0;
400
+ if (!wantName) return 1;
401
+ const have = norm(name);
402
+ const need = norm(wantName);
403
+ if (!have) return 0;
404
+ if (name.trim() === wantName.trim()) return 4;
405
+ if (have === need) return 3;
406
+ if (have.startsWith(need)) return 2;
407
+ if (have.includes(need)) return 1;
408
+ return 0;
409
+ };
410
+ let best = 0;
411
+ picked = [];
412
+ const sameRole = [];
413
+ for (const el of els) {
414
+ const s = read(el);
415
+ const sc = score(s.node.role, s.node.name);
416
+ if (sc === 0) {
417
+ if ((wantRole === null || norm(s.node.role) === wantRole) && sameRole.length < 8) {
418
+ sameRole.push(`${s.node.role} "${s.node.name}"`);
419
+ }
420
+ continue;
421
+ }
422
+ if (sc > best) {
423
+ best = sc;
424
+ picked = [s];
425
+ } else if (sc === best) {
426
+ picked.push(s);
427
+ }
428
+ }
429
+ if (picked.length === 0) nearby = sameRole;
430
+ refPrefix = `l${Date.now().toString(36).slice(-4)}-`;
431
+ } else {
432
+ picked = [];
433
+ for (const el of els) {
434
+ if (picked.length >= max) break;
435
+ picked.push(read(el));
436
+ }
437
+ truncated = els.length > picked.length;
391
438
  }
392
- return { url: location.href, title: document.title, nodes, truncated: els.length > nodes.length };
439
+ const nodes = [];
440
+ let n = 0;
441
+ for (const { el, node } of picked) {
442
+ const ref = `${refPrefix}${++n}`;
443
+ el.setAttribute("data-mcp-ref", ref);
444
+ nodes.push({ ref, ...node });
445
+ }
446
+ return { url: location.href, title: document.title, nodes, truncated, ...nearby ? { nearby } : {} };
393
447
  }
394
448
 
395
449
  // shared/page-fns.ts
@@ -440,177 +494,197 @@
440
494
  return { dx, dy, exact: true, scrollX: win.scrollX, scrollY: win.scrollY };
441
495
  };
442
496
  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
497
  const setValue = (node, next) => {
446
498
  const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), "value")?.set;
447
499
  if (setter) setter.call(node, next);
448
500
  else node.value = next;
449
501
  node.dispatchEvent(new Event("input", { bubbles: true }));
450
502
  };
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;
503
+ const perform = (el2) => {
504
+ const missing = sel !== null && el2 === null;
505
+ switch (a.op) {
506
+ case "probe":
507
+ return { found: true, url: location.href, title: document.title };
508
+ case "text": {
509
+ if (missing) return { found: false };
510
+ const root = el2 ?? document.body;
511
+ return { found: true, text: root ? root.innerText ?? "" : "" };
516
512
  }
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 };
513
+ case "html": {
514
+ if (missing) return { found: false };
515
+ const root = el2 ?? document.documentElement;
516
+ if (!root) return { found: true, html: "" };
517
+ const outer = a.outer === true || !sel;
518
+ return { found: true, html: outer ? root.outerHTML : root.innerHTML };
559
519
  }
560
- if (op === "remove") {
561
- store.removeItem(String(a.key));
562
- return { found: true, ok: true };
520
+ case "click": {
521
+ if (!el2) return { found: false };
522
+ el2.scrollIntoView({ block: "center" });
523
+ el2.click();
524
+ return { found: true };
563
525
  }
564
- if (op === "clear") {
565
- store.clear();
566
- return { found: true, ok: true };
526
+ case "type": {
527
+ const node = el2;
528
+ if (!node) return { found: false };
529
+ node.focus();
530
+ const next = (a.clear ? "" : node.value ?? "") + (a.text ?? "");
531
+ setValue(node, next);
532
+ node.dispatchEvent(new Event("change", { bubbles: true }));
533
+ return { found: true };
567
534
  }
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) ?? "";
535
+ case "focus": {
536
+ const node = el2;
537
+ if (!node) return { found: false };
538
+ node.focus();
539
+ if (a.clear) setValue(node, "");
540
+ return { found: true };
573
541
  }
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);
542
+ case "point": {
543
+ if (!el2) return { found: false };
544
+ el2.scrollIntoView({ block: "center", inline: "center" });
545
+ const r = el2.getBoundingClientRect();
546
+ const off = frameOffset();
547
+ return {
548
+ found: true,
549
+ x: r.left + r.width / 2 + off.dx,
550
+ y: r.top + r.height / 2 + off.dy,
551
+ // false => this frame's coordinates cannot be mapped to the top viewport.
552
+ exact: off.exact
586
553
  };
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;
554
+ }
555
+ case "hover": {
556
+ if (!el2) return { found: false };
557
+ el2.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
558
+ el2.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
559
+ return { found: true };
560
+ }
561
+ case "select": {
562
+ const node = el2;
563
+ if (!node || !node.options) return { found: false };
564
+ const want = new Set(a.values ?? []);
565
+ let matched = false;
566
+ for (const opt of Array.from(node.options)) {
567
+ const on = want.has(opt.value) || want.has(opt.label) || want.has(opt.text);
568
+ opt.selected = on;
569
+ if (on) matched = true;
570
+ }
571
+ node.dispatchEvent(new Event("input", { bubbles: true }));
572
+ node.dispatchEvent(new Event("change", { bubbles: true }));
573
+ return { found: true, matched };
574
+ }
575
+ case "measure": {
576
+ const d = document.documentElement;
577
+ const dims = {
578
+ w: window.innerWidth,
579
+ h: window.innerHeight,
580
+ fullW: Math.max(d.scrollWidth, d.clientWidth),
581
+ fullH: Math.max(d.scrollHeight, d.clientHeight),
582
+ dpr: window.devicePixelRatio || 1,
583
+ scrollX: window.scrollX,
584
+ scrollY: window.scrollY
602
585
  };
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);
586
+ if (!sel) return { found: true, dims, element: null, missing: false };
587
+ if (!el2) return { found: true, dims, element: null, missing: true };
588
+ el2.scrollIntoView({ block: "center", inline: "center" });
589
+ const r = el2.getBoundingClientRect();
590
+ const off = frameOffset();
591
+ return {
592
+ found: true,
593
+ dims,
594
+ element: {
595
+ x: r.left + off.dx + off.scrollX,
596
+ y: r.top + off.dy + off.scrollY,
597
+ w: r.width,
598
+ h: r.height
599
+ },
600
+ missing: false,
601
+ exact: off.exact
607
602
  };
608
- tick();
609
- });
603
+ }
604
+ case "scroll": {
605
+ if (el2) el2.scrollIntoView({ block: "center" });
606
+ else if (a.x != null || a.y != null) window.scrollTo(a.x ?? 0, a.y ?? 0);
607
+ else window.scrollBy(a.deltaX ?? 0, a.deltaY ?? 0);
608
+ return { found: !missing };
609
+ }
610
+ case "storage": {
611
+ const store = a.session ? window.sessionStorage : window.localStorage;
612
+ const op = a.storageOp;
613
+ if (op === "set") {
614
+ store.setItem(String(a.key), String(a.value ?? ""));
615
+ return { found: true, ok: true };
616
+ }
617
+ if (op === "remove") {
618
+ store.removeItem(String(a.key));
619
+ return { found: true, ok: true };
620
+ }
621
+ if (op === "clear") {
622
+ store.clear();
623
+ return { found: true, ok: true };
624
+ }
625
+ if (a.key) return { found: true, ok: true, value: store.getItem(a.key) };
626
+ const entries = {};
627
+ for (let i = 0; i < store.length; i++) {
628
+ const k = store.key(i);
629
+ if (k) entries[k] = store.getItem(k) ?? "";
630
+ }
631
+ return { found: true, ok: true, entries };
632
+ }
633
+ // -- the two polling ops: one injection that resolves in-page, rather than
634
+ // one executeScript round-trip per tick --
635
+ case "waitSelector": {
636
+ const deadline = Date.now() + (a.timeoutMs ?? 5e3);
637
+ const every = a.interval ?? 120;
638
+ return new Promise((resolve) => {
639
+ const tick = () => {
640
+ if (sel && deepQuery(sel)) return resolve({ found: true });
641
+ if (Date.now() > deadline) return resolve({ found: false });
642
+ setTimeout(tick, every);
643
+ };
644
+ tick();
645
+ });
646
+ }
647
+ case "waitFor": {
648
+ const deadline = Date.now() + (a.timeoutMs ?? 3e4);
649
+ const every = a.interval ?? 150;
650
+ const want = typeof a.textContains === "string" && a.textContains.length > 0 ? a.textContains : null;
651
+ const gone = a.gone === true;
652
+ return new Promise((resolve) => {
653
+ const hit = () => {
654
+ let present;
655
+ if (sel) present = !!deepQuery(sel);
656
+ else if (want) present = (document.body?.innerText ?? "").includes(want);
657
+ else present = true;
658
+ return gone ? !present : present;
659
+ };
660
+ const tick = () => {
661
+ if (hit()) return resolve({ found: true, matched: true });
662
+ if (Date.now() > deadline) return resolve({ found: true, matched: false });
663
+ setTimeout(tick, every);
664
+ };
665
+ tick();
666
+ });
667
+ }
668
+ default:
669
+ return { found: false, error: `unknown page op: ${String(a.op)}` };
610
670
  }
611
- default:
612
- return { found: false, error: `unknown page op: ${String(a.op)}` };
671
+ };
672
+ const el = sel ? deepQuery(sel) : null;
673
+ const waits = a.op === "click" || a.op === "type" || a.op === "focus" || a.op === "point" || a.op === "hover" || a.op === "select";
674
+ if (waits && sel && !el && a.timeoutMs && a.timeoutMs > 0) {
675
+ const deadline = Date.now() + a.timeoutMs;
676
+ const every = a.interval ?? 120;
677
+ return new Promise((resolve) => {
678
+ const tick = () => {
679
+ const hit = deepQuery(sel);
680
+ if (hit) return resolve(perform(hit));
681
+ if (Date.now() > deadline) return resolve({ found: false });
682
+ setTimeout(tick, every);
683
+ };
684
+ setTimeout(tick, every);
685
+ });
613
686
  }
687
+ return perform(el);
614
688
  }
615
689
 
616
690
  // shared/observers.ts
@@ -676,13 +750,18 @@
676
750
  }
677
751
 
678
752
  // shared/screenshot.ts
753
+ var DEFAULT_SCREENSHOT_FORMAT = "jpeg";
754
+ var DEFAULT_JPEG_QUALITY = 70;
755
+ var DEFAULT_SCREENSHOT_SCALE = 1;
679
756
  var MAX_CAPTURE_PX = 16384;
680
757
  function planScreenshot(dims, opts = {}) {
758
+ const dpr = dims.dpr && dims.dpr > 0 ? dims.dpr : 1;
759
+ const scale = (opts.scale && opts.scale > 0 ? opts.scale : DEFAULT_SCREENSHOT_SCALE) / dpr;
681
760
  if (opts.element) {
682
761
  const realH = Math.max(1, Math.round(opts.element.h));
683
762
  const clipH = Math.min(opts.element.h, MAX_CAPTURE_PX);
684
763
  return {
685
- clip: { x: opts.element.x, y: opts.element.y, width: opts.element.w, height: clipH, scale: 1 },
764
+ clip: { x: opts.element.x, y: opts.element.y, width: opts.element.w, height: clipH, scale },
686
765
  captureBeyondViewport: true,
687
766
  width: Math.max(1, Math.round(opts.element.w)),
688
767
  height: Math.min(realH, MAX_CAPTURE_PX),
@@ -693,7 +772,7 @@
693
772
  if (opts.fullPage) {
694
773
  const clipH = Math.min(dims.fullH, MAX_CAPTURE_PX);
695
774
  return {
696
- clip: { x: 0, y: 0, width: dims.fullW, height: clipH, scale: 1 },
775
+ clip: { x: 0, y: 0, width: dims.fullW, height: clipH, scale },
697
776
  captureBeyondViewport: true,
698
777
  width: dims.fullW,
699
778
  height: clipH,
@@ -702,6 +781,7 @@
702
781
  };
703
782
  }
704
783
  return {
784
+ ...scale !== 1 ? { clip: { x: dims.scrollX ?? 0, y: dims.scrollY ?? 0, width: dims.w, height: dims.h, scale } } : {},
705
785
  captureBeyondViewport: false,
706
786
  width: dims.w,
707
787
  height: dims.h,
@@ -794,16 +874,21 @@
794
874
  async function targetTab(cmd) {
795
875
  return cmd.tabId ? parseTabId(cmd.tabId) : currentTabId();
796
876
  }
797
- async function urlForCommand(cmd) {
877
+ var TABLESS = /* @__PURE__ */ new Set(["tabs_list", "tab_new", "ping_probe", "download_file"]);
878
+ async function resolveTab(cmd) {
879
+ if (TABLESS.has(cmd.method)) return null;
880
+ return targetTab(cmd);
881
+ }
882
+ async function urlForCommand(cmd, tab) {
798
883
  if (cmd.method === "navigate") {
799
884
  const u = cmd.params.url;
800
885
  return typeof u === "string" ? u : "";
801
886
  }
802
- return observedTabUrl(cmd);
887
+ return observedTabUrl(cmd, tab);
803
888
  }
804
- async function observedTabUrl(cmd) {
889
+ async function observedTabUrl(cmd, tab) {
805
890
  try {
806
- const tabId = await targetTab(cmd);
891
+ const tabId = tab ?? await targetTab(cmd);
807
892
  const t = await chrome.tabs.get(tabId);
808
893
  return t.url ?? "";
809
894
  } catch {
@@ -836,14 +921,61 @@
836
921
  if (outcomes.length === 0) throw new CmdError("TARGET_GONE", "the page returned no result for this command");
837
922
  return outcomes.find((o) => o.found === true) ?? outcomes[0];
838
923
  }
839
- async function waitComplete(tabId, timeoutMs = 3e4) {
924
+ function waitUntilOf(cmd) {
925
+ const w = cmd.params.waitUntil;
926
+ return w === "domcontentloaded" || w === "networkidle" ? w : "load";
927
+ }
928
+ function armNavigationWait(tabId, waitUntil, timeoutMs = 3e4) {
840
929
  const start = Date.now();
841
- for (; ; ) {
842
- const t = await chrome.tabs.get(tabId);
843
- if (t.status === "complete") return;
844
- if (Date.now() - start > timeoutMs) return;
845
- await delay(100);
846
- }
930
+ const NETWORK_QUIET_MS = 500;
931
+ let done = () => void 0;
932
+ const finished = new Promise((resolve) => {
933
+ done = resolve;
934
+ });
935
+ const isTop = (d) => d.tabId === tabId && d.frameId === 0 && d.timeStamp >= start - 1;
936
+ const onDom = (d) => {
937
+ if (isTop(d) && waitUntil === "domcontentloaded") done();
938
+ };
939
+ const onCompleted = (d) => {
940
+ if (!isTop(d)) return;
941
+ if (waitUntil === "networkidle") setTimeout(done, NETWORK_QUIET_MS);
942
+ else done();
943
+ };
944
+ const onSameDoc = (d) => {
945
+ if (isTop(d)) done();
946
+ };
947
+ const onError = (d) => {
948
+ if (isTop(d)) done();
949
+ };
950
+ const onUpdated = (id, info) => {
951
+ if (id === tabId && info.status === "complete") done();
952
+ };
953
+ chrome.webNavigation.onDOMContentLoaded.addListener(onDom);
954
+ chrome.webNavigation.onCompleted.addListener(onCompleted);
955
+ chrome.webNavigation.onHistoryStateUpdated.addListener(onSameDoc);
956
+ chrome.webNavigation.onReferenceFragmentUpdated.addListener(onSameDoc);
957
+ chrome.webNavigation.onErrorOccurred.addListener(onError);
958
+ chrome.tabs.onUpdated.addListener(onUpdated);
959
+ const cleanup = () => {
960
+ chrome.webNavigation.onDOMContentLoaded.removeListener(onDom);
961
+ chrome.webNavigation.onCompleted.removeListener(onCompleted);
962
+ chrome.webNavigation.onHistoryStateUpdated.removeListener(onSameDoc);
963
+ chrome.webNavigation.onReferenceFragmentUpdated.removeListener(onSameDoc);
964
+ chrome.webNavigation.onErrorOccurred.removeListener(onError);
965
+ chrome.tabs.onUpdated.removeListener(onUpdated);
966
+ };
967
+ return {
968
+ async wait() {
969
+ const timer = setTimeout(done, timeoutMs);
970
+ try {
971
+ await finished;
972
+ } finally {
973
+ clearTimeout(timer);
974
+ cleanup();
975
+ }
976
+ },
977
+ cancel: cleanup
978
+ };
847
979
  }
848
980
  function resolveSelector(cmd) {
849
981
  const s = cmd.params.selector;
@@ -855,29 +987,73 @@
855
987
  function selectorOf(cmd) {
856
988
  return resolveSelector(cmd);
857
989
  }
858
- async function waitForSelector(tabId, selector, frameIds, timeoutMs = 5e3) {
990
+ var ACTION_WAIT_MS = 5e3;
991
+ var withWait = (a) => ({ ...a, timeoutMs: ACTION_WAIT_MS, interval: 120 });
992
+ async function waitForSelector(tabId, selector, frameIds, timeoutMs = ACTION_WAIT_MS) {
859
993
  const out = await execOp(tabId, { op: "waitSelector", selector, timeoutMs, interval: 120 }, frameIds);
860
994
  return out.found === true;
861
995
  }
996
+ var DEBUGGER_GRACE_MS = 1500;
997
+ var dbgSessions = /* @__PURE__ */ new Map();
998
+ function forgetDebugger(tabId) {
999
+ const s = dbgSessions.get(tabId);
1000
+ if (s?.detachTimer) clearTimeout(s.detachTimer);
1001
+ dbgSessions.delete(tabId);
1002
+ }
1003
+ chrome.debugger.onDetach.addListener((source) => {
1004
+ if (source.tabId !== void 0) forgetDebugger(source.tabId);
1005
+ });
1006
+ chrome.tabs.onRemoved.addListener((tabId) => forgetDebugger(tabId));
1007
+ function scheduleDetach(tabId) {
1008
+ const s = dbgSessions.get(tabId);
1009
+ if (!s) return;
1010
+ if (s.detachTimer) clearTimeout(s.detachTimer);
1011
+ s.detachTimer = setTimeout(() => {
1012
+ dbgSessions.delete(tabId);
1013
+ chrome.debugger.detach({ tabId }).catch(() => void 0);
1014
+ }, DEBUGGER_GRACE_MS);
1015
+ }
1016
+ async function ensureAttached(tabId) {
1017
+ const s = dbgSessions.get(tabId);
1018
+ if (s) {
1019
+ if (s.detachTimer) clearTimeout(s.detachTimer);
1020
+ s.detachTimer = null;
1021
+ return;
1022
+ }
1023
+ await chrome.debugger.attach({ tabId }, "1.3");
1024
+ dbgSessions.set(tabId, { detachTimer: null });
1025
+ }
1026
+ var DETACHED_RE = /not attached|no target with given id|detached/i;
862
1027
  async function withDebugger(tabId, fn) {
863
1028
  return locks.run(`dbg:${tabId}`, async () => {
864
1029
  const target = { tabId };
865
- await chrome.debugger.attach(target, "1.3");
1030
+ await ensureAttached(tabId);
866
1031
  try {
867
1032
  return await fn(target);
1033
+ } catch (err) {
1034
+ if (!DETACHED_RE.test(String(err?.message ?? err))) throw err;
1035
+ forgetDebugger(tabId);
1036
+ await ensureAttached(tabId);
1037
+ return await fn(target);
868
1038
  } finally {
869
- await chrome.debugger.detach(target).catch(() => void 0);
1039
+ scheduleDetach(tabId);
870
1040
  }
871
1041
  });
872
1042
  }
873
- async function trustedType(tabId, selector, text, clear, frameIds) {
874
- const focused = await execOp(tabId, { op: "focus", selector, clear }, frameIds);
1043
+ async function trustedType(tabId, selector, text, clear, frameIds, pressEnter = false) {
1044
+ const focused = await execOp(tabId, withWait({ op: "focus", selector, clear }), frameIds);
875
1045
  if (!focused.found) return false;
876
- await withDebugger(tabId, (t) => chrome.debugger.sendCommand(t, "Input.insertText", { text }));
1046
+ await withDebugger(tabId, async (t) => {
1047
+ await chrome.debugger.sendCommand(t, "Input.insertText", { text });
1048
+ if (!pressEnter) return;
1049
+ for (const type of ["keyDown", "keyUp"]) {
1050
+ await chrome.debugger.sendCommand(t, "Input.dispatchKeyEvent", { type, key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
1051
+ }
1052
+ });
877
1053
  return true;
878
1054
  }
879
1055
  async function trustedClick(tabId, selector, frameIds) {
880
- const pt = await execOp(tabId, { op: "point", selector }, frameIds);
1056
+ const pt = await execOp(tabId, withWait({ op: "point", selector }), frameIds);
881
1057
  if (!pt.found) return "missing";
882
1058
  if (pt.exact === false) return "inexact";
883
1059
  await withDebugger(tabId, async (t) => {
@@ -896,12 +1072,28 @@
896
1072
  missing: found.missing === true
897
1073
  };
898
1074
  }
899
- async function screenshotViaDebugger(tabId, fullPage, selector, frameIds) {
1075
+ function encodingOf(cmd) {
1076
+ const f = cmd.params.format;
1077
+ const q = cmd.params.quality;
1078
+ const sc = cmd.params.scale;
1079
+ return {
1080
+ format: f === "png" || f === "jpeg" ? f : DEFAULT_SCREENSHOT_FORMAT,
1081
+ quality: typeof q === "number" && q >= 1 && q <= 100 ? Math.round(q) : DEFAULT_JPEG_QUALITY,
1082
+ scale: typeof sc === "number" && sc > 0 ? sc : void 0
1083
+ };
1084
+ }
1085
+ async function screenshotViaDebugger(tabId, fullPage, enc, selector, frameIds) {
900
1086
  const measured = await measurePage(tabId, selector, frameIds);
901
1087
  if (!measured) throw new CmdError("CDP_ERROR", "could not read page dimensions");
902
1088
  if (selector && measured.missing) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${selector}`);
903
- const plan = planScreenshot(measured.dims, { fullPage, element: measured.element });
904
- const params = { format: "png", captureBeyondViewport: plan.captureBeyondViewport };
1089
+ const plan = planScreenshot(measured.dims, { fullPage, element: measured.element, scale: enc.scale });
1090
+ const params = {
1091
+ format: enc.format,
1092
+ captureBeyondViewport: plan.captureBeyondViewport,
1093
+ // Skip the slow PNG compression pass; bytes-on-the-wire matter less than latency here.
1094
+ optimizeForSpeed: true
1095
+ };
1096
+ if (enc.format === "jpeg") params.quality = enc.quality;
905
1097
  if (plan.clip) params.clip = plan.clip;
906
1098
  const data = await withDebugger(tabId, async (target) => {
907
1099
  const res = await chrome.debugger.sendCommand(target, "Page.captureScreenshot", params);
@@ -909,14 +1101,14 @@
909
1101
  });
910
1102
  return {
911
1103
  dataBase64: data,
912
- mimeType: "image/png",
1104
+ mimeType: enc.format === "jpeg" ? "image/jpeg" : "image/png",
913
1105
  width: plan.width,
914
1106
  height: plan.height,
915
1107
  truncated: plan.truncated,
916
1108
  fullHeight: plan.fullHeight
917
1109
  };
918
1110
  }
919
- async function screenshotViaVisibleTab(tabId, fullPage) {
1111
+ async function screenshotViaVisibleTab(tabId, fullPage, enc) {
920
1112
  let t = await chrome.tabs.get(tabId);
921
1113
  if (!t.active) {
922
1114
  await chrome.tabs.update(tabId, { active: true });
@@ -929,12 +1121,15 @@
929
1121
  () => ({ w: window.innerWidth, h: window.innerHeight, full: document.documentElement.scrollHeight }),
930
1122
  []
931
1123
  );
932
- const dataUrl = await chrome.tabs.captureVisibleTab(t.windowId, { format: "png" });
1124
+ const dataUrl = await chrome.tabs.captureVisibleTab(
1125
+ t.windowId,
1126
+ enc.format === "jpeg" ? { format: "jpeg", quality: enc.quality } : { format: "png" }
1127
+ );
933
1128
  const viewportH = dims?.h ?? 0;
934
1129
  const fullH = dims?.full ?? viewportH;
935
1130
  return {
936
1131
  dataBase64: dataUrl.split(",")[1] ?? "",
937
- mimeType: "image/png",
1132
+ mimeType: enc.format === "jpeg" ? "image/jpeg" : "image/png",
938
1133
  width: dims?.w ?? 0,
939
1134
  height: viewportH,
940
1135
  truncated: fullPage && fullH > viewportH,
@@ -1036,7 +1231,12 @@
1036
1231
  }
1037
1232
  return allowed;
1038
1233
  }
1039
- async run(cmd) {
1234
+ /**
1235
+ * Execute one command. `tab` is the id `resolveTab` already produced for it
1236
+ * (the router resolves once and shares it); omitted → resolve here.
1237
+ */
1238
+ async run(cmd, tab = null) {
1239
+ const targetTab2 = async (c) => tab ?? (c.tabId ? parseTabId(c.tabId) : currentTabId());
1040
1240
  switch (cmd.method) {
1041
1241
  case "ping_probe":
1042
1242
  return {};
@@ -1071,8 +1271,12 @@
1071
1271
  return { id: created.id, reused: false, needsNav: false };
1072
1272
  });
1073
1273
  if (claim.needsNav) {
1074
- await chrome.tabs.update(claim.id, { url });
1075
- await waitComplete(claim.id);
1274
+ const nav = armNavigationWait(claim.id, "load");
1275
+ await chrome.tabs.update(claim.id, { url }).catch((err) => {
1276
+ nav.cancel();
1277
+ throw err;
1278
+ });
1279
+ await nav.wait();
1076
1280
  }
1077
1281
  if (active) {
1078
1282
  const t = await chrome.tabs.get(claim.id);
@@ -1088,42 +1292,56 @@
1088
1292
  }
1089
1293
  // -- navigation --
1090
1294
  case "navigate": {
1091
- const id = await targetTab(cmd);
1295
+ const id = await targetTab2(cmd);
1092
1296
  const url = String(cmd.params.url);
1093
- await chrome.tabs.update(id, { url });
1094
- await waitComplete(id);
1297
+ const nav = armNavigationWait(id, waitUntilOf(cmd));
1298
+ await chrome.tabs.update(id, { url }).catch((err) => {
1299
+ nav.cancel();
1300
+ throw err;
1301
+ });
1302
+ await nav.wait();
1095
1303
  const t = await chrome.tabs.get(id);
1096
1304
  return { url: t.url ?? url, title: t.title ?? "" };
1097
1305
  }
1098
1306
  case "back": {
1099
- const id = await targetTab(cmd);
1100
- await chrome.tabs.goBack(id).catch(() => void 0);
1307
+ const id = await targetTab2(cmd);
1308
+ const nav = armNavigationWait(id, "load", 15e3);
1309
+ const moved = await chrome.tabs.goBack(id).then(() => true, () => false);
1310
+ if (moved) await nav.wait();
1311
+ else nav.cancel();
1101
1312
  const t = await chrome.tabs.get(id);
1102
1313
  return { url: t.url ?? "", title: t.title ?? "" };
1103
1314
  }
1104
1315
  case "forward": {
1105
- const id = await targetTab(cmd);
1106
- await chrome.tabs.goForward(id).catch(() => void 0);
1316
+ const id = await targetTab2(cmd);
1317
+ const nav = armNavigationWait(id, "load", 15e3);
1318
+ const moved = await chrome.tabs.goForward(id).then(() => true, () => false);
1319
+ if (moved) await nav.wait();
1320
+ else nav.cancel();
1107
1321
  const t = await chrome.tabs.get(id);
1108
1322
  return { url: t.url ?? "", title: t.title ?? "" };
1109
1323
  }
1110
1324
  case "reload": {
1111
- const id = await targetTab(cmd);
1112
- await chrome.tabs.reload(id);
1113
- await waitComplete(id);
1325
+ const id = await targetTab2(cmd);
1326
+ const nav = armNavigationWait(id, waitUntilOf(cmd));
1327
+ await chrome.tabs.reload(id).catch((err) => {
1328
+ nav.cancel();
1329
+ throw err;
1330
+ });
1331
+ await nav.wait();
1114
1332
  const t = await chrome.tabs.get(id);
1115
1333
  return { url: t.url ?? "", title: t.title ?? "" };
1116
1334
  }
1117
1335
  // -- reads (isolated world; CSP-safe) --
1118
1336
  case "get_text": {
1119
- const id = await targetTab(cmd);
1337
+ const id = await targetTab2(cmd);
1120
1338
  const frameIds = await this.frames(cmd, id);
1121
1339
  const out = await execOp(id, { op: "text", selector: selectorOf(cmd) ?? null }, frameIds);
1122
1340
  if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${selectorOf(cmd)}`);
1123
1341
  return { text: String(out.text ?? ""), frameId: out.frameId };
1124
1342
  }
1125
1343
  case "get_html": {
1126
- const id = await targetTab(cmd);
1344
+ const id = await targetTab2(cmd);
1127
1345
  const frameIds = await this.frames(cmd, id);
1128
1346
  const out = await execOp(
1129
1347
  id,
@@ -1135,14 +1353,19 @@
1135
1353
  }
1136
1354
  // -- accessibility snapshot (tags elements with data-mcp-ref so refs work) --
1137
1355
  case "snapshot": {
1138
- const id = await targetTab(cmd);
1356
+ const id = await targetTab2(cmd);
1139
1357
  const interactiveOnly = cmd.params.interactiveOnly !== false;
1140
1358
  const max = typeof cmd.params.max === "number" ? cmd.params.max : 200;
1359
+ const loc = cmd.params.locator;
1360
+ const locator = loc && typeof loc === "object" ? {
1361
+ role: typeof loc.role === "string" ? loc.role : void 0,
1362
+ name: typeof loc.name === "string" ? loc.name : void 0
1363
+ } : null;
1141
1364
  const frameIds = await this.frames(cmd, id);
1142
1365
  const raw = await execInTab(
1143
1366
  id,
1144
1367
  collectSnapshot,
1145
- [interactiveOnly, max],
1368
+ [interactiveOnly, max, locator],
1146
1369
  void 0,
1147
1370
  frameIds
1148
1371
  );
@@ -1150,19 +1373,18 @@
1150
1373
  }
1151
1374
  // -- <select> option(s) by value or visible label --
1152
1375
  case "select_option": {
1153
- const id = await targetTab(cmd);
1376
+ const id = await targetTab2(cmd);
1154
1377
  const sel = requireSelector(cmd);
1155
1378
  const frameIds = await this.frames(cmd, id);
1156
1379
  const values = Array.isArray(cmd.params.values) ? cmd.params.values.map(String) : [];
1157
- await waitForSelector(id, sel, frameIds);
1158
- const out = await execOp(id, { op: "select", selector: sel, values }, frameIds);
1380
+ const out = await execOp(id, withWait({ op: "select", selector: sel, values }), frameIds);
1159
1381
  if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
1160
1382
  if (out.matched !== true) throw new CmdError("SELECTOR_NOT_FOUND", `no <select> option matched for ${sel}`);
1161
1383
  return { ok: true, frameId: out.frameId };
1162
1384
  }
1163
1385
  // -- cookies for the tab's URL (chrome.cookies; needs "cookies" permission) --
1164
1386
  case "get_cookies": {
1165
- const id = await targetTab(cmd);
1387
+ const id = await targetTab2(cmd);
1166
1388
  const t = await chrome.tabs.get(id);
1167
1389
  const url = typeof cmd.params.url === "string" ? cmd.params.url : t.url;
1168
1390
  if (!url) throw new CmdError("BAD_ARGS", "no url to read cookies for");
@@ -1181,7 +1403,7 @@
1181
1403
  }
1182
1404
  // -- localStorage / sessionStorage (isolated world) --
1183
1405
  case "storage": {
1184
- const id = await targetTab(cmd);
1406
+ const id = await targetTab2(cmd);
1185
1407
  const op = String(cmd.params.op);
1186
1408
  const key = typeof cmd.params.key === "string" ? cmd.params.key : null;
1187
1409
  const value = typeof cmd.params.value === "string" ? cmd.params.value : null;
@@ -1193,49 +1415,36 @@
1193
1415
  }
1194
1416
  // -- interaction (synthetic events in the isolated world) --
1195
1417
  case "click": {
1196
- const id = await targetTab(cmd);
1418
+ const id = await targetTab2(cmd);
1197
1419
  const sel = requireSelector(cmd);
1198
1420
  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
- }
1202
1421
  if (cmd.params.trusted === true) {
1203
1422
  const verdict = await trustedClick(id, sel, frameIds);
1204
1423
  if (verdict === "missing") throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
1205
1424
  if (verdict === "ok") return { ok: true };
1206
1425
  }
1207
- const out = await execOp(id, { op: "click", selector: sel }, frameIds);
1426
+ const out = await execOp(id, withWait({ op: "click", selector: sel }), frameIds);
1208
1427
  if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
1209
1428
  return { ok: true, frameId: out.frameId, ...cmd.params.trusted === true ? { trusted: false } : {} };
1210
1429
  }
1211
1430
  case "type": {
1212
- const id = await targetTab(cmd);
1431
+ const id = await targetTab2(cmd);
1213
1432
  const sel = requireSelector(cmd);
1214
1433
  const frameIds = await this.frames(cmd, id);
1215
1434
  const text = String(cmd.params.text ?? "");
1216
1435
  const clear = cmd.params.clear === true;
1217
- if (!await waitForSelector(id, sel, frameIds)) {
1218
- throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
1219
- }
1220
1436
  if (cmd.params.trusted === true) {
1221
- if (!await trustedType(id, sel, text, clear, frameIds)) {
1437
+ if (!await trustedType(id, sel, text, clear, frameIds, cmd.params.pressEnter === true)) {
1222
1438
  throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
1223
1439
  }
1224
- if (cmd.params.pressEnter === true) {
1225
- await withDebugger(id, async (t) => {
1226
- for (const type of ["keyDown", "keyUp"]) {
1227
- await chrome.debugger.sendCommand(t, "Input.dispatchKeyEvent", { type, key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
1228
- }
1229
- });
1230
- }
1231
1440
  return { ok: true };
1232
1441
  }
1233
- const out = await execOp(id, { op: "type", selector: sel, text, clear }, frameIds);
1442
+ const out = await execOp(id, withWait({ op: "type", selector: sel, text, clear }), frameIds);
1234
1443
  if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
1235
1444
  return { ok: true, frameId: out.frameId };
1236
1445
  }
1237
1446
  case "press": {
1238
- const id = await targetTab(cmd);
1447
+ const id = await targetTab2(cmd);
1239
1448
  const key = String(cmd.params.key ?? "");
1240
1449
  await execInTab(
1241
1450
  id,
@@ -1250,16 +1459,15 @@
1250
1459
  return { ok: true };
1251
1460
  }
1252
1461
  case "hover": {
1253
- const id = await targetTab(cmd);
1462
+ const id = await targetTab2(cmd);
1254
1463
  const sel = requireSelector(cmd);
1255
1464
  const frameIds = await this.frames(cmd, id);
1256
- await waitForSelector(id, sel, frameIds);
1257
- const out = await execOp(id, { op: "hover", selector: sel }, frameIds);
1465
+ const out = await execOp(id, withWait({ op: "hover", selector: sel }), frameIds);
1258
1466
  if (!out.found) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
1259
1467
  return { ok: true, frameId: out.frameId };
1260
1468
  }
1261
1469
  case "scroll": {
1262
- const id = await targetTab(cmd);
1470
+ const id = await targetTab2(cmd);
1263
1471
  const frameIds = await this.frames(cmd, id);
1264
1472
  await execOp(
1265
1473
  id,
@@ -1281,20 +1489,21 @@
1281
1489
  // concurrent batches) and supports true full-page + element capture.
1282
1490
  // Falls back to captureVisibleTab only if the debugger can't attach.
1283
1491
  case "screenshot": {
1284
- const id = await targetTab(cmd);
1492
+ const id = await targetTab2(cmd);
1285
1493
  const fullPage = cmd.params.fullPage === true;
1286
1494
  const selector = selectorOf(cmd);
1495
+ const enc = encodingOf(cmd);
1287
1496
  const frameIds = await this.frames(cmd, id);
1288
1497
  try {
1289
- return await screenshotViaDebugger(id, fullPage, selector, frameIds);
1498
+ return await screenshotViaDebugger(id, fullPage, enc, selector, frameIds);
1290
1499
  } catch (err) {
1291
1500
  if (err instanceof CmdError && err.code === "SELECTOR_NOT_FOUND") throw err;
1292
- return await screenshotViaVisibleTab(id, fullPage);
1501
+ return await screenshotViaVisibleTab(id, fullPage, enc);
1293
1502
  }
1294
1503
  }
1295
1504
  // -- eval (MAIN world; may be blocked by strict page CSP) --
1296
1505
  case "eval": {
1297
- const id = await targetTab(cmd);
1506
+ const id = await targetTab2(cmd);
1298
1507
  const expr = String(cmd.params.expression ?? "");
1299
1508
  const result = await execInTab(
1300
1509
  id,
@@ -1316,7 +1525,7 @@
1316
1525
  // One injection that polls IN-PAGE until the condition holds or the deadline
1317
1526
  // passes, rather than one executeScript round-trip per tick.
1318
1527
  case "wait_for": {
1319
- const id = await targetTab(cmd);
1528
+ const id = await targetTab2(cmd);
1320
1529
  const frameIds = await this.frames(cmd, id);
1321
1530
  const timeout = typeof cmd.params.timeoutMs === "number" ? cmd.params.timeoutMs : 3e4;
1322
1531
  const start = Date.now();
@@ -1357,7 +1566,7 @@
1357
1566
  }
1358
1567
  // -- upload: set local file(s) on a file <input> via CDP DOM.setFileInputFiles --
1359
1568
  case "upload_file": {
1360
- const id = await targetTab(cmd);
1569
+ const id = await targetTab2(cmd);
1361
1570
  const sel = requireSelector(cmd);
1362
1571
  const files = Array.isArray(cmd.params.files) ? cmd.params.files.map(String) : [];
1363
1572
  if (files.length === 0) throw new CmdError("BAD_ARGS", 'upload_file requires a non-empty "files" array');
@@ -1378,7 +1587,7 @@
1378
1587
  // Read via a probe injection rather than chrome.webNavigation so no
1379
1588
  // extra host-level permission is needed to see them. --
1380
1589
  case "frames_list": {
1381
- const id = await targetTab(cmd);
1590
+ const id = await targetTab2(cmd);
1382
1591
  const probes = await chrome.scripting.executeScript({
1383
1592
  target: { tabId: id, allFrames: true },
1384
1593
  func: pageOp,
@@ -1400,7 +1609,7 @@
1400
1609
  // that loaded before registration gets it injected here, and the caller
1401
1610
  // is told capture only starts from that moment. --
1402
1611
  case "observers": {
1403
- const id = await targetTab(cmd);
1612
+ const id = await targetTab2(cmd);
1404
1613
  const frameIds = await this.frames(cmd, id);
1405
1614
  const opts = {
1406
1615
  console: cmd.params.console === true,
@@ -1462,7 +1671,7 @@
1462
1671
  // -- print_pdf: Page.printToPDF over the same one-op debugger attach the
1463
1672
  // screenshot path uses. Returns base64; the server writes the file. --
1464
1673
  case "print_pdf": {
1465
- const id = await targetTab(cmd);
1674
+ const id = await targetTab2(cmd);
1466
1675
  const params = {
1467
1676
  printBackground: cmd.params.printBackground !== false,
1468
1677
  landscape: cmd.params.landscape === true,
@@ -1510,14 +1719,15 @@
1510
1719
  "no policy has arrived from the chrome-mcp server yet, so this extension is refusing every command"
1511
1720
  );
1512
1721
  }
1722
+ const tab = await resolveTab(cmd);
1513
1723
  if (policy) {
1514
- const url = isUrlGated(cmd.method) ? await urlForCommand(cmd) : "";
1724
+ const url = isUrlGated(cmd.method) ? await urlForCommand(cmd, tab) : "";
1515
1725
  const verdict = evaluatePolicy(url, cmd.method, policy);
1516
1726
  if (!verdict.ok) throw new CmdError("POLICY_DENIED", verdict.reason);
1517
1727
  }
1518
- const data = await this.deps.exec.run(cmd);
1728
+ const data = await this.deps.exec.run(cmd, tab);
1519
1729
  const frame = { type: "result", v: PROTOCOL_VERSION, id: cmd.id, ok: true, data };
1520
- const tabUrl = await observedTabUrl(cmd);
1730
+ const tabUrl = await observedTabUrl(cmd, tab);
1521
1731
  if (tabUrl) frame.tabUrl = tabUrl;
1522
1732
  this.deps.send(frame);
1523
1733
  } catch (err) {
@@ -1570,10 +1780,29 @@
1570
1780
  var KEEPALIVE_ALARM = "chrome-mcp-keepalive";
1571
1781
  var currentPolicy = null;
1572
1782
  var executor = new ChromeExecutor(() => currentPolicy);
1783
+ var RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 1e4];
1784
+ var reconnectAttempt = 0;
1785
+ var reconnectTimer = null;
1786
+ function scheduleReconnect() {
1787
+ if (reconnectTimer) return;
1788
+ const delay2 = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
1789
+ reconnectAttempt++;
1790
+ reconnectTimer = setTimeout(() => {
1791
+ reconnectTimer = null;
1792
+ void ensureConnected();
1793
+ }, delay2);
1794
+ }
1795
+ function clearReconnect() {
1796
+ if (reconnectTimer) clearTimeout(reconnectTimer);
1797
+ reconnectTimer = null;
1798
+ reconnectAttempt = 0;
1799
+ }
1573
1800
  var ws = new WsClient({
1574
1801
  onCommand: (cmd) => void router.dispatch(cmd),
1575
1802
  onState: (state) => {
1576
1803
  void persistState(state);
1804
+ if (state === "connected") clearReconnect();
1805
+ else if (state === "idle") scheduleReconnect();
1577
1806
  if (state === "unauthorized") void adoptBundledPairing();
1578
1807
  },
1579
1808
  onPolicy: (policy) => {
@@ -1672,11 +1901,13 @@
1672
1901
  if (area === "local" && (changes.wsPort || changes.token || changes.profile)) {
1673
1902
  if (ws.state === "unauthorized") ws.state = "idle";
1674
1903
  if (changes.profile) ws.close();
1904
+ clearReconnect();
1675
1905
  void ensureConnected();
1676
1906
  }
1677
1907
  });
1678
1908
  chrome.runtime.onMessage.addListener((msg) => {
1679
1909
  if (msg?.type === "reconnect") {
1910
+ clearReconnect();
1680
1911
  ws.close();
1681
1912
  ws.state = "idle";
1682
1913
  void ensureConnected();