@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 +27 -0
- package/dist/src/cli.js +24 -3
- package/dist/src/config.d.ts +6 -0
- package/dist/src/config.js +32 -0
- package/dist/src/mcp/tools.d.ts +21 -0
- package/dist/src/mcp/tools.js +98 -29
- package/extension-dist/manifest.json +1 -1
- package/package.json +2 -2
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
|
|
289
|
-
//
|
|
290
|
-
const friendly = err instanceof Error &&
|
|
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
|
});
|
package/dist/src/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/config.js
CHANGED
|
@@ -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
|
package/dist/src/mcp/tools.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/mcp/tools.js
CHANGED
|
@@ -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
|
-
/**
|
|
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 (
|
|
41
|
-
ref: zod_1.z.string().describe('Element ref from a
|
|
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
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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('
|
|
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
|
-
*
|
|
62
|
-
* action needs no snapshot first
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
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: { ...
|
|
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: { ...
|
|
90
|
-
{ name: 'select_option', description: 'Select option(s) of a <select> by value or visible label.', inputSchema: { ...
|
|
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: { ...
|
|
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://
|
|
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
|
+
"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://
|
|
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"
|