@mehmoodqureshi/chrome-mcp 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -11
- package/dist/shared/mutex.d.ts +18 -0
- package/dist/shared/mutex.js +42 -0
- package/dist/shared/policy.d.ts +34 -0
- package/dist/shared/policy.js +151 -0
- package/dist/shared/protocol.d.ts +15 -0
- package/dist/shared/screenshot.d.ts +58 -0
- package/dist/shared/screenshot.js +54 -0
- package/dist/src/bridge/server.d.ts +4 -1
- package/dist/src/bridge/server.js +2 -0
- package/dist/src/cli.js +3 -0
- package/dist/src/executor/cdp-executor.d.ts +3 -1
- package/dist/src/executor/cdp-executor.js +3 -1
- package/dist/src/executor/extension-executor.d.ts +3 -1
- package/dist/src/executor/extension-executor.js +2 -2
- package/dist/src/executor/stub-executor.d.ts +3 -1
- package/dist/src/executor/stub-executor.js +1 -1
- package/dist/src/executor/types.d.ts +4 -1
- package/dist/src/mcp/batch.d.ts +26 -0
- package/dist/src/mcp/batch.js +130 -0
- package/dist/src/mcp/tools.js +29 -3
- package/dist/src/security/policy.d.ts +16 -34
- package/dist/src/security/policy.js +20 -124
- package/docs/BLUEPRINT.md +9 -4
- package/extension-dist/background.js +281 -42
- package/extension-dist/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
}
|
|
76
76
|
switch (frame.type) {
|
|
77
77
|
case "welcome":
|
|
78
|
+
this.deps.onPolicy(frame.policy);
|
|
78
79
|
this.setState("connected");
|
|
79
80
|
this.deps.log("paired with server");
|
|
80
81
|
break;
|
|
@@ -116,6 +117,92 @@
|
|
|
116
117
|
}
|
|
117
118
|
};
|
|
118
119
|
|
|
120
|
+
// shared/policy.ts
|
|
121
|
+
var READ_CONTENT = /* @__PURE__ */ new Set([
|
|
122
|
+
"get_text",
|
|
123
|
+
"get_html",
|
|
124
|
+
"screenshot",
|
|
125
|
+
"wait_for"
|
|
126
|
+
]);
|
|
127
|
+
var MUTATE_CONTENT = /* @__PURE__ */ new Set([
|
|
128
|
+
"click",
|
|
129
|
+
"type",
|
|
130
|
+
"press",
|
|
131
|
+
"hover",
|
|
132
|
+
"scroll"
|
|
133
|
+
]);
|
|
134
|
+
var NAVIGATION = /* @__PURE__ */ new Set([
|
|
135
|
+
"navigate",
|
|
136
|
+
"back",
|
|
137
|
+
"forward",
|
|
138
|
+
"reload"
|
|
139
|
+
]);
|
|
140
|
+
var TAB_MUTATE = /* @__PURE__ */ new Set([
|
|
141
|
+
"tab_select",
|
|
142
|
+
"tab_new",
|
|
143
|
+
"tab_close"
|
|
144
|
+
]);
|
|
145
|
+
function isMutatingMethod(method) {
|
|
146
|
+
return MUTATE_CONTENT.has(method) || NAVIGATION.has(method) || TAB_MUTATE.has(method);
|
|
147
|
+
}
|
|
148
|
+
function isUrlGated(method) {
|
|
149
|
+
return READ_CONTENT.has(method) || MUTATE_CONTENT.has(method) || NAVIGATION.has(method) || method === "eval" || method === "upload_file";
|
|
150
|
+
}
|
|
151
|
+
function hostOf(url) {
|
|
152
|
+
try {
|
|
153
|
+
return new URL(url).hostname.toLowerCase();
|
|
154
|
+
} catch {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function isAboutBlank(url) {
|
|
159
|
+
return url === "about:blank" || url === "" || url.startsWith("about:");
|
|
160
|
+
}
|
|
161
|
+
function globMatches(host, pattern) {
|
|
162
|
+
const p = pattern.trim().toLowerCase();
|
|
163
|
+
if (p === "*" || p === "*://*/*") return true;
|
|
164
|
+
if (p.startsWith("*.")) {
|
|
165
|
+
const base = p.slice(2);
|
|
166
|
+
return host === base || host.endsWith("." + base);
|
|
167
|
+
}
|
|
168
|
+
return host === p;
|
|
169
|
+
}
|
|
170
|
+
function isDomainAllowed(url, policy) {
|
|
171
|
+
const host = hostOf(url);
|
|
172
|
+
if (!host) return false;
|
|
173
|
+
return policy.allowDomains.some((pat) => globMatches(host, pat));
|
|
174
|
+
}
|
|
175
|
+
function evaluatePolicy(url, method, policy) {
|
|
176
|
+
if (method === "eval" && !policy.allowEval) {
|
|
177
|
+
return { ok: false, reason: "eval is disabled (safe-mode). Pass --unsafe-enable-eval to allow it." };
|
|
178
|
+
}
|
|
179
|
+
if (method === "download_file" && !policy.allowDownloads) {
|
|
180
|
+
return { ok: false, reason: "downloads are disabled. Pass --enable-downloads or set allowDownloads." };
|
|
181
|
+
}
|
|
182
|
+
if (method === "upload_file" && !policy.allowUploads) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
reason: "uploads are disabled (sending local files to a page is an exfiltration risk). Pass --enable-uploads or set allowUploads."
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (isMutatingMethod(method) && !policy.enableMutations) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
reason: `mutating tool "${method}" is disabled (safe-mode). Pass --enable-mutations to allow it.`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (!isUrlGated(method)) return { ok: true };
|
|
195
|
+
if (isAboutBlank(url) && NAVIGATION.has(method)) return { ok: true };
|
|
196
|
+
if (!isDomainAllowed(url, policy)) {
|
|
197
|
+
const host = hostOf(url) || url;
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
reason: `"${method}" denied: ${host} is not in the domain allowlist. Add it to allowDomains, or pass --unsafe-all-domains.`
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return { ok: true };
|
|
204
|
+
}
|
|
205
|
+
|
|
119
206
|
// shared/download.ts
|
|
120
207
|
var MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
|
|
121
208
|
var DANGEROUS_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -272,6 +359,62 @@
|
|
|
272
359
|
return { url: location.href, title: document.title, nodes, truncated: els.length > nodes.length };
|
|
273
360
|
}
|
|
274
361
|
|
|
362
|
+
// shared/screenshot.ts
|
|
363
|
+
var MAX_CAPTURE_PX = 16384;
|
|
364
|
+
function planScreenshot(dims, opts = {}) {
|
|
365
|
+
if (opts.element) {
|
|
366
|
+
const realH = Math.max(1, Math.round(opts.element.h));
|
|
367
|
+
const clipH = Math.min(opts.element.h, MAX_CAPTURE_PX);
|
|
368
|
+
return {
|
|
369
|
+
clip: { x: opts.element.x, y: opts.element.y, width: opts.element.w, height: clipH, scale: 1 },
|
|
370
|
+
captureBeyondViewport: true,
|
|
371
|
+
width: Math.max(1, Math.round(opts.element.w)),
|
|
372
|
+
height: Math.min(realH, MAX_CAPTURE_PX),
|
|
373
|
+
truncated: realH > MAX_CAPTURE_PX,
|
|
374
|
+
fullHeight: realH
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (opts.fullPage) {
|
|
378
|
+
const clipH = Math.min(dims.fullH, MAX_CAPTURE_PX);
|
|
379
|
+
return {
|
|
380
|
+
clip: { x: 0, y: 0, width: dims.fullW, height: clipH, scale: 1 },
|
|
381
|
+
captureBeyondViewport: true,
|
|
382
|
+
width: dims.fullW,
|
|
383
|
+
height: clipH,
|
|
384
|
+
truncated: dims.fullH > clipH,
|
|
385
|
+
fullHeight: dims.fullH
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
captureBeyondViewport: false,
|
|
390
|
+
width: dims.w,
|
|
391
|
+
height: dims.h,
|
|
392
|
+
truncated: false
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// shared/mutex.ts
|
|
397
|
+
function noop() {
|
|
398
|
+
}
|
|
399
|
+
var KeyedMutex = class {
|
|
400
|
+
tails = /* @__PURE__ */ new Map();
|
|
401
|
+
/** Run `fn` after all earlier holders of `key` settle. Resolves/rejects with fn's outcome. */
|
|
402
|
+
run(key, fn) {
|
|
403
|
+
const prev = this.tails.get(key) ?? Promise.resolve();
|
|
404
|
+
const result = prev.then(fn, fn);
|
|
405
|
+
const tail = result.then(noop, noop);
|
|
406
|
+
this.tails.set(key, tail);
|
|
407
|
+
void tail.then(() => {
|
|
408
|
+
if (this.tails.get(key) === tail) this.tails.delete(key);
|
|
409
|
+
});
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
/** Number of keys with an outstanding or queued holder (for tests/inspection). */
|
|
413
|
+
get size() {
|
|
414
|
+
return this.tails.size;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
275
418
|
// extension/src/sw/executor.ts
|
|
276
419
|
var CmdError = class extends Error {
|
|
277
420
|
constructor(code, message) {
|
|
@@ -282,6 +425,8 @@
|
|
|
282
425
|
var SESSION = crypto.randomUUID();
|
|
283
426
|
var CONTENT_SCHEME = /^(https?|file):/i;
|
|
284
427
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
428
|
+
var locks = new KeyedMutex();
|
|
429
|
+
var claimedTabs = /* @__PURE__ */ new Set();
|
|
285
430
|
function mint(tabId) {
|
|
286
431
|
return `ext:${SESSION}:${tabId}`;
|
|
287
432
|
}
|
|
@@ -303,6 +448,19 @@
|
|
|
303
448
|
async function targetTab(cmd) {
|
|
304
449
|
return cmd.tabId ? parseTabId(cmd.tabId) : currentTabId();
|
|
305
450
|
}
|
|
451
|
+
async function urlForCommand(cmd) {
|
|
452
|
+
if (cmd.method === "navigate") {
|
|
453
|
+
const u = cmd.params.url;
|
|
454
|
+
return typeof u === "string" ? u : "";
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const tabId = await targetTab(cmd);
|
|
458
|
+
const t = await chrome.tabs.get(tabId);
|
|
459
|
+
return t.url ?? "";
|
|
460
|
+
} catch {
|
|
461
|
+
return "";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
306
464
|
async function execInTab(tabId, func, args = [], world) {
|
|
307
465
|
const [res] = await chrome.scripting.executeScript({
|
|
308
466
|
target: { tabId },
|
|
@@ -341,13 +499,15 @@
|
|
|
341
499
|
}
|
|
342
500
|
}
|
|
343
501
|
async function withDebugger(tabId, fn) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
502
|
+
return locks.run(`dbg:${tabId}`, async () => {
|
|
503
|
+
const target = { tabId };
|
|
504
|
+
await chrome.debugger.attach(target, "1.3");
|
|
505
|
+
try {
|
|
506
|
+
return await fn(target);
|
|
507
|
+
} finally {
|
|
508
|
+
await chrome.debugger.detach(target).catch(() => void 0);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
351
511
|
}
|
|
352
512
|
async function trustedType(tabId, selector, text, clear) {
|
|
353
513
|
const focused = await execInTab(
|
|
@@ -389,6 +549,72 @@
|
|
|
389
549
|
});
|
|
390
550
|
return true;
|
|
391
551
|
}
|
|
552
|
+
async function measurePage(tabId, selector) {
|
|
553
|
+
return execInTab(
|
|
554
|
+
tabId,
|
|
555
|
+
(sel) => {
|
|
556
|
+
const d = document.documentElement;
|
|
557
|
+
const dims = {
|
|
558
|
+
w: window.innerWidth,
|
|
559
|
+
h: window.innerHeight,
|
|
560
|
+
fullW: Math.max(d.scrollWidth, d.clientWidth),
|
|
561
|
+
fullH: Math.max(d.scrollHeight, d.clientHeight)
|
|
562
|
+
};
|
|
563
|
+
if (!sel) return { dims, element: null, missing: false };
|
|
564
|
+
const el = document.querySelector(sel);
|
|
565
|
+
if (!el) return { dims, element: null, missing: true };
|
|
566
|
+
el.scrollIntoView({ block: "center", inline: "center" });
|
|
567
|
+
const r = el.getBoundingClientRect();
|
|
568
|
+
return { dims, element: { x: r.left + window.scrollX, y: r.top + window.scrollY, w: r.width, h: r.height }, missing: false };
|
|
569
|
+
},
|
|
570
|
+
[selector ?? null]
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
async function screenshotViaDebugger(tabId, fullPage, selector) {
|
|
574
|
+
const measured = await measurePage(tabId, selector);
|
|
575
|
+
if (!measured) throw new CmdError("CDP_ERROR", "could not read page dimensions");
|
|
576
|
+
if (selector && measured.missing) throw new CmdError("SELECTOR_NOT_FOUND", `no element for selector: ${selector}`);
|
|
577
|
+
const plan = planScreenshot(measured.dims, { fullPage, element: measured.element });
|
|
578
|
+
const params = { format: "png", captureBeyondViewport: plan.captureBeyondViewport };
|
|
579
|
+
if (plan.clip) params.clip = plan.clip;
|
|
580
|
+
const data = await withDebugger(tabId, async (target) => {
|
|
581
|
+
const res = await chrome.debugger.sendCommand(target, "Page.captureScreenshot", params);
|
|
582
|
+
return res.data ?? "";
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
dataBase64: data,
|
|
586
|
+
mimeType: "image/png",
|
|
587
|
+
width: plan.width,
|
|
588
|
+
height: plan.height,
|
|
589
|
+
truncated: plan.truncated,
|
|
590
|
+
fullHeight: plan.fullHeight
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
async function screenshotViaVisibleTab(tabId, fullPage) {
|
|
594
|
+
let t = await chrome.tabs.get(tabId);
|
|
595
|
+
if (!t.active) {
|
|
596
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
597
|
+
await chrome.windows.update(t.windowId, { focused: true }).catch(() => void 0);
|
|
598
|
+
await delay(150);
|
|
599
|
+
t = await chrome.tabs.get(tabId);
|
|
600
|
+
}
|
|
601
|
+
const dims = await execInTab(
|
|
602
|
+
tabId,
|
|
603
|
+
() => ({ w: window.innerWidth, h: window.innerHeight, full: document.documentElement.scrollHeight }),
|
|
604
|
+
[]
|
|
605
|
+
);
|
|
606
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(t.windowId, { format: "png" });
|
|
607
|
+
const viewportH = dims?.h ?? 0;
|
|
608
|
+
const fullH = dims?.full ?? viewportH;
|
|
609
|
+
return {
|
|
610
|
+
dataBase64: dataUrl.split(",")[1] ?? "",
|
|
611
|
+
mimeType: "image/png",
|
|
612
|
+
width: dims?.w ?? 0,
|
|
613
|
+
height: viewportH,
|
|
614
|
+
truncated: fullPage && fullH > viewportH,
|
|
615
|
+
fullHeight: fullPage ? fullH : void 0
|
|
616
|
+
};
|
|
617
|
+
}
|
|
392
618
|
async function tabInfo(tab, index = 0) {
|
|
393
619
|
return {
|
|
394
620
|
tabId: mint(tab.id ?? -1),
|
|
@@ -442,19 +668,34 @@
|
|
|
442
668
|
}
|
|
443
669
|
case "tab_new": {
|
|
444
670
|
const url = typeof cmd.params.url === "string" ? cmd.params.url : void 0;
|
|
671
|
+
const active = cmd.params.active !== false;
|
|
445
672
|
const BLANK = /^(about:blank|chrome:\/\/newtab|chrome:\/\/new-tab-page|edge:\/\/newtab)/i;
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
673
|
+
const claim = await locks.run("tab_new", async () => {
|
|
674
|
+
const tabs = await chrome.tabs.query({});
|
|
675
|
+
const present = new Set(tabs.map((t) => t.id).filter((id) => id !== void 0));
|
|
676
|
+
for (const id of claimedTabs) if (!present.has(id)) claimedTabs.delete(id);
|
|
677
|
+
const blank = tabs.find(
|
|
678
|
+
(t) => t.id !== void 0 && !claimedTabs.has(t.id) && (BLANK.test(t.url ?? "") || (t.url ?? "") === "" || t.pendingUrl === "about:blank")
|
|
679
|
+
);
|
|
680
|
+
if (blank?.id !== void 0) {
|
|
681
|
+
claimedTabs.add(blank.id);
|
|
682
|
+
return { id: blank.id, reused: true, needsNav: url !== void 0 };
|
|
453
683
|
}
|
|
454
|
-
|
|
684
|
+
const created = await chrome.tabs.create({ url, active: false });
|
|
685
|
+
if (created.id === void 0) throw new CmdError("TARGET_GONE", "failed to create a tab");
|
|
686
|
+
claimedTabs.add(created.id);
|
|
687
|
+
return { id: created.id, reused: false, needsNav: false };
|
|
688
|
+
});
|
|
689
|
+
if (claim.needsNav) {
|
|
690
|
+
await chrome.tabs.update(claim.id, { url });
|
|
691
|
+
await waitComplete(claim.id);
|
|
455
692
|
}
|
|
456
|
-
|
|
457
|
-
|
|
693
|
+
if (active) {
|
|
694
|
+
const t = await chrome.tabs.get(claim.id);
|
|
695
|
+
await chrome.tabs.update(claim.id, { active: true }).catch(() => void 0);
|
|
696
|
+
await chrome.windows.update(t.windowId, { focused: true }).catch(() => void 0);
|
|
697
|
+
}
|
|
698
|
+
return { ...await tabInfo(await chrome.tabs.get(claim.id)), reused: claim.reused };
|
|
458
699
|
}
|
|
459
700
|
case "tab_close": {
|
|
460
701
|
const id = parseTabId(String(cmd.tabId));
|
|
@@ -709,34 +950,21 @@
|
|
|
709
950
|
);
|
|
710
951
|
return { ok: true };
|
|
711
952
|
}
|
|
712
|
-
// -- screenshot
|
|
953
|
+
// -- screenshot --
|
|
954
|
+
// Primary path: chrome.debugger Page.captureScreenshot, which captures a
|
|
955
|
+
// SPECIFIC tab WITHOUT activating it (no focus-stealing → safe under
|
|
956
|
+
// concurrent batches) and supports true full-page + element capture.
|
|
957
|
+
// Falls back to captureVisibleTab only if the debugger can't attach.
|
|
713
958
|
case "screenshot": {
|
|
714
959
|
const id = await targetTab(cmd);
|
|
715
|
-
let t = await chrome.tabs.get(id);
|
|
716
|
-
if (!t.active) {
|
|
717
|
-
await chrome.tabs.update(id, { active: true });
|
|
718
|
-
await chrome.windows.update(t.windowId, { focused: true }).catch(() => void 0);
|
|
719
|
-
await delay(150);
|
|
720
|
-
t = await chrome.tabs.get(id);
|
|
721
|
-
}
|
|
722
|
-
const dims = await execInTab(
|
|
723
|
-
id,
|
|
724
|
-
() => ({ w: window.innerWidth, h: window.innerHeight, full: document.documentElement.scrollHeight }),
|
|
725
|
-
[]
|
|
726
|
-
);
|
|
727
|
-
const dataUrl = await chrome.tabs.captureVisibleTab(t.windowId, { format: "png" });
|
|
728
960
|
const fullPage = cmd.params.fullPage === true;
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
// The scripting backend can only capture the viewport; flag when a fullPage was asked but clipped.
|
|
737
|
-
truncated: fullPage && fullH > viewportH,
|
|
738
|
-
fullHeight: fullPage ? fullH : void 0
|
|
739
|
-
};
|
|
961
|
+
const selector = selectorOf(cmd);
|
|
962
|
+
try {
|
|
963
|
+
return await screenshotViaDebugger(id, fullPage, selector);
|
|
964
|
+
} catch (err) {
|
|
965
|
+
if (err instanceof CmdError && err.code === "SELECTOR_NOT_FOUND") throw err;
|
|
966
|
+
return await screenshotViaVisibleTab(id, fullPage);
|
|
967
|
+
}
|
|
740
968
|
}
|
|
741
969
|
// -- eval (MAIN world; may be blocked by strict page CSP) --
|
|
742
970
|
case "eval": {
|
|
@@ -829,6 +1057,12 @@
|
|
|
829
1057
|
}
|
|
830
1058
|
async dispatch(cmd) {
|
|
831
1059
|
try {
|
|
1060
|
+
const policy = this.deps.getPolicy();
|
|
1061
|
+
if (policy) {
|
|
1062
|
+
const url = isUrlGated(cmd.method) ? await urlForCommand(cmd) : "";
|
|
1063
|
+
const verdict = evaluatePolicy(url, cmd.method, policy);
|
|
1064
|
+
if (!verdict.ok) throw new CmdError("POLICY_DENIED", verdict.reason);
|
|
1065
|
+
}
|
|
832
1066
|
const data = await this.deps.exec.run(cmd);
|
|
833
1067
|
const frame = { type: "result", v: PROTOCOL_VERSION, id: cmd.id, ok: true, data };
|
|
834
1068
|
this.deps.send(frame);
|
|
@@ -850,15 +1084,20 @@
|
|
|
850
1084
|
|
|
851
1085
|
// extension/src/sw/background.ts
|
|
852
1086
|
var KEEPALIVE_ALARM = "chrome-mcp-keepalive";
|
|
1087
|
+
var currentPolicy = null;
|
|
853
1088
|
var executor = new ChromeExecutor();
|
|
854
1089
|
var ws = new WsClient({
|
|
855
1090
|
onCommand: (cmd) => void router.dispatch(cmd),
|
|
856
1091
|
onState: (state) => void persistState(state),
|
|
1092
|
+
onPolicy: (policy) => {
|
|
1093
|
+
currentPolicy = policy;
|
|
1094
|
+
},
|
|
857
1095
|
log: (m) => console.debug("[chrome-mcp]", m)
|
|
858
1096
|
});
|
|
859
1097
|
var router = new CommandRouter({
|
|
860
1098
|
exec: executor,
|
|
861
1099
|
send: (frame) => ws.send(frame),
|
|
1100
|
+
getPolicy: () => currentPolicy,
|
|
862
1101
|
log: (m) => console.debug("[chrome-mcp]", m)
|
|
863
1102
|
});
|
|
864
1103
|
async function getConfig() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Chrome MCP Bridge",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Lets a local chrome-mcp server drive this browser. Pair it with the server's handshake token.",
|
|
6
6
|
"minimum_chrome_version": "116",
|
|
7
7
|
"background": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mehmoodqureshi/chrome-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Drive a real Chrome browser over MCP. A stdio MCP server (CLI) plus an MV3 extension, behind one pluggable Executor (extension via chrome.scripting, or a Playwright CDP fallback).",
|
|
5
5
|
"author": "Mehmood Ur Rehman Qureshi",
|
|
6
6
|
"license": "MIT",
|