mcp-browser-extension 0.9.28 → 0.9.30

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.
@@ -162,7 +162,13 @@
162
162
  // All three are page CONTENT and gate exactly like the reads above.
163
163
  "frames_list",
164
164
  "observers",
165
- "print_pdf"
165
+ "print_pdf",
166
+ // The page's accessibility tree, its cookies (session secrets), and its
167
+ // web storage are content too. A storage write is refused in safe mode by
168
+ // the extension's storage handler; the domain gate here covers every op.
169
+ "snapshot",
170
+ "get_cookies",
171
+ "storage"
166
172
  ]);
167
173
  var MUTATE_CONTENT = /* @__PURE__ */ new Set([
168
174
  "click",
@@ -171,7 +177,8 @@
171
177
  "press",
172
178
  "hover",
173
179
  "scroll",
174
- "fill_form"
180
+ "fill_form",
181
+ "select_option"
175
182
  ]);
176
183
  var NAVIGATION = /* @__PURE__ */ new Set([
177
184
  "navigate",
@@ -188,7 +195,8 @@
188
195
  return MUTATE_CONTENT.has(method) || NAVIGATION.has(method) || TAB_MUTATE.has(method);
189
196
  }
190
197
  function isUrlGated(method) {
191
- return READ_CONTENT.has(method) || MUTATE_CONTENT.has(method) || NAVIGATION.has(method) || method === "eval" || method === "upload_file";
198
+ return READ_CONTENT.has(method) || MUTATE_CONTENT.has(method) || NAVIGATION.has(method) || method === "eval" || method === "upload_file" || // Gated on where the bytes come from: the URL when one is given, else the tab.
199
+ method === "download_file";
192
200
  }
193
201
  function hostOf(url) {
194
202
  try {
@@ -377,6 +385,11 @@
377
385
  const t = labelledby.split(/\s+/).map((id) => document.getElementById(id)?.innerText ?? "").join(" ").trim();
378
386
  if (t) return t;
379
387
  }
388
+ const labels = el.labels;
389
+ if (labels && labels.length > 0) {
390
+ const t = Array.from(labels).map((l) => l.innerText).join(" ").replace(/\s+/g, " ").trim();
391
+ if (t) return t;
392
+ }
380
393
  const ph = el.getAttribute("placeholder");
381
394
  if (ph) return ph.trim();
382
395
  const title = el.getAttribute("title");
@@ -405,8 +418,9 @@
405
418
  };
406
419
  const seen = /* @__PURE__ */ new Set();
407
420
  const candidates = [];
421
+ const cap = Math.max(max * 2, max + 50);
408
422
  const collect = (root) => {
409
- if (candidates.length >= max) return;
423
+ if (candidates.length >= cap) return;
410
424
  let matched;
411
425
  try {
412
426
  matched = root.querySelectorAll(sel);
@@ -414,7 +428,7 @@
414
428
  matched = [];
415
429
  }
416
430
  for (let i = 0; i < matched.length; i++) {
417
- if (candidates.length >= max) break;
431
+ if (candidates.length >= cap) break;
418
432
  const el = matched[i];
419
433
  if (seen.has(el)) continue;
420
434
  seen.add(el);
@@ -427,7 +441,7 @@
427
441
  hosts = [];
428
442
  }
429
443
  for (let i = 0; i < hosts.length; i++) {
430
- if (candidates.length >= max) break;
444
+ if (candidates.length >= cap) break;
431
445
  const sr = hosts[i].shadowRoot;
432
446
  if (sr) collect(sr);
433
447
  }
@@ -491,7 +505,7 @@
491
505
  if (picked.length >= max) break;
492
506
  picked.push(read(el));
493
507
  }
494
- truncated = els.length > picked.length;
508
+ truncated = els.length > picked.length || candidates.length >= cap;
495
509
  }
496
510
  const nodes = [];
497
511
  let n = 0;
@@ -632,6 +646,15 @@
632
646
  el2.click();
633
647
  return { found: true };
634
648
  }
649
+ case "check": {
650
+ if (!el2) return { found: false };
651
+ const box = el2;
652
+ if (box.checked !== (a.checked !== false)) {
653
+ el2.scrollIntoView({ block: "center" });
654
+ el2.click();
655
+ }
656
+ return { found: true, checked: box.checked };
657
+ }
635
658
  case "type": {
636
659
  if (!el2) return { found: false };
637
660
  if (el2.isContentEditable) {
@@ -644,9 +667,24 @@
644
667
  const node = el2;
645
668
  if (!node) return { found: false };
646
669
  node.focus();
647
- const next = (a.clear ? "" : node.value ?? "") + (a.text ?? "");
648
- setValue(node, next);
670
+ if (a.keyEvents) {
671
+ if (a.clear) setValue(node, "");
672
+ for (const ch of a.text ?? "") {
673
+ const ev = { key: ch, bubbles: true, cancelable: true };
674
+ const go = node.dispatchEvent(new KeyboardEvent("keydown", ev));
675
+ if (go && node.dispatchEvent(new KeyboardEvent("keypress", ev))) setValue(node, (node.value ?? "") + ch);
676
+ node.dispatchEvent(new KeyboardEvent("keyup", ev));
677
+ }
678
+ } else {
679
+ setValue(node, (a.clear ? "" : node.value ?? "") + (a.text ?? ""));
680
+ }
649
681
  node.dispatchEvent(new Event("change", { bubbles: true }));
682
+ if (a.pressEnter) {
683
+ const ev = { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true, cancelable: true };
684
+ const go = node.dispatchEvent(new KeyboardEvent("keydown", ev)) && node.dispatchEvent(new KeyboardEvent("keypress", ev));
685
+ node.dispatchEvent(new KeyboardEvent("keyup", ev));
686
+ if (go && node instanceof HTMLInputElement && node.form) node.form.requestSubmit();
687
+ }
650
688
  return { found: true };
651
689
  }
652
690
  case "focus": {
@@ -1153,6 +1191,7 @@
1153
1191
  const u = cmd.params.url;
1154
1192
  return typeof u === "string" ? u : "";
1155
1193
  }
1194
+ if (cmd.method === "download_file" && typeof cmd.params.url === "string") return cmd.params.url;
1156
1195
  return observedTabUrl(cmd, tab);
1157
1196
  }
1158
1197
  async function observedTabUrl(cmd, tab) {
@@ -1202,23 +1241,41 @@
1202
1241
  done = resolve;
1203
1242
  });
1204
1243
  const isTop = (d) => d.tabId === tabId && d.frameId === 0 && d.timeStamp >= start - 1;
1244
+ let started = false;
1245
+ let startUrl = "";
1246
+ let committed = false;
1247
+ const reached = () => {
1248
+ if (waitUntil === "networkidle") setTimeout(done, NETWORK_QUIET_MS);
1249
+ else done();
1250
+ };
1251
+ const onBefore = (d) => {
1252
+ if (!isTop(d)) return;
1253
+ started = true;
1254
+ startUrl = d.url;
1255
+ committed = false;
1256
+ };
1257
+ const onCommitted = (d) => {
1258
+ if (isTop(d)) committed = true;
1259
+ };
1205
1260
  const onDom = (d) => {
1206
- if (isTop(d) && waitUntil === "domcontentloaded") done();
1261
+ if (isTop(d) && committed && waitUntil === "domcontentloaded") done();
1207
1262
  };
1208
1263
  const onCompleted = (d) => {
1209
- if (!isTop(d)) return;
1210
- if (waitUntil === "networkidle") setTimeout(done, NETWORK_QUIET_MS);
1211
- else done();
1264
+ if (isTop(d) && committed) reached();
1212
1265
  };
1213
1266
  const onSameDoc = (d) => {
1214
1267
  if (isTop(d)) done();
1215
1268
  };
1216
1269
  const onError = (d) => {
1217
- if (isTop(d)) done();
1270
+ if (!isTop(d) || !started) return;
1271
+ if (/ERR_ABORTED/.test(d.error) && d.url !== startUrl) return;
1272
+ done();
1218
1273
  };
1219
1274
  const onUpdated = (id, info) => {
1220
- if (id === tabId && info.status === "complete") done();
1275
+ if (id === tabId && info.status === "complete" && committed) reached();
1221
1276
  };
1277
+ chrome.webNavigation.onBeforeNavigate.addListener(onBefore);
1278
+ chrome.webNavigation.onCommitted.addListener(onCommitted);
1222
1279
  chrome.webNavigation.onDOMContentLoaded.addListener(onDom);
1223
1280
  chrome.webNavigation.onCompleted.addListener(onCompleted);
1224
1281
  chrome.webNavigation.onHistoryStateUpdated.addListener(onSameDoc);
@@ -1226,6 +1283,8 @@
1226
1283
  chrome.webNavigation.onErrorOccurred.addListener(onError);
1227
1284
  chrome.tabs.onUpdated.addListener(onUpdated);
1228
1285
  const cleanup = () => {
1286
+ chrome.webNavigation.onBeforeNavigate.removeListener(onBefore);
1287
+ chrome.webNavigation.onCommitted.removeListener(onCommitted);
1229
1288
  chrome.webNavigation.onDOMContentLoaded.removeListener(onDom);
1230
1289
  chrome.webNavigation.onCompleted.removeListener(onCompleted);
1231
1290
  chrome.webNavigation.onHistoryStateUpdated.removeListener(onSameDoc);
@@ -1246,6 +1305,20 @@
1246
1305
  cancel: cleanup
1247
1306
  };
1248
1307
  }
1308
+ async function historyStep(tabId, step) {
1309
+ const went = await execInTab(
1310
+ tabId,
1311
+ (d) => {
1312
+ const nav = window.navigation;
1313
+ const can = nav ? d < 0 ? nav.canGoBack : nav.canGoForward : d < 0 && history.length > 1;
1314
+ if (can) history.go(d);
1315
+ return can;
1316
+ },
1317
+ [step],
1318
+ "MAIN"
1319
+ ).catch(() => false);
1320
+ return went === true;
1321
+ }
1249
1322
  function resolveSelector(cmd) {
1250
1323
  const s = cmd.params.selector;
1251
1324
  if (typeof s === "string" && s.length > 0) return s;
@@ -1299,17 +1372,41 @@
1299
1372
  dbgSessions.set(tabId, { detachTimer: null });
1300
1373
  }
1301
1374
  var DETACHED_RE = /not attached|no target with given id|detached/i;
1302
- async function withDebugger(tabId, fn) {
1375
+ var DEBUGGER_OP_MS = 2e4;
1376
+ async function withinMs(work, ms, what, onExpire) {
1377
+ let timer;
1378
+ const expired = new Promise((_, reject) => {
1379
+ timer = setTimeout(() => {
1380
+ onExpire?.();
1381
+ reject(new CmdError("TIMEOUT", `${what} within ${ms / 1e3}s; it may be showing a dialog (alert/confirm), busy, or not painting`));
1382
+ }, ms);
1383
+ });
1384
+ try {
1385
+ return await Promise.race([work, expired]);
1386
+ } finally {
1387
+ clearTimeout(timer);
1388
+ }
1389
+ }
1390
+ function withDeadline(tabId, work, ms) {
1391
+ return withinMs(work, ms, "Chrome did not answer for the page", () => {
1392
+ forgetDebugger(tabId);
1393
+ chrome.debugger.detach({ tabId }).catch(() => void 0);
1394
+ });
1395
+ }
1396
+ async function withDebugger(tabId, fn, timeoutMs = DEBUGGER_OP_MS) {
1303
1397
  return locks.run(`dbg:${tabId}`, async () => {
1304
1398
  const target = { tabId };
1305
- await ensureAttached(tabId);
1399
+ const once = async () => {
1400
+ await ensureAttached(tabId);
1401
+ return fn(target);
1402
+ };
1306
1403
  try {
1307
- return await fn(target);
1404
+ return await withDeadline(tabId, once(), timeoutMs);
1308
1405
  } catch (err) {
1406
+ if (err instanceof CmdError && err.code === "TIMEOUT") throw err;
1309
1407
  if (!DETACHED_RE.test(String(err?.message ?? err))) throw err;
1310
1408
  forgetDebugger(tabId);
1311
- await ensureAttached(tabId);
1312
- return await fn(target);
1409
+ return await withDeadline(tabId, once(), timeoutMs);
1313
1410
  } finally {
1314
1411
  scheduleDetach(tabId);
1315
1412
  }
@@ -1334,9 +1431,15 @@
1334
1431
  }
1335
1432
  await chrome.debugger.sendCommand(t, "Input.insertText", { text });
1336
1433
  if (!pressEnter) return;
1337
- for (const type of ["keyDown", "keyUp"]) {
1338
- await chrome.debugger.sendCommand(t, "Input.dispatchKeyEvent", { type, key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
1339
- }
1434
+ await chrome.debugger.sendCommand(t, "Input.dispatchKeyEvent", {
1435
+ type: "keyDown",
1436
+ key: "Enter",
1437
+ code: "Enter",
1438
+ windowsVirtualKeyCode: 13,
1439
+ text: "\r",
1440
+ unmodifiedText: "\r"
1441
+ });
1442
+ await chrome.debugger.sendCommand(t, "Input.dispatchKeyEvent", { type: "keyUp", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
1340
1443
  });
1341
1444
  return true;
1342
1445
  }
@@ -1371,7 +1474,7 @@
1371
1474
  };
1372
1475
  }
1373
1476
  async function screenshotViaDebugger(tabId, fullPage, enc, selector, frameIds) {
1374
- const measured = await measurePage(tabId, selector, frameIds);
1477
+ const measured = await withinMs(measurePage(tabId, selector, frameIds), DEBUGGER_OP_MS, "the page did not answer");
1375
1478
  if (!measured) throw new CmdError("CDP_ERROR", "could not read page dimensions");
1376
1479
  if (selector && measured.missing) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${selector}`);
1377
1480
  const plan = planScreenshot(measured.dims, { fullPage, element: measured.element, scale: enc.scale });
@@ -1403,19 +1506,28 @@
1403
1506
  }
1404
1507
  async function screenshotViaVisibleTab(tabId, fullPage, enc) {
1405
1508
  let t = await chrome.tabs.get(tabId);
1509
+ let restore;
1406
1510
  if (!t.active) {
1511
+ const [previous] = await chrome.tabs.query({ active: true, windowId: t.windowId });
1512
+ restore = previous?.id;
1407
1513
  await chrome.tabs.update(tabId, { active: true });
1408
- await chrome.windows.update(t.windowId, { focused: true }).catch(() => void 0);
1409
1514
  await delay(150);
1410
1515
  t = await chrome.tabs.get(tabId);
1411
1516
  }
1517
+ try {
1518
+ return await captureActiveTab(tabId, t.windowId, fullPage, enc);
1519
+ } finally {
1520
+ if (restore !== void 0) await chrome.tabs.update(restore, { active: true }).catch(() => void 0);
1521
+ }
1522
+ }
1523
+ async function captureActiveTab(tabId, windowId, fullPage, enc) {
1412
1524
  const dims = await execInTab(
1413
1525
  tabId,
1414
1526
  () => ({ w: window.innerWidth, h: window.innerHeight, full: document.documentElement.scrollHeight }),
1415
1527
  []
1416
1528
  );
1417
1529
  const dataUrl = await chrome.tabs.captureVisibleTab(
1418
- t.windowId,
1530
+ windowId,
1419
1531
  enc.format === "jpeg" ? { format: "jpeg", quality: enc.quality } : { format: "png" }
1420
1532
  );
1421
1533
  const viewportH = dims?.h ?? 0;
@@ -1483,6 +1595,43 @@
1483
1595
  this.getPolicy = getPolicy;
1484
1596
  }
1485
1597
  getPolicy;
1598
+ /** The absolute URL a link (or img/source) element points at, for a download
1599
+ * by element. Gated like a download by URL: the router only checked the tab
1600
+ * the link sits in, and the link can lead to any site. */
1601
+ async linkTarget(cmd) {
1602
+ const id = await targetTab(cmd);
1603
+ const sel = requireSelector(cmd);
1604
+ const frameIds = await this.frames(cmd, id);
1605
+ if (!await waitForSelector(id, sel, frameIds)) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${sel}`);
1606
+ const url = await execInTab(
1607
+ id,
1608
+ (s) => {
1609
+ const el = document.querySelector(s);
1610
+ const raw = el?.getAttribute("href") ?? el?.getAttribute("src");
1611
+ try {
1612
+ return raw ? new URL(raw, document.baseURI).href : "";
1613
+ } catch {
1614
+ return "";
1615
+ }
1616
+ },
1617
+ [sel],
1618
+ void 0,
1619
+ frameIds
1620
+ );
1621
+ if (!url) throw new CmdError("DOWNLOAD_FAILED", `the element ${sel} has no href or src to download`);
1622
+ this.assertAllowed(url, "download_file");
1623
+ return url;
1624
+ }
1625
+ allowed(url, method) {
1626
+ const policy = this.getPolicy();
1627
+ return !!policy && evaluatePolicy(url, method, policy).ok;
1628
+ }
1629
+ /** Gate a URL the router could not see (it gates the tab, not arguments). */
1630
+ assertAllowed(url, method) {
1631
+ const policy = this.getPolicy();
1632
+ const verdict = policy ? evaluatePolicy(url, method, policy) : { ok: false, reason: "no policy has arrived yet" };
1633
+ if (!verdict.ok) throw new CmdError("POLICY_DENIED", verdict.reason);
1634
+ }
1486
1635
  /**
1487
1636
  * Resolve `frameId` / `allFrames` params into the frame ids an injection may
1488
1637
  * touch, dropping any whose own URL the policy does not allow for this method.
@@ -1551,6 +1700,7 @@
1551
1700
  }
1552
1701
  case "tab_new": {
1553
1702
  const url = typeof cmd.params.url === "string" ? cmd.params.url : void 0;
1703
+ if (url !== void 0) this.assertAllowed(url, "navigate");
1554
1704
  const active = cmd.params.active !== false;
1555
1705
  const BLANK = /^(about:blank|chrome:\/\/newtab|chrome:\/\/new-tab-page|edge:\/\/newtab)/i;
1556
1706
  const claim = await locks.run("tab_new", async () => {
@@ -1565,14 +1715,14 @@
1565
1715
  claimedTabs.add(blank.id);
1566
1716
  return { id: blank.id, reused: true, needsNav: url !== void 0 };
1567
1717
  }
1568
- const created = await chrome.tabs.create({ url, active: false });
1718
+ const created = await chrome.tabs.create({ url: url === void 0 ? void 0 : "about:blank", active: false });
1569
1719
  if (created.id === void 0) throw new CmdError("TARGET_GONE", "failed to create a tab");
1570
1720
  claimedTabs.add(created.id);
1571
1721
  await rememberOpenedByUs(created.id, present);
1572
- return { id: created.id, reused: false, needsNav: false };
1722
+ return { id: created.id, reused: false, needsNav: url !== void 0 };
1573
1723
  });
1574
1724
  if (claim.needsNav) {
1575
- const nav = armNavigationWait(claim.id, "load");
1725
+ const nav = armNavigationWait(claim.id, "load", 25e3);
1576
1726
  await chrome.tabs.update(claim.id, { url }).catch((err) => {
1577
1727
  nav.cancel();
1578
1728
  throw err;
@@ -1602,25 +1752,19 @@
1602
1752
  });
1603
1753
  await nav.wait();
1604
1754
  const t = await chrome.tabs.get(id);
1605
- return { url: t.url ?? url, title: t.title ?? "" };
1606
- }
1607
- case "back": {
1608
- const id = await targetTab2(cmd);
1609
- const nav = armNavigationWait(id, "load", 15e3);
1610
- const moved = await chrome.tabs.goBack(id).then(() => true, () => false);
1611
- if (moved) await nav.wait();
1612
- else nav.cancel();
1613
- const t = await chrome.tabs.get(id);
1614
- return { url: t.url ?? "", title: t.title ?? "" };
1755
+ const landed = t.url ?? url;
1756
+ return { url: landed, title: this.allowed(landed, "get_text") ? t.title ?? "" : "" };
1615
1757
  }
1758
+ case "back":
1616
1759
  case "forward": {
1617
1760
  const id = await targetTab2(cmd);
1761
+ const step = cmd.method === "back" ? -1 : 1;
1618
1762
  const nav = armNavigationWait(id, "load", 15e3);
1619
- const moved = await chrome.tabs.goForward(id).then(() => true, () => false);
1763
+ const moved = await (step < 0 ? chrome.tabs.goBack(id) : chrome.tabs.goForward(id)).then(() => true, () => false) || await historyStep(id, step);
1620
1764
  if (moved) await nav.wait();
1621
1765
  else nav.cancel();
1622
1766
  const t = await chrome.tabs.get(id);
1623
- return { url: t.url ?? "", title: t.title ?? "" };
1767
+ return { url: t.url ?? "", title: t.title ?? "", moved };
1624
1768
  }
1625
1769
  case "reload": {
1626
1770
  const id = await targetTab2(cmd);
@@ -1699,6 +1843,7 @@
1699
1843
  const t = await chrome.tabs.get(id);
1700
1844
  const url = typeof cmd.params.url === "string" ? cmd.params.url : t.url;
1701
1845
  if (!url) throw new CmdError("BAD_ARGS", "no url to read cookies for");
1846
+ if (url !== t.url) this.assertAllowed(url, "get_cookies");
1702
1847
  const cookies = await chrome.cookies.getAll({ url });
1703
1848
  return {
1704
1849
  cookies: cookies.map((c) => ({
@@ -1719,6 +1864,9 @@
1719
1864
  const key = typeof cmd.params.key === "string" ? cmd.params.key : null;
1720
1865
  const value = typeof cmd.params.value === "string" ? cmd.params.value : null;
1721
1866
  const session = cmd.params.session === true;
1867
+ if (op !== "get" && !this.getPolicy()?.enableMutations) {
1868
+ throw new CmdError("POLICY_DENIED", `storage "${op}" changes the page and is disabled (safe-mode). Pass --enable-mutations to allow it.`);
1869
+ }
1722
1870
  const frameIds = await this.frames(cmd, id);
1723
1871
  const out = await execOp(id, { op: "storage", storageOp: op, key, value, session }, frameIds);
1724
1872
  const { found: _found, frameId: _frameId, ...rest } = out;
@@ -1812,7 +1960,11 @@
1812
1960
  }
1813
1961
  return { ok: true };
1814
1962
  }
1815
- const out = await execOp(id, withWait({ op: "type", selector: sel, text, clear }), frameIds);
1963
+ const out = await execOp(
1964
+ id,
1965
+ withWait({ op: "type", selector: sel, text, clear, keyEvents: cmd.params.keyEvents === true, pressEnter: cmd.params.pressEnter === true }),
1966
+ frameIds
1967
+ );
1816
1968
  if (!out.found) throw notFound(sel, out);
1817
1969
  return { ok: true, frameId: out.frameId };
1818
1970
  }
@@ -1834,7 +1986,7 @@
1834
1986
  const out = await execOp(
1835
1987
  id,
1836
1988
  withWait(
1837
- typeof op.value === "boolean" ? { op: "click", selector: op.selector } : { op: "type", selector: op.selector, text: op.value, clear: true }
1989
+ typeof op.value === "boolean" ? { op: "check", selector: op.selector, checked: op.value } : { op: "type", selector: op.selector, text: op.value, clear: true }
1838
1990
  ),
1839
1991
  await this.frames(cmd, id)
1840
1992
  );
@@ -1849,15 +2001,25 @@
1849
2001
  case "press": {
1850
2002
  const id = await targetTab2(cmd);
1851
2003
  const key = String(cmd.params.key ?? "");
2004
+ const modifiers = Array.isArray(cmd.params.modifiers) ? cmd.params.modifiers.map((m) => String(m).toLowerCase()) : [];
1852
2005
  await execInTab(
1853
2006
  id,
1854
- (k) => {
2007
+ (k, mods) => {
2008
+ const m = mods;
2009
+ const has = (...names) => names.some((n) => m.includes(n));
2010
+ const init = {
2011
+ key: k,
2012
+ bubbles: true,
2013
+ cancelable: true,
2014
+ ctrlKey: has("control", "ctrl"),
2015
+ shiftKey: has("shift"),
2016
+ altKey: has("alt", "option"),
2017
+ metaKey: has("meta", "cmd", "command")
2018
+ };
1855
2019
  const el = document.activeElement ?? document.body;
1856
- for (const type of ["keydown", "keypress", "keyup"]) {
1857
- el.dispatchEvent(new KeyboardEvent(type, { key: k, bubbles: true }));
1858
- }
2020
+ for (const type of ["keydown", "keypress", "keyup"]) el.dispatchEvent(new KeyboardEvent(type, init));
1859
2021
  },
1860
- [key]
2022
+ [key, modifiers]
1861
2023
  );
1862
2024
  return { ok: true };
1863
2025
  }
@@ -1903,7 +2065,7 @@
1903
2065
  try {
1904
2066
  return await screenshotViaDebugger(id, fullPage, enc, selector, frameIds);
1905
2067
  } catch (err) {
1906
- if (err instanceof CmdError && err.code === "SELECTOR_NOT_FOUND") throw err;
2068
+ if (err instanceof CmdError && (err.code === "SELECTOR_NOT_FOUND" || err.code === "TIMEOUT")) throw err;
1907
2069
  return await screenshotViaVisibleTab(id, fullPage, enc);
1908
2070
  }
1909
2071
  }
@@ -1913,15 +2075,16 @@
1913
2075
  const expr = String(cmd.params.expression ?? "");
1914
2076
  const result = await execInTab(
1915
2077
  id,
1916
- (e) => {
2078
+ async (e, wait) => {
1917
2079
  try {
1918
- const v = (0, eval)(e);
2080
+ let v = (0, eval)(e);
2081
+ if (wait && v && typeof v.then === "function") v = await v;
1919
2082
  return { ok: true, value: v, type: typeof v };
1920
2083
  } catch (err) {
1921
2084
  return { ok: false, error: String(err) };
1922
2085
  }
1923
2086
  },
1924
- [expr],
2087
+ [expr, cmd.params.awaitPromise === true],
1925
2088
  "MAIN",
1926
2089
  await this.frames(cmd, id)
1927
2090
  );
@@ -1953,8 +2116,7 @@
1953
2116
  // into the active task's downloads/). We wait for completion and report
1954
2117
  // the absolute on-disk path so the server can relocate it. --
1955
2118
  case "download_file": {
1956
- const url = typeof cmd.params.url === "string" ? cmd.params.url : void 0;
1957
- if (!url) throw new CmdError("DOWNLOAD_FAILED", "the extension download path requires a url");
2119
+ const url = typeof cmd.params.url === "string" ? cmd.params.url : await this.linkTarget(cmd);
1958
2120
  const name = sanitizeDownloadName(
1959
2121
  typeof cmd.params.suggestedName === "string" ? cmd.params.suggestedName : void 0
1960
2122
  );
@@ -2090,7 +2252,7 @@
2090
2252
  const data = await withDebugger(id, async (t2) => {
2091
2253
  const res = await chrome.debugger.sendCommand(t2, "Page.printToPDF", params);
2092
2254
  return res.data ?? "";
2093
- });
2255
+ }, 25e3);
2094
2256
  if (!data) throw new CmdError("CDP_ERROR", "Chrome returned an empty PDF");
2095
2257
  const t = await chrome.tabs.get(id);
2096
2258
  return { dataBase64: data, mimeType: "application/pdf", url: t.url ?? "", title: t.title ?? "" };
@@ -2305,18 +2467,30 @@
2305
2467
  const tab = await chrome.tabs.get(tabId).catch(() => null);
2306
2468
  return !!tab?.url && evaluatePolicy(tab.url, "get_text", policy).ok;
2307
2469
  }
2308
- async function paint(tabId) {
2470
+ var PAGE_WAIT_MS = 2e3;
2471
+ async function bounded(work, fallback) {
2472
+ let timer;
2473
+ const late = new Promise((resolve) => {
2474
+ timer = setTimeout(() => resolve(fallback), PAGE_WAIT_MS);
2475
+ });
2309
2476
  try {
2310
- const [res] = await chrome.scripting.executeScript({ target: { tabId }, func: readPageTheme });
2311
- const css = borderCss(pickBorderColor(res?.result ?? { themeColor: "", background: "", prefersDark: false }));
2312
- await chrome.scripting.insertCSS({ target: { tabId }, css, origin: "USER" });
2313
- return css;
2314
- } catch {
2315
- return "";
2477
+ return await Promise.race([work.catch(() => fallback), late]);
2478
+ } finally {
2479
+ clearTimeout(timer);
2316
2480
  }
2317
2481
  }
2482
+ async function paint(tabId) {
2483
+ const theme = await bounded(
2484
+ chrome.scripting.executeScript({ target: { tabId }, func: readPageTheme }).then(([res]) => res?.result),
2485
+ null
2486
+ );
2487
+ if (theme === null) return "";
2488
+ const css = borderCss(pickBorderColor(theme ?? { themeColor: "", background: "", prefersDark: false }));
2489
+ const ok = await bounded(chrome.scripting.insertCSS({ target: { tabId }, css, origin: "USER" }).then(() => true), false);
2490
+ return ok ? css : "";
2491
+ }
2318
2492
  async function unpaint(tabId, css) {
2319
- if (css) await chrome.scripting.removeCSS({ target: { tabId }, css, origin: "USER" }).catch(() => void 0);
2493
+ if (css) await bounded(chrome.scripting.removeCSS({ target: { tabId }, css, origin: "USER" }), void 0);
2320
2494
  }
2321
2495
  var TabBorder = class {
2322
2496
  constructor(getPolicy) {
@@ -2393,7 +2567,7 @@
2393
2567
  if (left > 0) return void this.capturing.set(tabId, left);
2394
2568
  this.capturing.delete(tabId);
2395
2569
  if ((await loadMarked())[tabId] !== css) return;
2396
- await chrome.scripting.insertCSS({ target: { tabId }, css, origin: "USER" }).catch(() => void 0);
2570
+ await bounded(chrome.scripting.insertCSS({ target: { tabId }, css, origin: "USER" }), void 0);
2397
2571
  });
2398
2572
  }
2399
2573
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 3,
3
- "name": "MCP Browser Extension",
4
- "version": "0.9.26",
5
- "description": "Lets your AI agent (Claude, Cursor, Codex) use the Chrome you are signed into, through a local MCP server. Deny-all by default.",
3
+ "name": "MCP Browser Extension – AI agent in your logged-in Chrome",
4
+ "version": "0.9.30",
5
+ "description": "Lets your AI agent use the Chrome you are signed into, through a local MCP server. Deny-all by default.",
6
6
  "homepage_url": "https://mcp-browser-extension.com",
7
7
  "minimum_chrome_version": "116",
8
8
  "icons": {
@@ -83,6 +83,12 @@
83
83
  "error",
84
84
  (e) => {
85
85
  try {
86
+ const el = e.target;
87
+ if (el && el !== window && typeof el.tagName === "string") {
88
+ const src = el.currentSrc || el.src || el.href || "";
89
+ record("error", `Failed to load ${el.tagName.toLowerCase()}${src ? `: ${src}` : ""}`);
90
+ return;
91
+ }
86
92
  const err = e.error;
87
93
  const where = e.filename ? ` (${e.filename}:${e.lineno}:${e.colno})` : "";
88
94
  record("exception", `${e.message}${where}`, { stack: err?.stack });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-browser-extension",
3
3
  "mcpName": "io.github.Mehmoodqureshi/mcp-browser-extension",
4
- "version": "0.9.28",
4
+ "version": "0.9.30",
5
5
  "description": "Drive your real Chrome browser over MCP — real logins, real cookies. A stdio MCP server (CLI) plus an MV3 extension, driving Chrome via chrome.scripting/chrome.tabs. Multi-tab batch automation, accessibility snapshots, deny-all security by default.",
6
6
  "author": "Mehmood Ur Rehman Qureshi",
7
7
  "license": "MIT",
@@ -57,6 +57,7 @@
57
57
  "typecheck:ext": "tsc -p tsconfig.ext.json",
58
58
  "clean": "rimraf dist",
59
59
  "test": "npm run clean && npm run build && npm run build:ext && node --test dist/test/*.test.js",
60
+ "test:e2e": "npm run build && npm run build:ext && RUN_E2E=1 node --test --test-concurrency=1 dist/test/e2e/*.test.js",
60
61
  "test:hitl": "npm run build && node dist/hitl/index.js",
61
62
  "prepack": "npm run clean && npm run build && npm run build:ext"
62
63
  },