mcp-browser-extension 0.9.30 → 0.9.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +77 -14
  2. package/SETUP.md +40 -7
  3. package/dist/shared/activity.d.ts +25 -0
  4. package/dist/shared/activity.js +72 -0
  5. package/dist/shared/browser-permissions.d.ts +20 -0
  6. package/dist/shared/browser-permissions.js +40 -0
  7. package/dist/shared/client-auth.d.ts +28 -0
  8. package/dist/shared/client-auth.js +38 -0
  9. package/dist/shared/observers.d.ts +1 -1
  10. package/dist/shared/observers.js +1 -3
  11. package/dist/shared/policy.d.ts +5 -7
  12. package/dist/shared/policy.js +11 -26
  13. package/dist/shared/protocol.d.ts +4 -0
  14. package/dist/shared/protocol.js +3 -3
  15. package/dist/shared/screenshot.d.ts +4 -2
  16. package/dist/shared/screenshot.js +13 -8
  17. package/dist/src/access-browser.d.ts +15 -0
  18. package/dist/src/access-browser.js +202 -0
  19. package/dist/src/access.d.ts +2 -0
  20. package/dist/src/access.js +92 -18
  21. package/dist/src/bridge/client-identity.d.ts +10 -0
  22. package/dist/src/bridge/client-identity.js +28 -0
  23. package/dist/src/bridge/connection.d.ts +5 -0
  24. package/dist/src/bridge/connection.js +4 -12
  25. package/dist/src/bridge/peer.d.ts +4 -19
  26. package/dist/src/bridge/peer.js +17 -8
  27. package/dist/src/bridge/server.d.ts +7 -16
  28. package/dist/src/bridge/server.js +64 -17
  29. package/dist/src/bridge/site-grants.d.ts +1 -2
  30. package/dist/src/bridge/site-grants.js +4 -3
  31. package/dist/src/cli.js +1 -3
  32. package/dist/src/config.js +16 -5
  33. package/dist/src/executor/cdp-executor.d.ts +3 -0
  34. package/dist/src/executor/cdp-executor.js +69 -25
  35. package/dist/src/mcp/server.js +5 -3
  36. package/dist/src/mcp/tools.d.ts +0 -12
  37. package/dist/src/mcp/tools.js +48 -22
  38. package/dist/src/remote/key.js +12 -2
  39. package/dist/src/security/policy.d.ts +4 -0
  40. package/dist/src/security/policy.js +3 -1
  41. package/docs/BLUEPRINT.md +6 -1
  42. package/docs/CLIENT-APPROVAL.md +150 -0
  43. package/extension-dist/activity-dashboard.css +43 -0
  44. package/extension-dist/background.js +1036 -73
  45. package/extension-dist/manifest.json +10 -9
  46. package/extension-dist/options.html +101 -32
  47. package/extension-dist/options.js +558 -70
  48. package/extension-dist/popup.html +164 -90
  49. package/extension-dist/popup.js +576 -79
  50. package/package.json +3 -2
@@ -1,48 +1,493 @@
1
1
  "use strict";
2
2
  (() => {
3
+ // shared/client-auth.ts
4
+ var CLIENT_GRANT_MS = 8 * 60 * 60 * 1e3;
5
+ var CLIENT_CLOCK_SKEW_MS = 2 * 60 * 1e3;
6
+ var CLIENT_REQUEST_MS = 10 * 60 * 1e3;
7
+ var CLIENT_STORAGE_KEY = "clientConnections";
8
+
3
9
  // shared/protocol.ts
4
10
  var DEFAULT_WS_PORT = 38017;
5
11
 
6
- // extension/src/connect.ts
7
- var NATIVE_HOST = "com.mehmoodqureshi.mcp_browser_extension";
8
- async function fetchPairing() {
12
+ // shared/activity.ts
13
+ var ACCESS_STOPPED_KEY = "accessStopped";
14
+ var ACTIVITY_KEY = "recentActivity";
15
+ function activityRows(entries) {
16
+ const rows = [];
17
+ const opens = /* @__PURE__ */ new Map();
18
+ for (const entry of entries) {
19
+ const key = entry.tabId !== void 0 && entry.key ? `${entry.key}:${entry.tabId}` : "";
20
+ if (key && entry.method === "tab_new" && entry.outcome === "done") opens.set(key, entry);
21
+ const opened = key && entry.method === "tab_close" && entry.outcome === "done" ? opens.get(key) : void 0;
22
+ if (opened) {
23
+ const index = rows.findIndex((row) => row.id === opened.id);
24
+ if (index !== -1) rows.splice(index, 1);
25
+ rows.push({ ...entry, host: entry.host || opened.host, opened });
26
+ opens.delete(key);
27
+ } else rows.push(entry);
28
+ }
29
+ return rows;
30
+ }
31
+ var LABELS = {
32
+ tabs_list: "List tabs",
33
+ tab_select: "Select tab",
34
+ tab_new: "Open tab",
35
+ tab_close: "Close tab",
36
+ navigate: "Navigate",
37
+ back: "Go back",
38
+ forward: "Go forward",
39
+ reload: "Reload page",
40
+ click: "Click",
41
+ draw: "Draw",
42
+ type: "Type text",
43
+ press: "Press key",
44
+ hover: "Hover",
45
+ scroll: "Scroll",
46
+ screenshot: "Take screenshot",
47
+ get_text: "Read text",
48
+ get_html: "Read HTML",
49
+ eval: "Run page script",
50
+ snapshot: "Read page structure",
51
+ fill_form: "Fill form",
52
+ get_cookies: "Read cookies",
53
+ storage: "Access site storage",
54
+ download_file: "Download file",
55
+ upload_file: "Upload file",
56
+ print_pdf: "Print PDF",
57
+ select_option: "Choose option",
58
+ observers: "Read page observations",
59
+ proxy_status: "Check proxy",
60
+ frames: "List frames"
61
+ };
62
+ var activityLabel = (method) => LABELS[method] ?? method.replaceAll("_", " ");
63
+
64
+ // extension/src/ui-controls.ts
65
+ var lists = /* @__PURE__ */ new WeakMap();
66
+ var pending = /* @__PURE__ */ new Map();
67
+ var pressing = false;
68
+ var pressGeneration = 0;
69
+ document.addEventListener("pointerdown", () => {
70
+ pressing = true;
71
+ pressGeneration++;
72
+ }, true);
73
+ function release() {
74
+ const generation = pressGeneration;
75
+ setTimeout(() => {
76
+ if (generation !== pressGeneration) return;
77
+ pressing = false;
78
+ const updates = [...pending.values()];
79
+ pending.clear();
80
+ for (const update of updates) update();
81
+ }, 0);
82
+ }
83
+ window.addEventListener("pointerup", release, true);
84
+ window.addEventListener("pointercancel", release, true);
85
+ window.addEventListener("blur", release);
86
+ function renderList(list4, items, key, signature, create) {
87
+ if (pressing) {
88
+ pending.set(list4, () => renderList(list4, items, key, signature, create));
89
+ return;
90
+ }
91
+ pending.delete(list4);
92
+ const previous = lists.get(list4) ?? /* @__PURE__ */ new Map();
93
+ const next = /* @__PURE__ */ new Map();
94
+ const scrollTop = list4.scrollTop;
95
+ let cursor = list4.firstElementChild;
96
+ for (const item of items) {
97
+ const id = key(item), version = signature(item), old = previous.get(id);
98
+ const element = old?.signature === version ? old.element : create(item);
99
+ if (old && element !== old.element) {
100
+ const details = element.querySelector("details");
101
+ if (details) details.open = old.element.querySelector("details")?.open ?? false;
102
+ if (old.element.parentElement === list4) {
103
+ if (cursor === old.element) cursor = element;
104
+ old.element.replaceWith(element);
105
+ }
106
+ }
107
+ if (element === cursor) cursor = cursor.nextElementSibling;
108
+ else list4.insertBefore(element, cursor);
109
+ next.set(id, { signature: version, element });
110
+ }
111
+ const keep = new Set([...next.values()].map((row) => row.element));
112
+ for (const child of Array.from(list4.children)) if (!keep.has(child)) child.remove();
113
+ list4.scrollTop = scrollTop;
114
+ lists.set(list4, next);
115
+ }
116
+ async function busyButton(button2, operation) {
117
+ if (button2.disabled) return;
118
+ button2.disabled = true;
119
+ button2.setAttribute("aria-busy", "true");
9
120
  try {
10
- const reply = await chrome.runtime.sendNativeMessage(NATIVE_HOST, { type: "pairing" });
11
- return reply ?? { ok: false, message: "The helper gave no answer." };
12
- } catch (err) {
13
- const why = err instanceof Error ? err.message : String(err);
14
- if (/not found|not registered|Specified native messaging host/i.test(why)) {
15
- return {
16
- ok: false,
17
- error: "no_helper",
18
- message: "Add the MCP Browser Extension server to your AI client (Claude Code, Cursor\u2026) and start it once; it sets this up. Then click Connect again."
19
- };
121
+ await operation();
122
+ } finally {
123
+ button2.disabled = false;
124
+ button2.removeAttribute("aria-busy");
125
+ }
126
+ }
127
+
128
+ // extension/src/client-connections.ts
129
+ var list = document.getElementById("client-connections");
130
+ var note = document.getElementById("client-note");
131
+ var count = document.getElementById("client-count");
132
+ function appName(reported) {
133
+ const name = reported.replace(/\s+v?\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/, "");
134
+ return name === "codex-mcp-client" ? "Codex" : name;
135
+ }
136
+ function accessStatus(client, now) {
137
+ if (client.state === "revoked") return "Access removed";
138
+ if (client.expiresAt <= now) return "Access expired";
139
+ if (client.state === "pending") return "Waiting for your approval";
140
+ const minutes = Math.ceil((client.expiresAt - now) / 6e4);
141
+ const hours = Math.floor(minutes / 60);
142
+ const remaining = hours > 0 ? `${hours} ${hours === 1 ? "hour" : "hours"}` : `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
143
+ return `Access allowed \xB7 ${remaining} left`;
144
+ }
145
+ function action(label, id, allow) {
146
+ const button2 = document.createElement("button");
147
+ button2.textContent = label;
148
+ button2.addEventListener("click", async (event) => {
149
+ if (!event.isTrusted) return;
150
+ button2.disabled = true;
151
+ try {
152
+ const result = await chrome.runtime.sendMessage({ type: "client_decision", id, allow });
153
+ note.textContent = result?.ok ? "" : result?.error ?? "Could not save the decision.";
154
+ note.dataset.error = String(!result?.ok);
155
+ } catch {
156
+ note.textContent = "Could not reach the extension. Reopen this page and try again.";
157
+ note.dataset.error = "true";
158
+ } finally {
159
+ button2.disabled = false;
20
160
  }
21
- return { ok: false, message: why };
161
+ await render();
162
+ });
163
+ return button2;
164
+ }
165
+ var renderGeneration = 0;
166
+ async function render() {
167
+ const generation = ++renderGeneration;
168
+ const state = await chrome.storage.local.get([CLIENT_STORAGE_KEY, ACCESS_STOPPED_KEY]);
169
+ if (generation !== renderGeneration) return;
170
+ const saved = state[CLIENT_STORAGE_KEY];
171
+ const stopped = state[ACCESS_STOPPED_KEY] !== void 0 && state[ACCESS_STOPPED_KEY] !== false;
172
+ const clients = Array.isArray(saved) ? saved : [];
173
+ const now = Date.now();
174
+ const pending2 = (client) => client.state === "pending" && client.expiresAt > now;
175
+ const waiting = clients.filter(pending2).length;
176
+ for (const id of ["client-help", "client-limits"]) {
177
+ const hint = document.getElementById(id);
178
+ if (hint) hint.hidden = clients.length === 0 || id === "client-help" && document.body.dataset.view === "popup" && waiting === 0;
179
+ }
180
+ if (count) {
181
+ count.textContent = waiting ? `${waiting} waiting` : `${clients.length} ${clients.length === 1 ? "connection" : "connections"}`;
182
+ count.dataset.pending = String(waiting > 0);
22
183
  }
184
+ const sorted = clients.slice().sort(
185
+ (a, b) => Number(pending2(b)) - Number(pending2(a)) || b.requestedAt - a.requestedAt
186
+ );
187
+ renderList(
188
+ list,
189
+ sorted,
190
+ (client) => client.id,
191
+ (client) => JSON.stringify([client.name, client.state, client.expiresAt, client.expiresAt <= now, stopped]),
192
+ (client) => {
193
+ const li = document.createElement("li");
194
+ li.dataset.clientId = client.id;
195
+ const name = document.createElement("strong");
196
+ name.textContent = appName(client.name);
197
+ const key = document.createElement("code");
198
+ key.textContent = `Key ${client.id.slice(0, 12)}`;
199
+ key.title = client.id;
200
+ const status = document.createElement("span");
201
+ status.className = "client-status";
202
+ const expired = client.expiresAt <= now;
203
+ li.dataset.state = client.state === "revoked" ? "revoked" : expired ? "expired" : client.state;
204
+ status.textContent = accessStatus(client, Date.now());
205
+ const info = document.createElement("div");
206
+ info.className = "client-info";
207
+ info.append(name, status);
208
+ const needsApproval = client.state === "revoked" || client.state === "pending" && !expired;
209
+ if (needsApproval) info.append(key);
210
+ const details = document.createElement("details");
211
+ details.className = "client-details";
212
+ const summary = document.createElement("summary");
213
+ summary.textContent = "Details";
214
+ details.append(summary);
215
+ for (const [field, text] of [
216
+ ["app", `Reported app: ${client.name}`],
217
+ ["requested", `Requested: ${new Date(client.requestedAt).toLocaleString()}`],
218
+ ...client.state === "approved" ? [["expires", `Access ends: ${new Date(client.expiresAt).toLocaleString()}`]] : []
219
+ ]) {
220
+ const line = document.createElement("p");
221
+ line.className = `client-${field}`;
222
+ line.textContent = text;
223
+ details.append(line);
224
+ }
225
+ if (!needsApproval) details.append(key);
226
+ const controls = document.createElement("div");
227
+ controls.className = "client-actions";
228
+ if (client.state === "revoked") controls.append(action("Approve again for 8 hours", client.id, true));
229
+ else if (client.state === "pending" && !expired) {
230
+ const approve = action("Approve for 8 hours", client.id, true);
231
+ approve.className = "primary";
232
+ controls.append(approve, action("Deny", client.id, false));
233
+ } else if (client.state === "approved" && !expired) controls.append(action("Remove access", client.id, false));
234
+ if (stopped) controls.querySelectorAll("button").forEach((button2) => {
235
+ button2.disabled = true;
236
+ });
237
+ li.append(info, controls, details);
238
+ return li;
239
+ }
240
+ );
241
+ const byId = new Map(clients.map((client) => [client.id, client]));
242
+ for (const row of Array.from(list.querySelectorAll("[data-client-id]"))) {
243
+ const client = byId.get(row.dataset.clientId);
244
+ const status = row.querySelector(".client-status");
245
+ if (client && status) {
246
+ const label = accessStatus(client, now);
247
+ if (status.textContent !== label) status.textContent = label;
248
+ const requested = row.querySelector(".client-requested");
249
+ const requestTime = `Requested: ${new Date(client.requestedAt).toLocaleString()}`;
250
+ if (requested && requested.textContent !== requestTime) requested.textContent = requestTime;
251
+ }
252
+ }
253
+ if (clients.length && note.textContent?.startsWith("No requests yet.")) note.textContent = "";
254
+ if (!clients.length) note.textContent = "No requests yet. Ask your app to use Chrome. Its request will appear here.";
255
+ if (stopped) note.textContent = "Resume connection above to approve apps.";
256
+ else if (note.textContent === "Resume connection above to approve apps.") note.textContent = "";
257
+ }
258
+ chrome.storage.onChanged.addListener((changes, area) => {
259
+ if (area === "local" && (changes[CLIENT_STORAGE_KEY] || changes[ACCESS_STOPPED_KEY])) void render();
260
+ });
261
+ void render();
262
+ setInterval(() => void render(), 3e4);
263
+
264
+ // extension/src/activity-dashboard.ts
265
+ var stop = document.getElementById("access-stop");
266
+ var resume = document.getElementById("access-resume");
267
+ var accessNote = document.getElementById("access-note");
268
+ var historyNote = document.getElementById("activity-note");
269
+ var list2 = document.getElementById("activity-list");
270
+ var filter = document.getElementById("activity-filter");
271
+ var popup = document.body.dataset.view === "popup";
272
+ var accessGeneration = 0;
273
+ var historyGeneration = 0;
274
+ async function renderAccess() {
275
+ const generation = ++accessGeneration;
276
+ try {
277
+ const saved = await chrome.storage.local.get(ACCESS_STOPPED_KEY);
278
+ if (generation !== accessGeneration) return;
279
+ const stopped = saved[ACCESS_STOPPED_KEY] !== void 0 && saved[ACCESS_STOPPED_KEY] !== false;
280
+ stop.hidden = stopped && accessNote.dataset.error !== "true";
281
+ resume.hidden = !stopped;
282
+ document.getElementById("connect").hidden = stopped;
283
+ if (accessNote.dataset.error !== "true") accessNote.textContent = stopped ? "All apps are disconnected. Resume, then approve your app again." : "Stop disconnects all apps. An action already running may finish.";
284
+ } catch {
285
+ accessNote.textContent = "Could not read access settings. Reopen this page to try again.";
286
+ }
287
+ }
288
+ var OUTCOMES = {
289
+ running: "Running",
290
+ done: "Done",
291
+ blocked: "Blocked",
292
+ failed: "Failed",
293
+ interrupted: "Interrupted"
294
+ };
295
+ async function renderHistory() {
296
+ const generation = ++historyGeneration;
297
+ try {
298
+ const saved = await chrome.storage.session.get([ACTIVITY_KEY, "activityError"]);
299
+ if (generation !== historyGeneration) return;
300
+ const entries = Array.isArray(saved[ACTIVITY_KEY]) ? saved[ACTIVITY_KEY] : [];
301
+ const selected = filter?.value ?? "all";
302
+ const visible = activityRows(entries).filter((e) => selected === "all" || e.outcome === selected).reverse().slice(0, popup ? 5 : 100);
303
+ renderList(list2, visible, (entry) => entry.opened?.id ?? entry.id, (entry) => JSON.stringify(entry), (entry) => {
304
+ const item = document.createElement("li");
305
+ item.className = "activity-item";
306
+ item.dataset.outcome = entry.outcome;
307
+ const details = document.createElement("details");
308
+ const heading = document.createElement("summary");
309
+ heading.className = "activity-row";
310
+ const host = document.createElement("span");
311
+ host.className = "activity-host";
312
+ host.textContent = entry.host || (entry.method === "tabs_list" ? "All tabs" : entry.method === "proxy_status" ? "Proxy" : "No website");
313
+ host.title = host.textContent;
314
+ const label = document.createElement("span");
315
+ label.className = "activity-label";
316
+ const completedTab = entry.outcome === "done" && (entry.method === "tab_new" || entry.method === "tab_close");
317
+ label.textContent = entry.opened ? "Opened \u2192 Closed" : completedTab ? entry.method === "tab_new" ? "Opened" : "Closed" : activityLabel(entry.method);
318
+ const outcome = document.createElement("span");
319
+ outcome.className = "activity-outcome";
320
+ outcome.textContent = OUTCOMES[entry.outcome] ?? "Unknown";
321
+ outcome.hidden = completedTab;
322
+ heading.append(host, label, outcome);
323
+ const meta = document.createElement("div");
324
+ meta.className = "activity-meta";
325
+ const app = document.createElement("span");
326
+ app.textContent = entry.key ? `${entry.app} \xB7 ${entry.key.slice(0, 12)}` : "Unverified app";
327
+ const time = document.createElement("time");
328
+ time.dateTime = new Date(entry.at).toISOString();
329
+ time.textContent = new Date(entry.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
330
+ time.title = new Date(entry.at).toLocaleString();
331
+ meta.append(app, time);
332
+ if (entry.opened) {
333
+ const opened = document.createElement("span");
334
+ opened.className = "activity-opened";
335
+ opened.textContent = `Opened ${new Date(entry.opened.at).toLocaleString()} \xB7 Closed ${new Date(entry.at).toLocaleString()}`;
336
+ meta.append(opened);
337
+ }
338
+ details.append(heading, meta);
339
+ item.append(details);
340
+ return item;
341
+ });
342
+ historyNote.textContent = saved.activityError ? "Some activity could not be saved. This history may be incomplete." : visible.length ? popup ? `Latest ${visible.length} ${visible.length === 1 ? "activity" : "activities"}.` : `${visible.length} activities shown.` : selected === "all" ? "No activity yet." : "No matching requests.";
343
+ document.getElementById("activity-clear").disabled = entries.length === 0 && !saved.activityError;
344
+ } catch {
345
+ historyNote.textContent = "Activity is unavailable. Reopen this page to try again.";
346
+ }
347
+ }
348
+ function control(id, type, note3) {
349
+ const button2 = document.getElementById(id);
350
+ button2.addEventListener("click", async (event) => {
351
+ if (!event.isTrusted) return;
352
+ button2.disabled = true;
353
+ try {
354
+ const result = await chrome.runtime.sendMessage({ type });
355
+ note3.dataset.error = String(!result?.ok);
356
+ note3.textContent = result?.ok ? "" : result?.error ?? "Could not save this change. Please try again.";
357
+ } catch {
358
+ note3.dataset.error = "true";
359
+ note3.textContent = "Could not reach the extension. Reopen this page and try again.";
360
+ } finally {
361
+ button2.disabled = false;
362
+ }
363
+ if (type !== "activity_clear") await renderAccess();
364
+ else if (note3.dataset.error !== "true") await renderHistory();
365
+ });
366
+ }
367
+ control("access-stop", "access_stop", accessNote);
368
+ control("access-resume", "access_resume", accessNote);
369
+ control("activity-clear", "activity_clear", historyNote);
370
+ document.getElementById("activity-all")?.addEventListener("click", () => {
371
+ void chrome.tabs.create({ url: `${chrome.runtime.getURL("options.html")}#activity-section` });
372
+ });
373
+ filter?.addEventListener("change", () => void renderHistory());
374
+ chrome.storage.onChanged.addListener((changes, area) => {
375
+ if (area === "local" && changes[ACCESS_STOPPED_KEY]) void renderAccess();
376
+ if (area === "session" && (changes[ACTIVITY_KEY] || changes.activityError)) void renderHistory();
377
+ });
378
+ void renderAccess();
379
+ void renderHistory();
380
+
381
+ // shared/policy.ts
382
+ function normalizeDomainPattern(pattern) {
383
+ let p = pattern.trim().toLowerCase();
384
+ if (p === "*" || p === "*://*/*" || p === "<all_urls>") return "";
385
+ p = p.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
386
+ p = p.replace(/[/?#].*$/, "");
387
+ p = p.replace(/^[^@]*@/, "").replace(/:\d+$/, "");
388
+ return p === "*" || p === "*." ? "" : p;
389
+ }
390
+
391
+ // shared/browser-permissions.ts
392
+ function siteOrigins(input) {
393
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(input) && !/^https?:\/\//i.test(input)) return [];
394
+ const pattern = normalizeDomainPattern(input);
395
+ const host = pattern.startsWith("*.") ? pattern.slice(2) : pattern;
396
+ if (!host || host.length > 253 || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/.test(host)) return [];
397
+ return [`http://${pattern}/*`, `https://${pattern}/*`];
23
398
  }
24
- async function applyPairing(reply) {
25
- await chrome.storage.local.set({
26
- wsPort: reply.port,
27
- token: reply.token,
28
- // The server's own --profile when it set one; otherwise let the server name us.
29
- ...reply.profile ? { profile: reply.profile } : {},
30
- pairingSource: "native"
399
+ var OPTIONAL_FEATURES = {
400
+ cookies: { label: "Cookies", explanation: "Read cookies on websites you have allowed. Cookie values still follow your server settings." },
401
+ downloads: { label: "Downloads", explanation: "Save files from websites you have allowed." }
402
+ };
403
+ var FEATURE_REQUESTS_KEY = "browserPermissionRequests";
404
+ var PROXY_AUTH_PERMISSIONS = ["webRequest", "webRequestAuthProvider"];
405
+ var IP_LOOKUP_ORIGINS = ["https://ipinfo.io/*", "https://api.ipify.org/*"];
406
+
407
+ // extension/src/extra-permissions.ts
408
+ var section = document.getElementById("extra-permissions");
409
+ var list3 = document.getElementById("extra-permissions-list");
410
+ var note2 = document.getElementById("extra-permissions-note");
411
+ var rendering = 0;
412
+ async function render2() {
413
+ if (!section || !list3 || !note2) return;
414
+ const generation = ++rendering;
415
+ const saved = (await chrome.storage.local.get(FEATURE_REQUESTS_KEY))[FEATURE_REQUESTS_KEY];
416
+ const pending2 = Array.isArray(saved) ? saved : [];
417
+ const features = [];
418
+ for (const feature of Object.keys(OPTIONAL_FEATURES)) {
419
+ const granted = await chrome.permissions.contains({ permissions: [feature] }).catch(() => false);
420
+ if (!granted && !pending2.includes(feature)) continue;
421
+ features.push({ feature, granted });
422
+ }
423
+ if (generation !== rendering) return;
424
+ renderList(list3, features, ({ feature }) => feature, ({ granted }) => String(granted), ({ feature, granted }) => {
425
+ const { label, explanation } = OPTIONAL_FEATURES[feature];
426
+ const row = document.createElement("li");
427
+ row.className = "client-row";
428
+ row.dataset.feature = feature;
429
+ const title = document.createElement("strong");
430
+ title.textContent = label;
431
+ const description = document.createElement("p");
432
+ description.className = "note hint";
433
+ description.textContent = explanation;
434
+ const button2 = document.createElement("button");
435
+ button2.className = granted ? "ghost" : "primary";
436
+ button2.textContent = granted ? `Remove ${label.toLowerCase()}` : `Allow ${label.toLowerCase()}`;
437
+ button2.addEventListener("click", async (event) => {
438
+ if (!event.isTrusted) return;
439
+ try {
440
+ await busyButton(button2, async () => {
441
+ const result = await chrome.runtime.sendMessage({ type: "feature_access", feature, allow: !granted });
442
+ note2.textContent = result?.ok ? "" : result?.error ?? "Could not change this permission. Please try again.";
443
+ });
444
+ } catch {
445
+ note2.textContent = "Could not change this permission. Please try again.";
446
+ }
447
+ await render2();
448
+ });
449
+ row.append(title, description, button2);
450
+ return row;
31
451
  });
452
+ section.hidden = features.length === 0;
32
453
  }
33
- async function connectWithOneClick() {
34
- let granted = false;
454
+ chrome.storage.onChanged.addListener((changes, area) => {
455
+ if (area === "local" && changes[FEATURE_REQUESTS_KEY]) void render2();
456
+ });
457
+ chrome.permissions.onAdded.addListener(() => void render2());
458
+ chrome.permissions.onRemoved.addListener(() => void render2());
459
+ void render2();
460
+
461
+ // extension/src/site-access.ts
462
+ async function permittedSites(sites) {
463
+ const checks = await Promise.all(sites.map(async (site) => {
464
+ const origins = siteOrigins(site);
465
+ return origins.length && await chrome.permissions.contains({ origins }).catch(() => false) ? site : null;
466
+ }));
467
+ return checks.filter((site) => site !== null);
468
+ }
469
+ async function changeSiteAccess(input, allow) {
470
+ const origins = siteOrigins(input);
471
+ if (!origins.length) return { ok: false, error: "Please enter one website, such as example.com. All-site access is not available." };
35
472
  try {
36
- granted = await chrome.permissions.request({ permissions: ["nativeMessaging"] });
473
+ const result = await chrome.runtime.sendMessage({
474
+ type: "site_access",
475
+ host: normalizeDomainPattern(input),
476
+ allow
477
+ });
478
+ return result ?? { ok: false, error: "Please start your connected app and try again." };
37
479
  } catch (err) {
38
- return { ok: false, message: err instanceof Error ? err.message : String(err) };
480
+ return { ok: false, error: err instanceof Error ? err.message : "Could not change this website permission. Please try again." };
481
+ }
482
+ }
483
+
484
+ // extension/src/connect.ts
485
+ async function connectWithOneClick() {
486
+ try {
487
+ return await chrome.runtime.sendMessage({ type: "connect_with_click" }) ?? { ok: false, message: "Could not reach the extension." };
488
+ } catch {
489
+ return { ok: false, message: "Could not reach the extension. Reopen the popup and try again." };
39
490
  }
40
- if (!granted) return { ok: false, message: "Chrome needs that permission to fetch the pairing for you. You can also pair by hand in Settings." };
41
- const reply = await fetchPairing();
42
- if (!reply.ok) return { ok: false, message: reply.message };
43
- await applyPairing(reply);
44
- await chrome.runtime.sendMessage({ type: "reconnect" }).catch(() => void 0);
45
- return { ok: true };
46
491
  }
47
492
 
48
493
  // shared/proxy.ts
@@ -107,12 +552,13 @@
107
552
  sourceEl.textContent = pairingSource === "auto" ? "Paired automatically from the pairing.json the server wrote into this extension folder. Saving here overrides it." : pairingSource === "manual" ? "Paired by hand. Saved values take precedence over the bundled pairing.json." : typeof wsPort === "number" && wsPort > 0 && connState !== "idle" ? "Paired with values saved by an earlier version. Saving here keeps them manual." : "Not paired yet. If you loaded this extension from the MCP Browser Extension package folder, start the server once and it pairs itself; otherwise paste the values below.";
108
553
  portEl.value = typeof wsPort === "number" && wsPort > 0 ? String(wsPort) : String(DEFAULT_WS_PORT);
109
554
  profileEl.value = typeof profile === "string" ? profile : "";
110
- render(typeof connState === "string" ? connState : "idle");
555
+ render3(typeof connState === "string" ? connState : "idle");
111
556
  }
112
557
  var quickEl = document.getElementById("quick");
113
558
  var connectEl = document.getElementById("connect");
114
559
  var connectMsgEl = document.getElementById("connect-msg");
115
- connectEl.addEventListener("click", async () => {
560
+ connectEl.addEventListener("click", async (event) => {
561
+ if (!event.isTrusted) return;
116
562
  connectEl.disabled = true;
117
563
  connectEl.textContent = "Connecting\u2026";
118
564
  const res = await connectWithOneClick();
@@ -121,19 +567,23 @@
121
567
  connectMsgEl.className = res.ok ? "hint" : "hint error";
122
568
  connectMsgEl.textContent = res.ok ? "Connecting\u2026" : res.message ?? "Could not connect.";
123
569
  });
124
- function render(state) {
570
+ function render3(state) {
125
571
  quickEl.hidden = false;
126
572
  if (!connectEl.disabled) connectEl.textContent = state === "connected" ? "Connect again" : "Connect";
127
573
  const labels = {
128
- connected: "\u2705 connected",
574
+ connected: "connected",
129
575
  connecting: "\u2026 connecting",
130
- unauthorized: "\u26D4 rejected (bad/stale token \u2014 re-paste)",
131
- superseded: "\u26D4 another browser connected under the same profile name. Type a different Profile name above and Save & connect.",
576
+ unauthorized: "rejected (bad/stale token \u2014 re-paste)",
577
+ superseded: "another browser connected under the same profile name. Type a different Profile name above and Save & connect.",
132
578
  idle: "\u25CB not connected"
133
579
  };
134
580
  statusEl.textContent = `Status: ${labels[state] ?? state}`;
581
+ void chrome.storage.local.get(ACCESS_STOPPED_KEY).then((saved) => {
582
+ if (saved[ACCESS_STOPPED_KEY]) statusEl.textContent = "Status: access stopped";
583
+ });
135
584
  }
136
- saveEl.addEventListener("click", async () => {
585
+ saveEl.addEventListener("click", async (event) => {
586
+ if (!event.isTrusted) return;
137
587
  const wsPort = Number(portEl.value);
138
588
  const stored = await chrome.storage.local.get("token");
139
589
  const token = tokenEl.value.trim() || (typeof stored.token === "string" ? stored.token : "");
@@ -143,8 +593,8 @@
143
593
  return;
144
594
  }
145
595
  await chrome.storage.local.set({ wsPort, token, profile, pairingSource: "manual" });
146
- await chrome.runtime.sendMessage({ type: "reconnect" }).catch(() => void 0);
147
- statusEl.textContent = "Status: \u2026 connecting";
596
+ const result = await chrome.runtime.sendMessage({ type: "reconnect" }).catch(() => void 0);
597
+ statusEl.textContent = result?.ok === false ? `Status: ${result.error}` : "Status: \u2026 connecting";
148
598
  });
149
599
  var blockedWrapEl = document.getElementById("blocked-wrap");
150
600
  var blockedEl = document.getElementById("blocked");
@@ -153,13 +603,18 @@
153
603
  var newSiteEl = document.getElementById("new-site");
154
604
  var addSiteEl = document.getElementById("add-site");
155
605
  async function grant(host, allow) {
156
- const res = await chrome.runtime.sendMessage({ type: "site_grant", host, allow }).catch(() => null);
157
- sitesNoteEl.textContent = res?.ok ? "" : `Could not reach the MCP Browser Extension server${res?.error ? ` (${res.error})` : ""}. Is your AI client running?`;
606
+ const res = await changeSiteAccess(host, allow);
607
+ await renderSites();
608
+ sitesNoteEl.textContent = res.ok ? "" : res.error ?? "Could not change website access. Please try again.";
158
609
  }
159
610
  function button(label, onClick) {
160
611
  const b = document.createElement("button");
161
612
  b.textContent = label;
162
- b.addEventListener("click", onClick);
613
+ b.addEventListener("click", (event) => {
614
+ if (event.isTrusted) void busyButton(b, onClick).catch(() => {
615
+ sitesNoteEl.textContent = "Could not finish that action. Please try again.";
616
+ });
617
+ });
163
618
  return b;
164
619
  }
165
620
  function siteRow(host, extra) {
@@ -175,36 +630,46 @@
175
630
  t.textContent = text;
176
631
  return t;
177
632
  }
633
+ var sitesGeneration = 0;
178
634
  async function renderSites() {
179
- const { allowedSites, grantedSites, blockedSites, connState } = await chrome.storage.local.get([
635
+ const generation = ++sitesGeneration;
636
+ const { allowedSites, grantedSites, blockedSites, dismissedSites, connState } = await chrome.storage.local.get([
180
637
  "allowedSites",
181
638
  "grantedSites",
182
639
  "blockedSites",
640
+ "dismissedSites",
183
641
  "connState"
184
642
  ]);
185
- const allowed = Array.isArray(allowedSites) ? allowedSites : [];
643
+ const allowed = Array.isArray(allowedSites) ? allowedSites.map(normalizeDomainPattern).filter(Boolean) : [];
186
644
  const granted = Array.isArray(grantedSites) ? grantedSites : null;
187
- const blocked = Array.isArray(blockedSites) ? blockedSites : [];
188
- blockedEl.replaceChildren(
189
- ...blocked.map(
190
- (b) => siteRow(b.host, [
191
- tag(b.method ? `blocked ${b.method}` : "blocked"),
192
- button("Allow", () => void grant(b.host, true)),
193
- button("Dismiss", () => void chrome.runtime.sendMessage({ type: "dismiss_blocked", host: b.host }))
194
- ])
195
- )
645
+ const permitted = await permittedSites(allowed);
646
+ if (generation !== sitesGeneration) return;
647
+ const pending2 = Array.isArray(blockedSites) ? blockedSites : [];
648
+ const blocked = [.../* @__PURE__ */ new Set([...pending2.map((b) => b.host), ...allowed.filter((site) => !permitted.includes(site))])].filter((host) => !permitted.includes(host) && (!Array.isArray(dismissedSites) || !dismissedSites.includes(host)));
649
+ renderList(
650
+ blockedEl,
651
+ blocked,
652
+ (host) => host,
653
+ (host) => host,
654
+ (host) => siteRow(host, [
655
+ tag("needs permission"),
656
+ button("Allow", () => grant(host, true)),
657
+ button("Dismiss", () => chrome.runtime.sendMessage({ type: "dismiss_blocked", host }))
658
+ ])
196
659
  );
197
- blockedWrapEl.hidden = blocked.length === 0 || granted === null;
198
- allowedEl.replaceChildren(
199
- ...allowed.map(
200
- (host) => host === "*" ? siteRow("Every site", [tag("--unsafe-all-domains")]) : granted?.includes(host) ? siteRow(host, [button("Remove", () => void grant(host, false))]) : siteRow(host, [tag("set by server flags")])
201
- )
660
+ blockedWrapEl.hidden = blocked.length === 0 || connState !== "connected";
661
+ renderList(
662
+ allowedEl,
663
+ permitted,
664
+ (host) => host,
665
+ (host) => JSON.stringify([host, granted?.includes(host)]),
666
+ (host) => siteRow(host, [...granted?.includes(host) ? [] : [tag("set by server flags")], button("Remove", () => grant(host, false))])
202
667
  );
203
668
  if (connState !== "connected") {
204
669
  sitesNoteEl.textContent = "Connect to the MCP Browser Extension server to see and change the allowed sites.";
205
670
  } else if (granted === null) {
206
671
  sitesNoteEl.textContent = "This MCP Browser Extension server does not take sites from here (it is older, or runs with --no-site-grants). Use --allow-domain instead.";
207
- } else if (allowed.length === 0) {
672
+ } else if (permitted.length === 0) {
208
673
  sitesNoteEl.textContent = "No sites are allowed yet.";
209
674
  } else {
210
675
  sitesNoteEl.textContent = "";
@@ -213,23 +678,33 @@
213
678
  addSiteEl.disabled = !canGrant;
214
679
  newSiteEl.disabled = !canGrant;
215
680
  }
216
- addSiteEl.addEventListener("click", () => {
681
+ function addSite(event) {
682
+ if (!event.isTrusted) return;
217
683
  const host = newSiteEl.value.trim();
218
684
  if (!host) return;
219
685
  newSiteEl.value = "";
220
686
  void grant(host, true);
221
- });
687
+ }
688
+ addSiteEl.addEventListener("click", addSite);
222
689
  newSiteEl.addEventListener("keydown", (e) => {
223
- if (e.key === "Enter") addSiteEl.click();
690
+ if (e.key === "Enter") {
691
+ e.preventDefault();
692
+ addSite(e);
693
+ }
224
694
  });
225
695
  chrome.storage.onChanged.addListener((changes, area) => {
226
- if (area === "local" && changes.connState) render(String(changes.connState.newValue));
696
+ if (area === "local" && changes[ACCESS_STOPPED_KEY]) {
697
+ void chrome.storage.local.get("connState").then((saved) => render3(String(saved.connState ?? "idle")));
698
+ }
699
+ if (area === "local" && changes.connState) render3(String(changes.connState.newValue));
227
700
  if (area === "local" && changes.pairedProfile) renderPairedAs(changes.pairedProfile.newValue);
228
- if (area === "local" && (changes.allowedSites || changes.grantedSites || changes.blockedSites || changes.connState)) {
701
+ if (area === "local" && (changes.allowedSites || changes.grantedSites || changes.blockedSites || changes.dismissedSites || changes.connState)) {
229
702
  void renderSites();
230
703
  }
231
704
  });
232
705
  void renderSites();
706
+ chrome.permissions.onAdded.addListener(() => void renderSites());
707
+ chrome.permissions.onRemoved.addListener(() => void renderSites());
233
708
  void loadExisting();
234
709
  var proxyEnabledEl = document.getElementById("proxy-enabled");
235
710
  var proxySchemeEl = document.getElementById("proxy-scheme");
@@ -269,7 +744,8 @@
269
744
  }
270
745
  proxyHostEl.addEventListener("change", splitPastedProxy);
271
746
  proxyHostEl.addEventListener("paste", () => setTimeout(splitPastedProxy, 0));
272
- proxyWebrtcEl.addEventListener("change", async () => {
747
+ proxyWebrtcEl.addEventListener("change", async (event) => {
748
+ if (!event.isTrusted) return;
273
749
  if (proxyWebrtcEl.checked) {
274
750
  proxyWebrtcEl.checked = await chrome.permissions.request({ permissions: ["privacy"] }).catch(() => false);
275
751
  } else {
@@ -278,9 +754,9 @@
278
754
  }
279
755
  await chrome.runtime.sendMessage({ type: "proxy_apply" }).catch(() => void 0);
280
756
  });
281
- proxySaveEl.addEventListener("click", async () => {
757
+ proxySaveEl.addEventListener("click", async (event) => {
758
+ if (!event.isTrusted) return;
282
759
  splitPastedProxy();
283
- const stored = parseProxyConfig((await chrome.storage.local.get(PROXY_STORAGE_KEY))[PROXY_STORAGE_KEY]);
284
760
  const host = proxyHostEl.value.trim();
285
761
  const port = Number(proxyPortEl.value);
286
762
  const enabled = proxyEnabledEl.checked;
@@ -288,16 +764,28 @@
288
764
  return proxyNote("Proxy: enter a host and a port (1-65535) first", "err");
289
765
  }
290
766
  const username = proxyUserEl.value.trim();
291
- const password = proxyPassEl.value || (stored && stored.username === username ? stored.password : "");
292
767
  const scheme = proxySchemeEl.value;
293
768
  if (enabled && username && scheme.startsWith("socks")) {
294
769
  return proxyNote("Proxy: Chrome cannot log in to a SOCKS proxy. Pick HTTP or HTTPS, or use a proxy without a login.", "err");
295
770
  }
771
+ if (enabled && username && !await chrome.permissions.request({ permissions: PROXY_AUTH_PERMISSIONS }).catch(() => false)) {
772
+ return proxyNote("Proxy access was not added. Click Save proxy whenever you are ready.", "err");
773
+ }
774
+ const stored = parseProxyConfig((await chrome.storage.local.get(PROXY_STORAGE_KEY))[PROXY_STORAGE_KEY]);
775
+ const password = proxyPassEl.value || (stored && stored.username === username ? stored.password : "");
296
776
  await chrome.storage.local.set({ [PROXY_STORAGE_KEY]: { enabled, scheme, host, port, username, password } });
777
+ if (!enabled) {
778
+ if (await chrome.permissions.contains({ permissions: ["proxy"] })) await chrome.proxy.settings.clear({ scope: "regular" });
779
+ await chrome.permissions.remove({ permissions: PROXY_AUTH_PERMISSIONS });
780
+ }
297
781
  proxyPassEl.value = "";
298
782
  proxyNote(enabled ? `Proxy: on (${scheme}://${host}:${port}). Click Check IP to confirm.` : "Proxy: off", enabled ? "on" : "");
299
783
  });
300
- proxyCheckEl.addEventListener("click", async () => {
784
+ proxyCheckEl.addEventListener("click", async (event) => {
785
+ if (!event.isTrusted) return;
786
+ if (!await chrome.permissions.request({ origins: IP_LOOKUP_ORIGINS }).catch(() => false)) {
787
+ return proxyNote("IP lookup permission was not added. You can try again whenever you are ready.", "err");
788
+ }
301
789
  proxyCheckEl.disabled = true;
302
790
  proxyNote("Proxy: checking\u2026");
303
791
  const res = await chrome.runtime.sendMessage({ type: "proxy_status" }).catch(() => null);