mcp-browser-extension 0.9.26 → 0.9.28

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 CHANGED
@@ -82,8 +82,8 @@ or load from the folder the server unpacks.
82
82
  ### Up and running in one paste
83
83
 
84
84
  Hand this to your AI agent (Claude Code, Cursor, Windsurf, anything MCP) and it
85
- installs the server, wires it into the client, and walks you through the two
86
- steps that must happen inside Chrome:
85
+ installs the server, wires it into the client, and walks you through installing
86
+ the extension, verifying your email, and pairing Chrome:
87
87
 
88
88
  ```text
89
89
  Set up MCP Browser Extension on this machine by fetching and following
@@ -140,6 +140,15 @@ By default everything is **deny-all** (no domains, no eval, no mutations). Grant
140
140
  exactly what you need with `--allow-domain <glob>` (repeatable), `--enable-mutations`,
141
141
  `--enable-downloads`, `--enable-uploads`, `--unsafe-enable-eval`, or `--unsafe-all-domains`.
142
142
 
143
+ Email verification is on by default. For a trusted, local unattended setup, you
144
+ can opt out with `--no-email-verification` (or set
145
+ `MCP_BROWSER_EXTENSION_EMAIL_VERIFICATION=0`). This skips the email prompt and
146
+ the Firebase access service, so no email or device grant is sent there. The
147
+ local MCP client can then use the browser within the configured domain and
148
+ capability policy without an account check. Treat every process that can launch
149
+ or reach this local server as trusted. This option is rejected with `--tunnel`
150
+ and does not turn off telemetry or the npm update check.
151
+
143
152
  > `--enable-uploads` permits `upload_file` (setting local file(s) on a page's file
144
153
  > `<input>`). It is **off by default** because sending local files to a page is an
145
154
  > exfiltration risk; it is also gated by the destination-domain allowlist. Pair it
@@ -162,7 +171,19 @@ explicitly (and is never written to disk).
162
171
  One click, no Developer mode, and **Chrome keeps it updated for you**. This is
163
172
  the way to install it.
164
173
 
165
- **3. Click Connect.** A fresh install opens the extension's Settings with a
174
+ **3. Verify your email.** In your MCP client, ask the agent to call
175
+ `chrome_status`. On this first browser tool call, the client shows an email
176
+ prompt. Enter an address you can access, then open the sign-in link sent to
177
+ that address. The waiting tool reports **Email verified** and continues.
178
+
179
+ If the tool returns `ACCESS_PENDING`, ask the agent to call `chrome_status`
180
+ again to keep waiting on the same link. You do not need another email or a
181
+ verification code. The MCP client must support interactive input prompts
182
+ (MCP elicitation). If no prompt appears, enable interactive prompts in your
183
+ client and retry; installing the extension alone does not activate access.
184
+
185
+ **4. Click Connect if Chrome is not already paired.** A fresh install opens the
186
+ extension's Settings with a
166
187
  **Connect** button (it is in the toolbar popup too). Click it, allow the one
167
188
  permission Chrome asks for, and you are paired: the server registers a small
168
189
  helper with Chrome every time it starts, and the button asks it for the port and
package/SETUP.md CHANGED
@@ -59,6 +59,12 @@ if they want the agent to click, type, and navigate (almost always yes). Add
59
59
  `--persist-token` so the pairing survives restarts. Leave every other gate off
60
60
  unless they ask.
61
61
 
62
+ Email verification stays enabled by default. Only if the human asks for a
63
+ trusted local or unattended setup, add `--no-email-verification`. It skips the
64
+ email prompt and Firebase access check, while the domain allowlist and other
65
+ capability gates still apply. Do not combine it with `--tunnel` or use it when
66
+ untrusted clients can reach the local server.
67
+
62
68
  **Claude Code** (registers for every project on this machine):
63
69
 
64
70
  ```
@@ -129,6 +135,10 @@ the result includes **Email verified** and the requested browser action continue
129
135
  The user does not need to send another chat message. Progress updates and status
130
136
  notifications are also sent to clients that display them.
131
137
 
138
+ If the human explicitly configured `--no-email-verification`, there is no email
139
+ step; continue to pairing. This option is intended only for trusted local
140
+ clients and cannot be combined with a public tunnel.
141
+
132
142
  Each verification wait lasts up to 45 seconds. If the result says
133
143
  `ACCESS_PENDING`, call `chrome_status` again to continue waiting on the same
134
144
  link. Expired or denied verification returns a clear failure; browser actions
@@ -18,4 +18,6 @@ declare function result(message: string): {
18
18
  };
19
19
  /** Returns null when authorized, otherwise a user-facing tool error. */
20
20
  export declare function requireAccess(server: Server, dataDir: string, request?: AccessRequest): Promise<ReturnType<typeof result> | null>;
21
+ /** Explicit local-only opt-out; the CLI rejects it when a public tunnel is enabled. */
22
+ export declare const bypassEmailVerification: typeof requireAccess;
21
23
  export {};
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bypassEmailVerification = void 0;
3
4
  exports.setAccessPollIntervalForTesting = setAccessPollIntervalForTesting;
4
5
  exports.requireAccess = requireAccess;
5
6
  /** Email-link access gate for the MCP tool surface. */
@@ -209,6 +210,9 @@ async function requireAccess(server, dataDir, request = {}) {
209
210
  completionToReport.delete(dataDir);
210
211
  return outcome ? result(outcome.message) : null;
211
212
  }
213
+ /** Explicit local-only opt-out; the CLI rejects it when a public tunnel is enabled. */
214
+ const bypassEmailVerification = async () => null;
215
+ exports.bypassEmailVerification = bypassEmailVerification;
212
216
  async function check(server, dataDir, request) {
213
217
  const state = loadOrCreate(dataDir);
214
218
  try {
package/dist/src/cli.js CHANGED
@@ -14,6 +14,7 @@ const compat_1 = require("./compat");
14
14
  const node_fs_1 = require("node:fs");
15
15
  const node_path_1 = require("node:path");
16
16
  const config_1 = require("./config");
17
+ const access_1 = require("./access");
17
18
  const tasks_1 = require("./bridge/tasks");
18
19
  const manager_1 = require("./executor/manager");
19
20
  const select_1 = require("./executor/select");
@@ -225,6 +226,7 @@ async function main() {
225
226
  // silent from the first line rather than from wherever the first log happened
226
227
  // to sit after startup.
227
228
  (0, server_2.setLogLevel)(cfg.logLevel);
229
+ const accessGuard = cfg.noEmailVerification ? access_1.bypassEmailVerification : access_1.requireAccess;
228
230
  if (cfg.showHelp) {
229
231
  process.stdout.write(config_1.HELP_TEXT);
230
232
  // The catalog is what `--tools` takes, so `--help` has to name it.
@@ -250,6 +252,9 @@ async function main() {
250
252
  });
251
253
  if (handedOff !== null)
252
254
  process.exit(handedOff);
255
+ if (cfg.noEmailVerification) {
256
+ (0, server_2.logErr)('SECURITY: hosted email verification is disabled; only trusted local MCP clients should connect. The browser-domain allowlist and other safety gates remain active.');
257
+ }
253
258
  // Before the bridge binds a port or writes a handshake: a typo'd `--tools`
254
259
  // name should fail as a plain startup error, not leave a half-started server.
255
260
  (0, tools_1.setToolAllowlist)(cfg.tools);
@@ -449,7 +454,7 @@ async function main() {
449
454
  });
450
455
  // The resolved policy shapes the advertised catalog: a capability that is off
451
456
  // means its tools are never described to the model.
452
- await (0, server_2.startMcpServer)(version(), cfg.policy);
457
+ await (0, server_2.startMcpServer)(version(), cfg.policy, accessGuard);
453
458
  }
454
459
  /**
455
460
  * `--http` / `--tunnel`: a standalone server for clients on other machines.
@@ -459,7 +464,14 @@ async function serveRemote(cfg, dataDir, stopBridge) {
459
464
  const key = (0, key_1.resolveRemoteKey)(dataDir, { rotate: cfg.rotateRemoteKey });
460
465
  if (cfg.rotateRemoteKey)
461
466
  (0, server_2.logErr)('remote key rotated: every earlier connector URL stops working.');
462
- const http = await (0, http_1.startHttpMcp)({ port: cfg.httpPort, key, version: version(), policy: cfg.policy, log: (m) => (0, server_2.logErr)(m) });
467
+ const http = await (0, http_1.startHttpMcp)({
468
+ port: cfg.httpPort,
469
+ key,
470
+ version: version(),
471
+ policy: cfg.policy,
472
+ log: (m) => (0, server_2.logErr)(m),
473
+ accessGuard: cfg.noEmailVerification ? access_1.bypassEmailVerification : undefined,
474
+ });
463
475
  let tunnel = null;
464
476
  let shuttingDown = false;
465
477
  const shutdown = () => {
@@ -38,6 +38,8 @@ export interface CliConfig {
38
38
  persistToken: boolean;
39
39
  /** `--no-telemetry`: send no anonymous usage statistics (see src/telemetry.ts). */
40
40
  noTelemetry: boolean;
41
+ /** `--no-email-verification`: bypass the hosted access gate for trusted local use. */
42
+ noEmailVerification: boolean;
41
43
  /** `--no-site-grants`: the allowlist is exactly what the flags say; the
42
44
  * extension's Options page cannot add sites at runtime. */
43
45
  noSiteGrants: boolean;
@@ -82,6 +82,7 @@ function parseArgs(argv) {
82
82
  let printPairing = false;
83
83
  let persistToken = false;
84
84
  let noTelemetry = false;
85
+ let noEmailVerification = ['0', 'false', 'off'].includes(((0, compat_1.runtimeEnv)('EMAIL_VERIFICATION') ?? '').toLowerCase());
85
86
  let noSiteGrants = false;
86
87
  let noAutoUpdate = false;
87
88
  let noNativeHost = false;
@@ -192,6 +193,9 @@ function parseArgs(argv) {
192
193
  case '--no-telemetry':
193
194
  noTelemetry = true;
194
195
  break;
196
+ case '--no-email-verification':
197
+ noEmailVerification = true;
198
+ break;
195
199
  case '--no-site-grants':
196
200
  noSiteGrants = true;
197
201
  break;
@@ -234,6 +238,11 @@ function parseArgs(argv) {
234
238
  cdpFallback = false;
235
239
  cdpEndpoint = undefined;
236
240
  prefer = 'extension';
241
+ // The local verification bypass is intended for trusted processes on this
242
+ // machine. Do not expose it through a public Cloudflare tunnel.
243
+ if (noEmailVerification && tunnel) {
244
+ throw new Error('--no-email-verification cannot be used with --tunnel');
245
+ }
237
246
  // File first, then flags win.
238
247
  const policy = (0, policy_1.resolvePolicy)({ ...policyFile, ...policyFlags });
239
248
  // Uploads must be confined to a directory — refuse to start with uploads enabled
@@ -271,6 +280,7 @@ function parseArgs(argv) {
271
280
  printPairing,
272
281
  persistToken,
273
282
  noTelemetry,
283
+ noEmailVerification,
274
284
  noSiteGrants,
275
285
  noAutoUpdate,
276
286
  noNativeHost,
@@ -375,6 +385,11 @@ Connection:
375
385
  MCP_BROWSER_EXTENSION_TELEMETRY=0 or DO_NOT_TRACK=1. Only counts are
376
386
  ever sent (version, OS, tool calls, error codes) — never
377
387
  URLs, page content or arguments.
388
+ --no-email-verification Skip the hosted email/device access gate for trusted local use.
389
+ Email and device data are not sent to the access service.
390
+ Same as MCP_BROWSER_EXTENSION_EMAIL_VERIFICATION=0.
391
+ Cannot be combined with --tunnel; the domain allowlist,
392
+ deny-all defaults, and other safety gates still apply.
378
393
 
379
394
  Backend:
380
395
  This build is EXTENSION-ONLY — it drives ONLY your real Chrome via the paired
@@ -310,6 +310,7 @@ export interface Executor {
310
310
  outer?: boolean;
311
311
  } & FrameOpts): Promise<{
312
312
  html: string;
313
+ frameId?: number;
313
314
  }>;
314
315
  /** Accessibility snapshot: interactive/landmark elements with stable refs the model can target. */
315
316
  snapshot(opts?: {
@@ -5,7 +5,7 @@
5
5
  * when the backend can (else sequences fill+click).
6
6
  * (Only `download_file` is privileged and lives on the executor.)
7
7
  */
8
- import type { Executor, FrameOpts } from '../executor/types';
8
+ import { type Executor, type FrameOpts } from '../executor/types';
9
9
  export interface LinkOut {
10
10
  href: string;
11
11
  text: string;
@@ -10,6 +10,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.extractLinks = extractLinks;
11
11
  exports.readAsMarkdown = readAsMarkdown;
12
12
  exports.fillForm = fillForm;
13
+ const htmlparser2_1 = require("htmlparser2");
14
+ const types_1 = require("../executor/types");
13
15
  const markdown_extract_1 = require("./markdown-extract");
14
16
  /**
15
17
  * Collect anchors from the page (or a subtree). Implemented as a single page
@@ -25,31 +27,99 @@ async function extractLinks(ex, args) {
25
27
  const root = ${root} ? document.querySelector(${root}) : document;
26
28
  if (!root) return [];
27
29
  const here = location.origin;
28
- return [...root.querySelectorAll('a[href]')].map(a => ({
29
- href: a.href, text: (a.textContent || '').trim().slice(0, 200),
30
- })).filter(l => l.href && (${args.sameOriginOnly ? 'l.href.startsWith(here)' : 'true'}));
30
+ return [...root.querySelectorAll('a[href]')].filter(a => {
31
+ const raw = (a.getAttribute('href') || '').trim();
32
+ if (!raw || raw.startsWith('#')) return false;
33
+ try {
34
+ const url = new URL(a.href);
35
+ return !['javascript:', 'data:', 'blob:'].includes(url.protocol) &&
36
+ (${args.sameOriginOnly ? 'url.origin === here' : 'true'});
37
+ } catch { return false; }
38
+ }).map(a => ({ href: a.href, text: (a.textContent || '').trim().slice(0, 200) }));
31
39
  })()`;
32
40
  const frames = { frameId: args.frameId, allFrames: args.allFrames };
33
41
  let links;
34
- const res = await ex.eval(expr, { tabId: args.tabId, ...frames });
35
- if (res.ok && Array.isArray(res.value)) {
36
- links = res.value;
42
+ try {
43
+ const res = await ex.eval(expr, { tabId: args.tabId, ...frames });
44
+ if (res.ok && Array.isArray(res.value))
45
+ links = res.value;
37
46
  }
38
- else {
47
+ catch (err) {
48
+ if (!(err instanceof types_1.ExecutorError) || err.code !== 'POLICY_DENIED')
49
+ throw err;
50
+ }
51
+ if (!links) {
39
52
  // Fallback: parse hrefs out of the HTML (e.g. when eval is policy-denied).
40
- const { html } = await ex.getHtml(args.selector ? { selector: args.selector } : undefined, {
53
+ const result = await ex.getHtml(args.selector ? { selector: args.selector } : undefined, {
41
54
  tabId: args.tabId,
42
55
  ...frames,
43
56
  });
44
- links = [];
45
- const re = /<a[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
46
- let m;
47
- while ((m = re.exec(html)) !== null) {
48
- links.push({ href: m[1], text: m[2].replace(/<[^>]+>/g, '').trim().slice(0, 200) });
57
+ const tabs = await ex.tabsList();
58
+ const tab = args.tabId ? tabs.find(t => t.tabId === args.tabId) : tabs.find(t => t.active);
59
+ let pageUrl = tab?.url;
60
+ const frameId = result.frameId ?? args.frameId;
61
+ if (frameId !== undefined && frameId !== 0) {
62
+ pageUrl = (await ex.framesList?.({ tabId: args.tabId }))?.find(f => f.frameId === frameId)?.url;
49
63
  }
64
+ if (!pageUrl)
65
+ throw new types_1.ExecutorError('TARGET_GONE', 'could not resolve the page URL for link extraction');
66
+ links = linksFromHtml(result.html, pageUrl, args.sameOriginOnly);
50
67
  }
51
68
  return { links: refineLinks(links, args) };
52
69
  }
70
+ /** Parse serialized HTML without executing page code; the parser decodes entities. */
71
+ function linksFromHtml(html, pageUrl, sameOriginOnly = false) {
72
+ const raw = [];
73
+ let current;
74
+ let baseHref;
75
+ let skippedDepth = 0;
76
+ new htmlparser2_1.Parser({
77
+ onopentag(name, attrs) {
78
+ if (skippedDepth || ['script', 'style', 'template'].includes(name)) {
79
+ skippedDepth++;
80
+ return;
81
+ }
82
+ if (name === 'base' && baseHref === undefined && attrs.href !== undefined)
83
+ baseHref = attrs.href;
84
+ if (name === 'a' && attrs.href !== undefined) {
85
+ current = { href: attrs.href.trim(), text: '' };
86
+ raw.push(current);
87
+ }
88
+ },
89
+ ontext(text) { if (current && !skippedDepth)
90
+ current.text += text; },
91
+ onclosetag(name) {
92
+ if (skippedDepth) {
93
+ skippedDepth--;
94
+ return;
95
+ }
96
+ if (name === 'a')
97
+ current = undefined;
98
+ },
99
+ }, { decodeEntities: true }).end(html);
100
+ const here = new URL(pageUrl);
101
+ let base = here.href;
102
+ try {
103
+ if (baseHref !== undefined)
104
+ base = new URL(baseHref, here).href;
105
+ }
106
+ catch { /* ignore invalid base */ }
107
+ return raw.flatMap(link => {
108
+ if (!link.href || link.href.startsWith('#'))
109
+ return [];
110
+ try {
111
+ const url = new URL(link.href, base);
112
+ if (['javascript:', 'data:', 'blob:'].includes(url.protocol))
113
+ return [];
114
+ if (sameOriginOnly && url.origin !== here.origin)
115
+ return [];
116
+ return [{ href: /^[a-z][a-z\d+.-]*:/i.test(link.href) ? link.href : url.href, text: link.text.trim().slice(0, 200) }];
117
+ }
118
+ catch {
119
+ return [];
120
+ }
121
+ });
122
+ }
53
123
  /**
54
124
  * Collapse links that share an href (keeping the first, but preferring a
55
125
  * non-empty label) when `dedupe` is set, then cap to `limit`. Order is
@@ -27,7 +27,7 @@ export declare const SERVER_INSTRUCTIONS: string;
27
27
  */
28
28
  export declare function createServer(version?: string, policy?: Policy, accessGuard?: typeof requireAccess): McpServer;
29
29
  /** Start over stdio. Idempotent. */
30
- export declare function startMcpServer(version?: string, policy?: Policy): Promise<void>;
30
+ export declare function startMcpServer(version?: string, policy?: Policy, accessGuard?: typeof requireAccess): Promise<void>;
31
31
  /** Stop and release the transport. Idempotent, best-effort. */
32
32
  export declare function stopMcpServer(): Promise<void>;
33
33
  /** The MCP client this session serves, as it named itself at initialize (for analytics). */
@@ -43,6 +43,10 @@ exports.SERVER_INSTRUCTIONS = "MCP Browser Extension drives the user's real, sig
43
43
  'toolbar icon (its popup has an Allow button). It applies at once, with no restart. You cannot approve a site yourself. ' +
44
44
  "Work in your own tab: tab_new opens one in the background, and calls without a tabId go to it, never to the tab the user is on. " +
45
45
  "To act on the user's current page, pass its tabId from tabs_list.";
46
+ const LOCAL_BYPASS_INSTRUCTIONS = 'Email verification is disabled by the local operator for this server. Only trusted local MCP clients should connect; the browser-domain allowlist and other capability safety gates remain active. ' +
47
+ 'Start with chrome_status. If it reports no paired browser (or any tool fails with NO_BACKEND), give the user the steps in its `setup` field. ' +
48
+ 'If a tool fails with POLICY_DENIED because a site is not allowed, ask the user to allow it from the extension\'s toolbar icon. ' +
49
+ "Work in your own tab: tab_new opens one in the background. To act on the user's current page, pass its tabId from tabs_list.";
46
50
  /** Default version reported when no explicit version is passed in (legacy callers/tests). */
47
51
  const DEFAULT_VERSION = SERVER_VERSION;
48
52
  let server = null;
@@ -56,7 +60,10 @@ let transport = null;
56
60
  * full catalog is advertised, which is what the tests want.
57
61
  */
58
62
  function createServer(version = DEFAULT_VERSION, policy, accessGuard = access_1.requireAccess) {
59
- const srv = new mcp_js_1.McpServer({ name: SERVER_NAME, version }, { capabilities: { tools: {}, logging: {} }, instructions: exports.SERVER_INSTRUCTIONS });
63
+ const srv = new mcp_js_1.McpServer({ name: SERVER_NAME, version }, {
64
+ capabilities: { tools: {}, logging: {} },
65
+ instructions: accessGuard === access_1.bypassEmailVerification ? LOCAL_BYPASS_INSTRUCTIONS : exports.SERVER_INSTRUCTIONS,
66
+ });
60
67
  const advertised = (0, tools_1.registerTools)(srv, policy, accessGuard);
61
68
  watchInitialize(srv, advertised);
62
69
  // `McpServer` wraps the low-level `Server`, which owns the `onerror` hook.
@@ -88,12 +95,12 @@ function watchInitialize(srv, advertised) {
88
95
  };
89
96
  }
90
97
  /** Start over stdio. Idempotent. */
91
- async function startMcpServer(version = DEFAULT_VERSION, policy) {
98
+ async function startMcpServer(version = DEFAULT_VERSION, policy, accessGuard = access_1.requireAccess) {
92
99
  if (server) {
93
100
  (0, log_1.logErr)('startMcpServer called but already running; ignoring.');
94
101
  return;
95
102
  }
96
- const srv = createServer(version, policy);
103
+ const srv = createServer(version, policy, accessGuard);
97
104
  const tx = new stdio_js_1.StdioServerTransport();
98
105
  try {
99
106
  await srv.connect(tx);
@@ -857,7 +857,7 @@ exports.TOOL_HANDLERS = {
857
857
  const res = await ctx.ex.getText((0, validators_1.optionalTarget)(a), { tabId: tabId(a), ...frameOpts(a) });
858
858
  // Save the FULL read before capping — the artifact on disk stays lossless;
859
859
  // only what crosses into the caller's context is bounded.
860
- (0, workspace_1.saveResult)('get_text', 'json', JSON.stringify(res, null, 2));
860
+ const savedTo = (0, workspace_1.saveResult)('get_text', 'json', JSON.stringify(res, null, 2));
861
861
  const clean = (0, redact_1.redactText)(res.text, redaction(ctx.policy));
862
862
  (0, audit_1.noteRedactions)(clean.redactions);
863
863
  const capped = (0, limits_1.capText)(clean.value, maxBytes(a));
@@ -865,6 +865,7 @@ exports.TOOL_HANDLERS = {
865
865
  return (0, envelopes_1.jsonResult)({
866
866
  ...res,
867
867
  text: capped.text,
868
+ ...(savedTo ? { savedTo } : {}),
868
869
  ...(0, limits_1.truncationMeta)(capped),
869
870
  ...(clean.redactions ? { redactions: clean.redactions } : {}),
870
871
  });
@@ -993,7 +994,7 @@ exports.TOOL_HANDLERS = {
993
994
  tabId: tabId(a),
994
995
  ...frameOpts(a),
995
996
  });
996
- (0, workspace_1.saveResult)('read_as_markdown', 'md', md);
997
+ const savedTo = (0, workspace_1.saveResult)('read_as_markdown', 'md', md);
997
998
  const clean = (0, redact_1.redactText)(md, redaction(ctx.policy));
998
999
  (0, audit_1.noteRedactions)(clean.redactions);
999
1000
  const capped = (0, limits_1.capText)(clean.value, maxBytes(a));
@@ -1001,7 +1002,7 @@ exports.TOOL_HANDLERS = {
1001
1002
  // Markdown is returned as plain text, so the truncation notice has to ride in
1002
1003
  // the body rather than as sibling JSON fields.
1003
1004
  return (0, envelopes_1.textResult)(capped.truncated
1004
- ? `${capped.text}\n\n[truncated: ${capped.returnedBytes} of ${capped.totalBytes} bytes — raise maxBytes or narrow with selector; the full document was saved to the task's results/ dir]`
1005
+ ? `${capped.text}\n\n[truncated: ${capped.returnedBytes} of ${capped.totalBytes} bytes — raise maxBytes or narrow with selector${savedTo ? `; full document saved to ${savedTo}` : '; full document could not be saved'}]`
1005
1006
  : capped.text);
1006
1007
  },
1007
1008
  fill_form: async (a, ctx) => {
@@ -49,6 +49,7 @@
49
49
  constructor(deps) {
50
50
  this.deps = deps;
51
51
  }
52
+ deps;
52
53
  ws = null;
53
54
  state = "idle";
54
55
  isConnected() {
@@ -1070,6 +1071,7 @@
1070
1071
  super(message);
1071
1072
  this.code = code;
1072
1073
  }
1074
+ code;
1073
1075
  };
1074
1076
  var DOWNLOAD_TIMEOUT_MS = 12e4;
1075
1077
  function waitForDownloadComplete(id) {
@@ -1480,6 +1482,7 @@
1480
1482
  constructor(getPolicy = () => null) {
1481
1483
  this.getPolicy = getPolicy;
1482
1484
  }
1485
+ getPolicy;
1483
1486
  /**
1484
1487
  * Resolve `frameId` / `allFrames` params into the frame ids an injection may
1485
1488
  * touch, dropping any whose own URL the policy does not allow for this method.
@@ -2121,6 +2124,7 @@
2121
2124
  if (!HANDLED.has(m)) throw new Error(`router drift: no handler for wire method "${m}"`);
2122
2125
  }
2123
2126
  }
2127
+ deps;
2124
2128
  async dispatch(cmd) {
2125
2129
  try {
2126
2130
  const policy = this.deps.getPolicy();
@@ -2318,6 +2322,7 @@
2318
2322
  constructor(getPolicy) {
2319
2323
  this.getPolicy = getPolicy;
2320
2324
  }
2325
+ getPolicy;
2321
2326
  /** One lock for all border state: parallel batch ops touching the same tab
2322
2327
  * must not both inject (removeCSS takes out one copy, not both). */
2323
2328
  lock = new KeyedMutex();
@@ -63,7 +63,6 @@
63
63
 
64
64
  main { padding: 4px 12px 12px; display: grid; gap: 10px; }
65
65
  .card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 12px; box-shadow: var(--shadow); }
66
- .card.connect { color: #0f172a; background: color-mix(in oklch, oklch(0.58 0.19 255) 7%, white); border-color: color-mix(in oklch, oklch(0.58 0.19 255) 25%, #e6e8ec); }
67
66
  h2 { display: flex; align-items: center; justify-content: space-between; margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
68
67
  .count { font-size: 11px; letter-spacing: 0; text-transform: none; padding: 1px 7px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); color: var(--muted); }
69
68
 
@@ -118,7 +117,7 @@
118
117
  ol { margin: 2px 0 12px; padding-left: 18px; color: var(--muted); }
119
118
  ol li { margin: 3px 0; }
120
119
  code { font: 11.5px ui-monospace, SFMono-Regular, Menlo, monospace; padding: 1px 5px; border-radius: 5px; background: var(--panel); border: 1px solid var(--line); color: var(--fg); }
121
- .connect .note { margin: 0 0 10px; color: inherit; }
120
+ .connect .note { margin: 0 0 10px; color: var(--fg); }
122
121
  #connect { width: 100%; padding: 10px 12px; font-size: 13.5px; font-weight: 600; }
123
122
  #connect.secondary { background: var(--panel); color: var(--fg); border-color: var(--line); box-shadow: none; padding: 7px 12px; font-size: 12.5px; font-weight: 500; }
124
123
  .connect .error { margin: 8px 0 0; color: var(--warn); }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "mcp-browser-extension",
3
3
  "mcpName": "io.github.Mehmoodqureshi/mcp-browser-extension",
4
- "version": "0.9.26",
5
- "description": "Drive your real Chrome browser over MCP \u2014 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.",
4
+ "version": "0.9.28",
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",
8
8
  "homepage": "https://mcp-browser-extension.com",
@@ -61,7 +61,8 @@
61
61
  "prepack": "npm run clean && npm run build && npm run build:ext"
62
62
  },
63
63
  "dependencies": {
64
- "@modelcontextprotocol/sdk": "^1.29.0",
64
+ "@modelcontextprotocol/sdk": "^1.32.1",
65
+ "htmlparser2": "^10.1.0",
65
66
  "ws": "^8.18.0",
66
67
  "zod": "^4.4.3"
67
68
  },
@@ -69,7 +70,7 @@
69
70
  "@types/chrome": "^0.0.287",
70
71
  "@types/node": "^20.17.10",
71
72
  "@types/ws": "^8.5.13",
72
- "esbuild": "^0.24.0",
73
+ "esbuild": "^0.28.2",
73
74
  "playwright": "^1.49.1",
74
75
  "rimraf": "^5.0.10",
75
76
  "typescript": "^5.7.2"