@mehmoodqureshi/chrome-mcp 0.9.3 → 0.9.4

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
@@ -383,6 +383,33 @@ Renders through Chrome's own print pipeline and saves to the task's `results/`
383
383
  dir, returning the path and size. The bytes themselves are never returned — a
384
384
  PDF is megabytes of base64 no model can read.
385
385
 
386
+ ### Paying less per turn — `--tools`
387
+
388
+ Every MCP server you connect costs context before you ask it anything: the host
389
+ sends the whole tool catalog to the model on **every** turn. chrome-mcp's 39
390
+ tools are 27 KB of JSON Schema, about 6.9k tokens, on each one.
391
+
392
+ Most runs need a handful of them. `--tools` advertises only those:
393
+
394
+ ```
395
+ npx -y @mehmoodqureshi/chrome-mcp \
396
+ --allow-domain app.example.com --enable-mutations \
397
+ --tools tabs_list,tab_new,navigate,snapshot,click,type,get_text
398
+ ```
399
+
400
+ That surface is **6.0 KB, ~1.5k tokens** — an 82% cut against the full catalog,
401
+ for a run that was never going to print a PDF or upload a file.
402
+
403
+ - Comma-separated and repeatable: `--tools navigate,get_text --tools click`.
404
+ - A tool left out is hidden from `tools/list` **and refused if called** — a
405
+ `batch` op naming it fails the same way an unknown tool does. Hiding a tool is
406
+ a real restriction, not a display filter. It is not a substitute for the
407
+ policy gate, though: `--tools eval` still does nothing without
408
+ `--unsafe-enable-eval`.
409
+ - An unknown name fails at startup and prints the catalog, so a typo can never
410
+ quietly drop `click` from the surface.
411
+ - `chrome-mcp --help` prints the full catalog of 39 names to pick from.
412
+
386
413
  ## Status
387
414
 
388
415
  v0.5.0 — **safe multi-tab concurrency.** Adds the `batch` fan-out tool, makes
package/dist/src/cli.js CHANGED
@@ -21,6 +21,7 @@ const datadir_1 = require("./bridge/datadir");
21
21
  const workspace_1 = require("./bridge/workspace");
22
22
  const auth_1 = require("./bridge/auth");
23
23
  const server_2 = require("./mcp/server");
24
+ const tools_1 = require("./mcp/tools");
24
25
  const extension_install_1 = require("./extension-install");
25
26
  /** Hard deadline for clean shutdown before we force-exit (a stuck socket must not hang us). */
26
27
  const SHUTDOWN_DEADLINE_MS = 3000;
@@ -158,6 +159,21 @@ function runTasksCommand(argv) {
158
159
  process.stdout.write(TASKS_HELP);
159
160
  return true;
160
161
  }
162
+ /** The tool names, wrapped into indented lines for `--help`. */
163
+ function toolList() {
164
+ const lines = [];
165
+ let line = ' ';
166
+ for (const name of tools_1.TOOL_NAMES) {
167
+ if (line.length + name.length + 2 > 78) {
168
+ lines.push(line);
169
+ line = ' ';
170
+ }
171
+ line += ` ${name}`;
172
+ }
173
+ if (line.trim())
174
+ lines.push(line);
175
+ return lines.join('\n');
176
+ }
161
177
  async function main() {
162
178
  if (runTasksCommand(process.argv.slice(2)))
163
179
  return;
@@ -168,6 +184,8 @@ async function main() {
168
184
  (0, server_2.setLogLevel)(cfg.logLevel);
169
185
  if (cfg.showHelp) {
170
186
  process.stdout.write(config_1.HELP_TEXT);
187
+ // The catalog is what `--tools` takes, so `--help` has to name it.
188
+ process.stdout.write(`\nTools (${tools_1.TOOL_NAMES.length}) — any of these for --tools:\n${toolList()}\n`);
171
189
  return;
172
190
  }
173
191
  if (cfg.showVersion) {
@@ -178,6 +196,9 @@ async function main() {
178
196
  process.stdout.write(`${installExtension()}\n`);
179
197
  return;
180
198
  }
199
+ // Before the bridge binds a port or writes a handshake: a typo'd `--tools`
200
+ // name should fail as a plain startup error, not leave a half-started server.
201
+ (0, tools_1.setToolAllowlist)(cfg.tools);
181
202
  const dataDir = (0, datadir_1.ensureDataDir)(cfg.dataDir);
182
203
  const token = (0, auth_1.resolveToken)(dataDir, { persist: cfg.persistToken });
183
204
  const { allowDomains, allowEval, allowDownloads, allowUploads, allowAllTabs, enableMutations } = cfg.policy;
@@ -285,9 +306,9 @@ async function main() {
285
306
  await (0, server_2.startMcpServer)(version());
286
307
  }
287
308
  main().catch((err) => {
288
- // Port-busy and similar startup failures carry a plain-English message already;
289
- // show that to the user without a noisy stack trace.
290
- const friendly = err instanceof Error && /Couldn't start:/.test(err.message);
309
+ // Port-busy and bad-flag failures carry a plain-English message already
310
+ // (a flag error starts with the flag); show it without a noisy stack trace.
311
+ const friendly = err instanceof Error && /^(Couldn't start:|--|unknown argument:)/.test(err.message);
291
312
  (0, server_2.logErr)(friendly ? err.message : `fatal: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
292
313
  process.exit(1);
293
314
  });
@@ -34,6 +34,12 @@ export interface CliConfig {
34
34
  showExtensionPath: boolean;
35
35
  /** `--persist-token`: reuse a stable on-disk token so the extension never re-pairs. */
36
36
  persistToken: boolean;
37
+ /**
38
+ * `--tools`: advertise only these tools. `undefined` = the whole catalog.
39
+ * Names are validated against the catalog by `setToolAllowlist` at startup —
40
+ * config.ts deliberately does not import the tool surface.
41
+ */
42
+ tools?: string[];
37
43
  showHelp: boolean;
38
44
  showVersion: boolean;
39
45
  logLevel: LogLevel;
@@ -82,6 +82,10 @@ function parseArgs(argv) {
82
82
  let showVersion = false;
83
83
  let showExtensionPath = false;
84
84
  let logLevel = 'info';
85
+ // Insertion-ordered so `--tools` keeps the order the operator wrote, and a
86
+ // name repeated across two flags is listed once.
87
+ const tools = new Set();
88
+ let toolsFlagSeen = false;
85
89
  // Policy assembled from flags, layered over an optional file.
86
90
  let policyFile;
87
91
  const policyFlags = {};
@@ -174,6 +178,11 @@ function parseArgs(argv) {
174
178
  case '--persist-token':
175
179
  persistToken = true;
176
180
  break;
181
+ case '--tools':
182
+ toolsFlagSeen = true;
183
+ for (const name of splitList(requireValue(argv[++i], '--tools')))
184
+ tools.add(name);
185
+ break;
177
186
  case '--log-level':
178
187
  logLevel = requireLogLevel(argv[++i]);
179
188
  break;
@@ -196,6 +205,12 @@ function parseArgs(argv) {
196
205
  if (policy.allowUploads && !policy.uploadsDir) {
197
206
  throw new Error('--enable-uploads requires --uploads-dir <path> (uploads must be confined to a directory)');
198
207
  }
208
+ // `--tools ""` (or `--tools ,,`) means the operator asked for a restriction and
209
+ // got none — an empty surface is never what they wanted, and silently serving
210
+ // all 39 tools is the opposite of what they asked for.
211
+ if (toolsFlagSeen && tools.size === 0) {
212
+ throw new Error('--tools requires at least one tool name (comma-separated, e.g. --tools navigate,get_text)');
213
+ }
199
214
  // Fail at startup, not on the first read, if a redaction pattern is malformed:
200
215
  // a pattern the user believes is scrubbing secrets but that never compiled is
201
216
  // the worst of both worlds.
@@ -223,6 +238,7 @@ function parseArgs(argv) {
223
238
  showVersion,
224
239
  showExtensionPath,
225
240
  logLevel,
241
+ tools: tools.size > 0 ? [...tools] : undefined,
226
242
  };
227
243
  }
228
244
  // ---------------------------------------------------------------------------
@@ -248,6 +264,13 @@ function requireInt(value, flag) {
248
264
  throw new Error(`${flag} must be a non-negative integer`);
249
265
  return n;
250
266
  }
267
+ /** Split a repeatable comma/space-separated list flag, dropping empty entries. */
268
+ function splitList(value) {
269
+ return value
270
+ .split(/[,\s]+/)
271
+ .map((part) => part.trim())
272
+ .filter((part) => part.length > 0);
273
+ }
251
274
  function requirePreference(value) {
252
275
  if (value === 'extension' || value === 'cdp')
253
276
  return value;
@@ -318,6 +341,15 @@ Security (default: deny-all safe mode):
318
341
  high-confidence sign-in wall (expired session), so an
319
342
  eval harness never scores it as some other failure.
320
343
 
344
+ Tool surface:
345
+ --tools <list> Advertise ONLY these tools (comma-separated, repeatable).
346
+ Everything else is hidden from tools/list and refused if
347
+ called — including from inside a batch op. The catalog is
348
+ re-sent to the model on every turn, so trimming it to the
349
+ tools a run actually needs is the cheapest context saving
350
+ there is. Unknown names fail at startup.
351
+ e.g. --tools tabs_list,navigate,get_text,click,type
352
+
321
353
  Misc:
322
354
  --log-level <lvl> silent | info | debug (default info)
323
355
  -h, --help Show this help
@@ -22,6 +22,27 @@ export interface ToolDefinition {
22
22
  inputSchema: z.ZodRawShape;
23
23
  }
24
24
  export declare const TOOL_DEFINITIONS: ToolDefinition[];
25
+ /** Every advertised tool name, in catalog order. */
26
+ export declare const TOOL_NAMES: readonly string[];
27
+ /**
28
+ * Restrict the tool surface to `names` (`--tools`). The catalog is the single
29
+ * largest fixed cost of having this server connected: every tool's JSON Schema
30
+ * is re-sent to the model on EVERY turn. A run that only reads pages has no use
31
+ * for uploads, PDFs or task management, and should not pay for their schemas.
32
+ *
33
+ * Excluded tools are neither advertised in `tools/list` nor callable — a
34
+ * `batch` op naming one is refused exactly like an unknown tool, so hiding a
35
+ * tool is a real restriction and not just a display filter.
36
+ *
37
+ * Passing `null`/`undefined`/`[]` restores the full catalog. Unknown names
38
+ * throw rather than being ignored: a typo that silently drops `click` from the
39
+ * surface is far more expensive to debug than a startup error.
40
+ */
41
+ export declare function setToolAllowlist(names: readonly string[] | null | undefined): void;
42
+ /** Is `name` on the surface? True for every catalog tool when no allowlist is set. */
43
+ export declare function isToolEnabled(name: string): boolean;
44
+ /** The names actually advertised, in catalog order. */
45
+ export declare function enabledToolNames(): readonly string[];
25
46
  interface ToolCtx {
26
47
  ex: Executor;
27
48
  policy: Policy;
@@ -12,7 +12,10 @@
12
12
  * `Error` as an `isError` result.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.TOOL_HANDLERS = exports.TOOL_DEFINITIONS = void 0;
15
+ exports.TOOL_HANDLERS = exports.TOOL_NAMES = exports.TOOL_DEFINITIONS = void 0;
16
+ exports.setToolAllowlist = setToolAllowlist;
17
+ exports.isToolEnabled = isToolEnabled;
18
+ exports.enabledToolNames = enabledToolNames;
16
19
  exports.resetRateLimiter = resetRateLimiter;
17
20
  exports.dispatchToolCall = dispatchToolCall;
18
21
  exports.assertNoDrift = assertNoDrift;
@@ -35,45 +38,59 @@ const log_1 = require("./log");
35
38
  const tasks_1 = require("../bridge/tasks");
36
39
  const workspace_1 = require("../bridge/workspace");
37
40
  const validators_1 = require("./validators");
38
- /** Shared selector|ref target — both optional; a handler that needs one calls `requireTarget`. */
41
+ /**
42
+ * Element targeting, defined ONCE and spread whole by every tool that acts on
43
+ * an element. `TARGET_PROPS` is the plain selector|ref pair (for tools whose
44
+ * handler resolves a node and nothing else); `LOCATOR_PROPS` is the full block
45
+ * — selector | ref | role+name, in any frame — and is what click/type/hover/
46
+ * select_option spread as a single object.
47
+ *
48
+ * Keeping the block in one place is also what keeps `tools/list` cheap: these
49
+ * seven fields repeat across a dozen tools, so every byte of prose here is paid
50
+ * a dozen times on EVERY turn. Detail an agent does not need on every call
51
+ * belongs in the owning tool's description (`frames_list`, `auth_check`) or the
52
+ * README, not here.
53
+ *
54
+ * A handler that needs a target calls `requireTarget`; ambiguity fails loudly
55
+ * rather than acting on the wrong element.
56
+ */
39
57
  const TARGET_PROPS = {
40
- selector: zod_1.z.string().describe('CSS selector (exactly one of selector|ref)').optional(),
41
- ref: zod_1.z.string().describe('Element ref from a prior read (exactly one of selector|ref)').optional(),
58
+ selector: zod_1.z.string().describe('CSS selector (or pass ref)').optional(),
59
+ ref: zod_1.z.string().describe('Element ref from a snapshot').optional(),
42
60
  };
43
- const tabIdField = zod_1.z.string().describe('Target tab id (defaults to the active tab)').optional();
44
- const authWallField = zod_1.z.boolean().describe('Fail with [AUTH_REQUIRED] when the page this call lands on is a high-confidence sign-in wall (session expired). Off by default unless the server runs with --fail-on-auth-wall; snapshot still reports the verdict as `authWall` either way.').optional();
45
61
  /**
46
- * Frame targeting. Omitted = the top frame, which is what every call did before
47
- * frames were addressable. `allFrames` is the one to reach for when a selector
48
- * "should" match but does not: the element is almost always inside an iframe
49
- * (checkout widgets, OAuth consent, embedded editors). Every frame is
50
- * authorized against its own URL, so a scan never reaches a site the allowlist
51
- * does not cover.
62
+ * Frame targeting. Omitted = the top frame. `allFrames` is the one to reach for
63
+ * when a selector "should" match but does not — the element is almost always
64
+ * inside an iframe. Every frame is authorized against its own URL, so a scan
65
+ * never reaches a site the allowlist does not cover.
52
66
  */
53
67
  const FRAME_PROPS = {
54
- frameId: zod_1.z.number().describe('Act inside this frame (ids come from frames_list)').optional(),
55
- allFrames: zod_1.z
56
- .boolean()
57
- .describe('Search every frame of the tab and act on the first that matches - use when a selector should match but does not (the element is in an iframe)')
58
- .optional(),
68
+ frameId: zod_1.z.number().describe('Frame id from frames_list').optional(),
69
+ allFrames: zod_1.z.boolean().describe('Act on the first match in ANY frame (the element may be in an iframe)').optional(),
59
70
  };
60
71
  /**
61
- * Target an element by role + accessible name instead of a CSS selector, so an
62
- * action needs no snapshot first. Resolution is server-side and fails loudly on
63
- * ambiguity rather than acting on the wrong element.
72
+ * The full locator block. role+name targets an element the way a person reads
73
+ * the page, so an action needs no snapshot first; resolution is server-side.
64
74
  */
65
75
  const LOCATOR_PROPS = {
66
- role: zod_1.z.string().describe('Target by ARIA role (e.g. button, link, textbox) - alternative to selector/ref').optional(),
67
- name: zod_1.z.string().describe('Target by accessible name/visible label (pairs with role)').optional(),
68
- nth: zod_1.z.number().describe('Pick the nth (0-based) match when a role+name locator is legitimately ambiguous').optional(),
76
+ ...TARGET_PROPS,
77
+ // These tools take role+name too, so their `selector` says so; the tools that
78
+ // only resolve a node keep the plain TARGET_PROPS wording.
79
+ selector: zod_1.z.string().describe('CSS selector (or ref, or role+name)').optional(),
80
+ role: zod_1.z.string().describe('ARIA role, e.g. button|link|textbox (pair with name)').optional(),
81
+ name: zod_1.z.string().describe('Accessible name / visible label (pair with role)').optional(),
82
+ nth: zod_1.z.number().describe('0-based index when role+name is ambiguous').optional(),
83
+ ...FRAME_PROPS,
69
84
  };
85
+ const tabIdField = zod_1.z.string().describe('Tab id (default: active tab)').optional();
86
+ const authWallField = zod_1.z.boolean().describe('Error with [AUTH_REQUIRED] if this lands on a sign-in wall (expired session)').optional();
70
87
  const snapshotAfterField = zod_1.z
71
88
  .boolean()
72
- .describe('Return what CHANGED on the page after this action (added/removed/changed elements vs the last snapshot) instead of making you re-read the page')
89
+ .describe('Return what CHANGED on the page after this action instead of making you re-read it')
73
90
  .optional();
74
91
  const maxBytesField = zod_1.z
75
92
  .number()
76
- .describe(`Cap the returned content at this many UTF-8 bytes (default ${limits_1.DEFAULT_MAX_OUTPUT_BYTES}). A truncated result reports truncated/totalBytes/returnedBytes. The full payload is still written to the task's results/ dir.`)
93
+ .describe(`Cap returned content at N UTF-8 bytes (default ${limits_1.DEFAULT_MAX_OUTPUT_BYTES}); the full payload still lands in results/`)
77
94
  .optional();
78
95
  const waitUntilField = zod_1.z.enum(['load', 'domcontentloaded', 'networkidle']).describe('When to consider navigation done').optional();
79
96
  exports.TOOL_DEFINITIONS = [
@@ -85,11 +102,11 @@ exports.TOOL_DEFINITIONS = [
85
102
  { name: 'back', description: 'Go back in history.', inputSchema: { tabId: tabIdField, failOnAuthWall: authWallField } },
86
103
  { name: 'forward', description: 'Go forward in history.', inputSchema: { tabId: tabIdField, failOnAuthWall: authWallField } },
87
104
  { name: 'reload', description: 'Reload the active (or given) tab.', inputSchema: { tabId: tabIdField, waitUntil: waitUntilField, failOnAuthWall: authWallField } },
88
- { name: 'click', description: 'Click an element. Target by selector, a snapshot ref, or role+name (e.g. role:"button", name:"Sign in") - the locator needs no snapshot first. trusted=true uses real OS-level input.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, tabId: tabIdField, button: zod_1.z.enum(['left', 'right', 'middle']).optional(), clickCount: zod_1.z.number().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
89
- { name: 'type', description: 'Type text into an element (target by selector, ref, or role+name). trusted=true sends real keystrokes (works on React/Vue controlled inputs).', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, text: zod_1.z.string(), tabId: tabIdField, clear: zod_1.z.boolean().optional(), pressEnter: zod_1.z.boolean().optional(), keyEvents: zod_1.z.boolean().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
90
- { name: 'select_option', description: 'Select option(s) of a <select> by value or visible label.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, values: zod_1.z.array(zod_1.z.string()), tabId: tabIdField, snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
105
+ { name: 'click', description: 'Click an element. Target by selector, a snapshot ref, or role+name (e.g. role:"button", name:"Sign in") - the locator needs no snapshot first. trusted=true uses real OS-level input.', inputSchema: { ...LOCATOR_PROPS, tabId: tabIdField, button: zod_1.z.enum(['left', 'right', 'middle']).optional(), clickCount: zod_1.z.number().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
106
+ { name: 'type', description: 'Type text into an element (target by selector, ref, or role+name). trusted=true sends real keystrokes (works on React/Vue controlled inputs).', inputSchema: { ...LOCATOR_PROPS, text: zod_1.z.string(), tabId: tabIdField, clear: zod_1.z.boolean().optional(), pressEnter: zod_1.z.boolean().optional(), keyEvents: zod_1.z.boolean().optional(), trusted: zod_1.z.boolean().optional(), snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
107
+ { name: 'select_option', description: 'Select option(s) of a <select> by value or visible label.', inputSchema: { ...LOCATOR_PROPS, values: zod_1.z.array(zod_1.z.string()), tabId: tabIdField, snapshotAfter: snapshotAfterField, failOnAuthWall: authWallField } },
91
108
  { name: 'press', description: 'Press a key (with optional modifiers).', inputSchema: { key: zod_1.z.string(), modifiers: zod_1.z.array(zod_1.z.string()).optional(), tabId: tabIdField, failOnAuthWall: authWallField } },
92
- { name: 'hover', description: 'Hover over an element.', inputSchema: { ...TARGET_PROPS, ...LOCATOR_PROPS, ...FRAME_PROPS, tabId: tabIdField, snapshotAfter: snapshotAfterField } },
109
+ { name: 'hover', description: 'Hover over an element.', inputSchema: { ...LOCATOR_PROPS, tabId: tabIdField, snapshotAfter: snapshotAfterField } },
93
110
  { name: 'scroll', description: 'Scroll the page or to an element.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, x: zod_1.z.number().optional(), y: zod_1.z.number().optional(), deltaX: zod_1.z.number().optional(), deltaY: zod_1.z.number().optional(), tabId: tabIdField } },
94
111
  { name: 'screenshot', description: 'Capture a screenshot (page or element). Default is JPEG (quality 70) at CSS-pixel size, which is several times smaller than PNG and reads fine. Pass format:"png" for lossless, quality 1-100 for JPEG, scale 2 for device pixels on a Retina display or 0.5 to shrink.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, fullPage: zod_1.z.boolean().optional(), format: zod_1.z.enum(['jpeg', 'png']).optional(), quality: zod_1.z.number().optional(), scale: zod_1.z.number().optional(), tabId: tabIdField } },
95
112
  { name: 'get_text', description: 'Get visible text of the page or an element.', inputSchema: { ...TARGET_PROPS, ...FRAME_PROPS, tabId: tabIdField, maxBytes: maxBytesField } },
@@ -182,6 +199,48 @@ exports.TOOL_DEFINITIONS = [
182
199
  },
183
200
  },
184
201
  ];
202
+ // ---------------------------------------------------------------------------
203
+ // Tool allowlist (--tools)
204
+ // ---------------------------------------------------------------------------
205
+ /** Every advertised tool name, in catalog order. */
206
+ exports.TOOL_NAMES = exports.TOOL_DEFINITIONS.map((d) => d.name);
207
+ /** `null` = the whole catalog (the default). */
208
+ let toolAllowlist = null;
209
+ /**
210
+ * Restrict the tool surface to `names` (`--tools`). The catalog is the single
211
+ * largest fixed cost of having this server connected: every tool's JSON Schema
212
+ * is re-sent to the model on EVERY turn. A run that only reads pages has no use
213
+ * for uploads, PDFs or task management, and should not pay for their schemas.
214
+ *
215
+ * Excluded tools are neither advertised in `tools/list` nor callable — a
216
+ * `batch` op naming one is refused exactly like an unknown tool, so hiding a
217
+ * tool is a real restriction and not just a display filter.
218
+ *
219
+ * Passing `null`/`undefined`/`[]` restores the full catalog. Unknown names
220
+ * throw rather than being ignored: a typo that silently drops `click` from the
221
+ * surface is far more expensive to debug than a startup error.
222
+ */
223
+ function setToolAllowlist(names) {
224
+ if (!names || names.length === 0) {
225
+ toolAllowlist = null;
226
+ return;
227
+ }
228
+ const known = new Set(exports.TOOL_NAMES);
229
+ const unknown = names.filter((n) => !known.has(n));
230
+ if (unknown.length > 0) {
231
+ throw new Error(`--tools: unknown tool ${unknown.map((n) => JSON.stringify(n)).join(', ')}. ` +
232
+ `Known tools: ${exports.TOOL_NAMES.join(', ')}`);
233
+ }
234
+ toolAllowlist = new Set(names);
235
+ }
236
+ /** Is `name` on the surface? True for every catalog tool when no allowlist is set. */
237
+ function isToolEnabled(name) {
238
+ return toolAllowlist === null || toolAllowlist.has(name);
239
+ }
240
+ /** The names actually advertised, in catalog order. */
241
+ function enabledToolNames() {
242
+ return exports.TOOL_NAMES.filter(isToolEnabled);
243
+ }
185
244
  const GATE_CONTEXT = 'cannot resolve the target tab URL for the policy gate';
186
245
  /**
187
246
  * Resolve the URL the policy should be evaluated against: the URL of the tab
@@ -984,6 +1043,10 @@ function isRetryableFault(name, err) {
984
1043
  return err instanceof types_1.ExecutorError && err.code === 'EXTENSION_DISCONNECTED' && RETRY_SAFE_TOOLS.has(name);
985
1044
  }
986
1045
  async function dispatchToolCall(name, rawArgs) {
1046
+ // The allowlist is checked here, not only at registration, so a `batch` op
1047
+ // cannot reach a tool the operator kept off the surface.
1048
+ if (!isToolEnabled(name))
1049
+ return (0, envelopes_1.errorResult)(`tool not enabled on this server (--tools): ${name}`);
987
1050
  const handler = exports.TOOL_HANDLERS[name];
988
1051
  if (!handler)
989
1052
  return (0, envelopes_1.errorResult)(`unknown tool: ${name}`);
@@ -1046,7 +1109,13 @@ function registerTools(server) {
1046
1109
  // just routes back through `dispatchToolCall` — our never-throw firewall that
1047
1110
  // applies the rate limit, executor readiness, policy gate, and history log.
1048
1111
  for (const d of exports.TOOL_DEFINITIONS) {
1112
+ if (!isToolEnabled(d.name))
1113
+ continue;
1049
1114
  server.registerTool(d.name, { description: d.description, inputSchema: d.inputSchema }, async (args) => dispatchToolCall(d.name, args));
1050
1115
  }
1116
+ const advertised = enabledToolNames();
1117
+ if (advertised.length < exports.TOOL_NAMES.length) {
1118
+ (0, log_1.logErr)(`--tools: advertising ${advertised.length} of ${exports.TOOL_NAMES.length} tools (${advertised.join(', ')})`);
1119
+ }
1051
1120
  }
1052
1121
  //# sourceMappingURL=tools.js.map
@@ -3,7 +3,7 @@
3
3
  "name": "MCP Extension for Chrome",
4
4
  "version": "0.9.3",
5
5
  "description": "Lets a local chrome-mcp server drive this browser. Pair it with the server's handshake token.",
6
- "homepage_url": "https://github.com/Mehmoodqureshi/chrome-mcp",
6
+ "homepage_url": "https://chrome-mcp-omega.vercel.app",
7
7
  "minimum_chrome_version": "116",
8
8
  "icons": {
9
9
  "16": "icon16.png",
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@mehmoodqureshi/chrome-mcp",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "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.",
5
5
  "author": "Mehmood Ur Rehman Qureshi",
6
6
  "license": "MIT",
7
- "homepage": "https://github.com/Mehmoodqureshi/chrome-mcp#readme",
7
+ "homepage": "https://chrome-mcp-omega.vercel.app",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/Mehmoodqureshi/chrome-mcp.git"