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.
- package/README.md +20 -2
- package/dist/shared/page-fns.d.ts +7 -1
- package/dist/shared/page-fns.js +37 -2
- package/dist/shared/policy.js +10 -1
- package/dist/shared/snapshot.js +15 -4
- package/dist/src/bridge/host-claim.d.ts +17 -0
- package/dist/src/bridge/host-claim.js +42 -0
- package/dist/src/cli.js +17 -5
- package/dist/src/executor/cdp-executor.d.ts +3 -0
- package/dist/src/executor/cdp-executor.js +14 -0
- package/dist/src/executor/extension-executor.js +14 -4
- package/dist/src/executor/types.d.ts +4 -0
- package/dist/src/mcp/helpers.js +11 -2
- package/dist/src/mcp/page-watch.d.ts +12 -0
- package/dist/src/mcp/page-watch.js +79 -0
- package/dist/src/mcp/tools.js +73 -9
- package/dist/src/security/policy.d.ts +7 -0
- package/dist/src/security/policy.js +11 -0
- package/extension-dist/background.js +238 -64
- package/extension-dist/manifest.json +3 -3
- package/extension-dist/page-hook.js +6 -0
- package/package.json +2 -1
|
@@ -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 >=
|
|
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 >=
|
|
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 >=
|
|
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
|
-
|
|
648
|
-
|
|
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 (
|
|
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))
|
|
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")
|
|
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
|
-
|
|
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
|
-
|
|
1399
|
+
const once = async () => {
|
|
1400
|
+
await ensureAttached(tabId);
|
|
1401
|
+
return fn(target);
|
|
1402
|
+
};
|
|
1306
1403
|
try {
|
|
1307
|
-
return await
|
|
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
|
|
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
|
-
|
|
1338
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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: "
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
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" })
|
|
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" })
|
|
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.
|
|
5
|
-
"description": "Lets your AI agent
|
|
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.
|
|
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
|
},
|