@mehmoodqureshi/chrome-mcp 0.9.9 → 0.9.11
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/dist/src/cli.js +3 -1
- package/dist/src/mcp/server.d.ts +11 -3
- package/dist/src/mcp/server.js +12 -5
- package/dist/src/mcp/tools.d.ts +20 -1
- package/dist/src/mcp/tools.js +161 -4
- package/package.json +2 -1
package/dist/src/cli.js
CHANGED
|
@@ -329,7 +329,9 @@ async function main() {
|
|
|
329
329
|
// the next connection. This is what makes reconnects "just work".
|
|
330
330
|
process.stdin.on('end', shutdown);
|
|
331
331
|
process.stdin.on('close', shutdown);
|
|
332
|
-
|
|
332
|
+
// The resolved policy shapes the advertised catalog: a capability that is off
|
|
333
|
+
// means its tools are never described to the model.
|
|
334
|
+
await (0, server_2.startMcpServer)(version(), cfg.policy);
|
|
333
335
|
}
|
|
334
336
|
main().catch((err) => {
|
|
335
337
|
// Port-busy and bad-flag failures carry a plain-English message already
|
package/dist/src/mcp/server.d.ts
CHANGED
|
@@ -7,11 +7,19 @@
|
|
|
7
7
|
* go to stderr via `logErr`.
|
|
8
8
|
*/
|
|
9
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import type { Policy } from '../security/policy';
|
|
10
11
|
export { getLogLevel, logDebug, logErr, setLogLevel } from './log';
|
|
11
|
-
/**
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Build a fresh `Server` with the tool surface registered (no transport).
|
|
14
|
+
*
|
|
15
|
+
* `policy` is optional: pass it and tools whose capability the policy has
|
|
16
|
+
* switched off are left out of the catalog (they could only answer
|
|
17
|
+
* POLICY_DENIED, and the catalog is re-sent on every turn). Omit it and the
|
|
18
|
+
* full catalog is advertised, which is what the tests want.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createServer(version?: string, policy?: Policy): McpServer;
|
|
13
21
|
/** Start over stdio. Idempotent. */
|
|
14
|
-
export declare function startMcpServer(version?: string): Promise<void>;
|
|
22
|
+
export declare function startMcpServer(version?: string, policy?: Policy): Promise<void>;
|
|
15
23
|
/** Stop and release the transport. Idempotent, best-effort. */
|
|
16
24
|
export declare function stopMcpServer(): Promise<void>;
|
|
17
25
|
export declare function isMcpServerRunning(): boolean;
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -30,10 +30,17 @@ const SERVER_VERSION = '0.1.0';
|
|
|
30
30
|
const DEFAULT_VERSION = SERVER_VERSION;
|
|
31
31
|
let server = null;
|
|
32
32
|
let transport = null;
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Build a fresh `Server` with the tool surface registered (no transport).
|
|
35
|
+
*
|
|
36
|
+
* `policy` is optional: pass it and tools whose capability the policy has
|
|
37
|
+
* switched off are left out of the catalog (they could only answer
|
|
38
|
+
* POLICY_DENIED, and the catalog is re-sent on every turn). Omit it and the
|
|
39
|
+
* full catalog is advertised, which is what the tests want.
|
|
40
|
+
*/
|
|
41
|
+
function createServer(version = DEFAULT_VERSION, policy) {
|
|
35
42
|
const srv = new mcp_js_1.McpServer({ name: SERVER_NAME, version }, { capabilities: { tools: {} } });
|
|
36
|
-
(0, tools_1.registerTools)(srv);
|
|
43
|
+
(0, tools_1.registerTools)(srv, policy);
|
|
37
44
|
// `McpServer` wraps the low-level `Server`, which owns the `onerror` hook.
|
|
38
45
|
srv.server.onerror = (err) => {
|
|
39
46
|
(0, log_1.logErr)(`server error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
@@ -41,12 +48,12 @@ function createServer(version = DEFAULT_VERSION) {
|
|
|
41
48
|
return srv;
|
|
42
49
|
}
|
|
43
50
|
/** Start over stdio. Idempotent. */
|
|
44
|
-
async function startMcpServer(version = DEFAULT_VERSION) {
|
|
51
|
+
async function startMcpServer(version = DEFAULT_VERSION, policy) {
|
|
45
52
|
if (server) {
|
|
46
53
|
(0, log_1.logErr)('startMcpServer called but already running; ignoring.');
|
|
47
54
|
return;
|
|
48
55
|
}
|
|
49
|
-
const srv = createServer(version);
|
|
56
|
+
const srv = createServer(version, policy);
|
|
50
57
|
const tx = new stdio_js_1.StdioServerTransport();
|
|
51
58
|
try {
|
|
52
59
|
await srv.connect(tx);
|
package/dist/src/mcp/tools.d.ts
CHANGED
|
@@ -42,6 +42,25 @@ export declare const TOOL_NAMES: readonly string[];
|
|
|
42
42
|
export declare function setToolAllowlist(names: readonly string[] | null | undefined): void;
|
|
43
43
|
/** Is `name` on the surface? True for every catalog tool when no allowlist is set. */
|
|
44
44
|
export declare function isToolEnabled(name: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Is `name` usable at all under `policy`? Undefined policy = advertise
|
|
47
|
+
* everything (tests and any caller that builds a server without one).
|
|
48
|
+
*/
|
|
49
|
+
export declare function isToolPolicyUsable(name: string, policy?: Policy): boolean;
|
|
50
|
+
export interface DisabledCapability {
|
|
51
|
+
capability: string;
|
|
52
|
+
flag: string;
|
|
53
|
+
hiddenTools: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Which capabilities the policy has switched off, the flag for each, and the
|
|
57
|
+
* tools that left the catalog because of it. Surfaced by `chrome_status`: once
|
|
58
|
+
* `navigate` is hidden the model cannot learn about it from a POLICY_DENIED
|
|
59
|
+
* any more, so without this it would tell the user it "can't browse" instead
|
|
60
|
+
* of naming the one flag that fixes it. Tools already cut by `--tools` are not
|
|
61
|
+
* listed, since the flag would not bring them back.
|
|
62
|
+
*/
|
|
63
|
+
export declare function disabledCapabilities(policy: Policy): DisabledCapability[];
|
|
45
64
|
/** The names actually advertised, in catalog order. */
|
|
46
65
|
export declare function enabledToolNames(): readonly string[];
|
|
47
66
|
interface ToolCtx {
|
|
@@ -56,5 +75,5 @@ export declare function resetRateLimiter(): void;
|
|
|
56
75
|
export declare function dispatchToolCall(name: string, rawArgs: unknown): Promise<CallToolResult>;
|
|
57
76
|
/** Assert the catalog and the dispatch table describe the same tool set. */
|
|
58
77
|
export declare function assertNoDrift(): void;
|
|
59
|
-
export declare function registerTools(server: McpServer): void;
|
|
78
|
+
export declare function registerTools(server: McpServer, policy?: Policy): void;
|
|
60
79
|
export {};
|
package/dist/src/mcp/tools.js
CHANGED
|
@@ -15,6 +15,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.TOOL_HANDLERS = exports.TOOL_NAMES = exports.TOOL_DEFINITIONS = void 0;
|
|
16
16
|
exports.setToolAllowlist = setToolAllowlist;
|
|
17
17
|
exports.isToolEnabled = isToolEnabled;
|
|
18
|
+
exports.isToolPolicyUsable = isToolPolicyUsable;
|
|
19
|
+
exports.disabledCapabilities = disabledCapabilities;
|
|
18
20
|
exports.enabledToolNames = enabledToolNames;
|
|
19
21
|
exports.setProfileBridge = setProfileBridge;
|
|
20
22
|
exports.resetRateLimiter = resetRateLimiter;
|
|
@@ -27,6 +29,7 @@ const zod_1 = require("zod");
|
|
|
27
29
|
const types_1 = require("../executor/types");
|
|
28
30
|
const manager_1 = require("../executor/manager");
|
|
29
31
|
const policy_1 = require("../security/policy");
|
|
32
|
+
const policy_2 = require("../../shared/policy");
|
|
30
33
|
const envelopes_1 = require("./envelopes");
|
|
31
34
|
const limits_1 = require("./limits");
|
|
32
35
|
const batch_1 = require("./batch");
|
|
@@ -183,7 +186,7 @@ exports.TOOL_DEFINITIONS = [
|
|
|
183
186
|
tabId: tabIdField,
|
|
184
187
|
},
|
|
185
188
|
},
|
|
186
|
-
{ name: 'chrome_status', description: 'Report backend/session status
|
|
189
|
+
{ name: 'chrome_status', description: 'Report backend/session status: paired browser profiles and how each was named, and flags for switched-off capabilities.', inputSchema: {} },
|
|
187
190
|
{ name: 'auth_check', description: 'Is the tab sitting on a sign-in wall? Reads the page (URL, title, password fields, sign-in controls) and returns { authRequired, confidence, signals }. Use it after a navigate, or whenever a step fails unexpectedly, to tell "the session expired" apart from "the agent got lost". Pass failOnAuthWall:true to get an [AUTH_REQUIRED] error instead of a verdict, so a harness can bucket the run as an auth failure.', inputSchema: { failOnAuthWall: authWallField, ...FRAME_PROPS, tabId: tabIdField } },
|
|
188
191
|
{ name: 'profile_use', description: 'Switch the active browser profile (identity). Subsequent downloads, results, screenshots, and the action log are stored under profiles/<name>/. Resets the active task to "default" unless you then call task_new.', inputSchema: { name: zod_1.z.string().describe('Profile name (becomes a folder; sanitized to a safe path segment).') } },
|
|
189
192
|
{ name: 'profile_rename', description: 'Rename an automatically named browser profile (e.g. "profile-2" -> "work"). The name sticks across restarts; its saved artifacts move with it.', inputSchema: { from: zod_1.z.string().describe('Current profile name'), to: zod_1.z.string().describe('New profile name') } },
|
|
@@ -242,6 +245,141 @@ function setToolAllowlist(names) {
|
|
|
242
245
|
function isToolEnabled(name) {
|
|
243
246
|
return toolAllowlist === null || toolAllowlist.has(name);
|
|
244
247
|
}
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Policy-dead tools (capability gates)
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
/**
|
|
252
|
+
* The wire method each tool is gated on, mirroring the `gate(ctx, ...)` call in
|
|
253
|
+
* its handler. `null` = the tool touches no page and no capability (tab listing,
|
|
254
|
+
* status, task bookkeeping, `batch` itself), so no policy can disable it.
|
|
255
|
+
*
|
|
256
|
+
* This exists so the catalog can drop tools the policy has switched off. The
|
|
257
|
+
* catalog is re-sent to the model on EVERY turn, and a tool whose capability is
|
|
258
|
+
* off can only ever answer POLICY_DENIED — so describing it is pure waste, and
|
|
259
|
+
* it also invites the model to spend a call discovering that.
|
|
260
|
+
*
|
|
261
|
+
* Kept in step with the handlers by `assertNoDrift`, which fails startup if a
|
|
262
|
+
* tool is missing an entry.
|
|
263
|
+
*/
|
|
264
|
+
const TOOL_GATE = {
|
|
265
|
+
// no gate — always usable
|
|
266
|
+
tabs_list: null,
|
|
267
|
+
chrome_status: null,
|
|
268
|
+
batch: null,
|
|
269
|
+
profile_use: null,
|
|
270
|
+
profile_rename: null,
|
|
271
|
+
task_new: null,
|
|
272
|
+
task_status: null,
|
|
273
|
+
tasks_list: null,
|
|
274
|
+
storage: null,
|
|
275
|
+
// tab management (mutation-gated)
|
|
276
|
+
tab_select: 'tab_select',
|
|
277
|
+
tab_new: 'tab_new',
|
|
278
|
+
tab_close: 'tab_close',
|
|
279
|
+
// navigation (mutation-gated)
|
|
280
|
+
navigate: 'navigate',
|
|
281
|
+
back: 'back',
|
|
282
|
+
forward: 'forward',
|
|
283
|
+
reload: 'reload',
|
|
284
|
+
// interaction (mutation-gated)
|
|
285
|
+
click: 'click',
|
|
286
|
+
type: 'type',
|
|
287
|
+
select_option: 'type',
|
|
288
|
+
press: 'press',
|
|
289
|
+
hover: 'hover',
|
|
290
|
+
scroll: 'scroll',
|
|
291
|
+
fill_form: 'type',
|
|
292
|
+
// reads (domain-gated only)
|
|
293
|
+
screenshot: 'screenshot',
|
|
294
|
+
get_text: 'get_text',
|
|
295
|
+
get_html: 'get_html',
|
|
296
|
+
snapshot: 'get_text',
|
|
297
|
+
get_cookies: 'get_text',
|
|
298
|
+
extract_links: 'get_text',
|
|
299
|
+
read_as_markdown: 'get_text',
|
|
300
|
+
auth_check: 'get_text',
|
|
301
|
+
wait_for: 'wait_for',
|
|
302
|
+
frames_list: 'frames_list',
|
|
303
|
+
print_pdf: 'print_pdf',
|
|
304
|
+
// capability-gated
|
|
305
|
+
eval: 'eval',
|
|
306
|
+
download_file: 'download_file',
|
|
307
|
+
upload_file: 'upload_file',
|
|
308
|
+
console_logs: 'observers',
|
|
309
|
+
network_log: 'observers',
|
|
310
|
+
dialogs: 'observers',
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* Would `method` be refused on EVERY page under this policy?
|
|
314
|
+
*
|
|
315
|
+
* Runs the REAL `evaluatePolicy` — the same function the gate and the extension
|
|
316
|
+
* router run, so this can never disagree with them — but with the domain
|
|
317
|
+
* allowlist widened to `*`. Only the capability gates (eval / downloads /
|
|
318
|
+
* uploads / observers / mutations) can then reject, which is exactly the
|
|
319
|
+
* question we want: a tool blocked merely because the current tab is off the
|
|
320
|
+
* allowlist stays advertised, since another tab may well be on it.
|
|
321
|
+
*/
|
|
322
|
+
function isCapabilityDenied(method, policy) {
|
|
323
|
+
return !(0, policy_2.evaluatePolicy)('https://probe.invalid/', method, { ...policy, allowDomains: ['*'] }).ok;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Is `name` usable at all under `policy`? Undefined policy = advertise
|
|
327
|
+
* everything (tests and any caller that builds a server without one).
|
|
328
|
+
*/
|
|
329
|
+
function isToolPolicyUsable(name, policy) {
|
|
330
|
+
if (!policy)
|
|
331
|
+
return true;
|
|
332
|
+
const method = TOOL_GATE[name];
|
|
333
|
+
if (!method)
|
|
334
|
+
return true;
|
|
335
|
+
return !isCapabilityDenied(method, policy);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The switchable capabilities, the flag that turns each on, and which wire
|
|
339
|
+
* methods it covers. `probe` is any one method it gates, fed to the real
|
|
340
|
+
* `isCapabilityDenied` so this can never disagree with the catalog filter.
|
|
341
|
+
*/
|
|
342
|
+
const CAPABILITIES = [
|
|
343
|
+
{ capability: 'mutations', flag: '--enable-mutations', probe: 'click', covers: policy_2.isMutatingMethod },
|
|
344
|
+
{ capability: 'eval', flag: '--unsafe-enable-eval', probe: 'eval', covers: (m) => m === 'eval' },
|
|
345
|
+
{ capability: 'downloads', flag: '--enable-downloads', probe: 'download_file', covers: (m) => m === 'download_file' },
|
|
346
|
+
{ capability: 'uploads', flag: '--enable-uploads', probe: 'upload_file', covers: (m) => m === 'upload_file' },
|
|
347
|
+
{ capability: 'observers', flag: '--enable-observers', probe: 'observers', covers: (m) => m === 'observers' },
|
|
348
|
+
];
|
|
349
|
+
/**
|
|
350
|
+
* Which capabilities the policy has switched off, the flag for each, and the
|
|
351
|
+
* tools that left the catalog because of it. Surfaced by `chrome_status`: once
|
|
352
|
+
* `navigate` is hidden the model cannot learn about it from a POLICY_DENIED
|
|
353
|
+
* any more, so without this it would tell the user it "can't browse" instead
|
|
354
|
+
* of naming the one flag that fixes it. Tools already cut by `--tools` are not
|
|
355
|
+
* listed, since the flag would not bring them back.
|
|
356
|
+
*/
|
|
357
|
+
function disabledCapabilities(policy) {
|
|
358
|
+
const out = [];
|
|
359
|
+
for (const c of CAPABILITIES) {
|
|
360
|
+
if (!isCapabilityDenied(c.probe, policy))
|
|
361
|
+
continue;
|
|
362
|
+
const hiddenTools = enabledToolNames().filter((n) => {
|
|
363
|
+
const m = TOOL_GATE[n];
|
|
364
|
+
return m != null && c.covers(m);
|
|
365
|
+
});
|
|
366
|
+
if (hiddenTools.length > 0)
|
|
367
|
+
out.push({ capability: c.capability, flag: c.flag, hiddenTools });
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
/** The `chrome_status` fields describing switched-off capabilities, or nothing. */
|
|
372
|
+
function capabilityStatus(policy) {
|
|
373
|
+
const off = disabledCapabilities(policy);
|
|
374
|
+
if (off.length === 0)
|
|
375
|
+
return {};
|
|
376
|
+
return {
|
|
377
|
+
disabledCapabilities: off,
|
|
378
|
+
capabilityHint: 'These tools are not available in this session because their capability is off. ' +
|
|
379
|
+
'If the user asks for one of them, tell them to add the listed flag to the chrome-mcp ' +
|
|
380
|
+
'command in their MCP config and restart the client.',
|
|
381
|
+
};
|
|
382
|
+
}
|
|
245
383
|
/** The names actually advertised, in catalog order. */
|
|
246
384
|
function enabledToolNames() {
|
|
247
385
|
return exports.TOOL_NAMES.filter(isToolEnabled);
|
|
@@ -927,11 +1065,12 @@ exports.TOOL_HANDLERS = {
|
|
|
927
1065
|
...(path ? {} : { note: 'no active task workspace, so the PDF was not saved to disk' }),
|
|
928
1066
|
});
|
|
929
1067
|
},
|
|
930
|
-
chrome_status: async () => {
|
|
1068
|
+
chrome_status: async (_a, ctx) => {
|
|
931
1069
|
const profiles = profileBridge ? { profiles: profileBridge.pairedProfiles() } : {};
|
|
1070
|
+
const capabilities = capabilityStatus(ctx.policy);
|
|
932
1071
|
try {
|
|
933
1072
|
const ex = await (0, manager_1.getManager)().ensureReady();
|
|
934
|
-
return (0, envelopes_1.jsonResult)({ ...ex.status(), ...profiles });
|
|
1073
|
+
return (0, envelopes_1.jsonResult)({ ...ex.status(), ...profiles, ...capabilities });
|
|
935
1074
|
}
|
|
936
1075
|
catch (err) {
|
|
937
1076
|
return (0, envelopes_1.jsonResult)({
|
|
@@ -940,6 +1079,7 @@ exports.TOOL_HANDLERS = {
|
|
|
940
1079
|
detail: errMessage(err),
|
|
941
1080
|
activeProfile: (0, workspace_1.peekActiveWorkspace)()?.profile ?? 'default',
|
|
942
1081
|
...profiles,
|
|
1082
|
+
...capabilities,
|
|
943
1083
|
});
|
|
944
1084
|
}
|
|
945
1085
|
},
|
|
@@ -1169,24 +1309,41 @@ function assertNoDrift() {
|
|
|
1169
1309
|
for (const n of handlers)
|
|
1170
1310
|
if (!defs.has(n))
|
|
1171
1311
|
throw new Error(`handler "${n}" has no advertised definition`);
|
|
1312
|
+
// A tool with no TOOL_GATE entry would silently be treated as always-usable,
|
|
1313
|
+
// so a new tool must declare the method it gates on (or `null`) explicitly.
|
|
1314
|
+
for (const n of defs) {
|
|
1315
|
+
if (!(n in TOOL_GATE))
|
|
1316
|
+
throw new Error(`tool "${n}" has no TOOL_GATE entry (declare its wire method, or null)`);
|
|
1317
|
+
}
|
|
1172
1318
|
}
|
|
1173
1319
|
// ---------------------------------------------------------------------------
|
|
1174
1320
|
// Wiring
|
|
1175
1321
|
// ---------------------------------------------------------------------------
|
|
1176
|
-
function registerTools(server) {
|
|
1322
|
+
function registerTools(server, policy) {
|
|
1177
1323
|
assertNoDrift();
|
|
1178
1324
|
// Register each tool with its zod `inputSchema`. The SDK advertises it in
|
|
1179
1325
|
// `tools/list` and validates arguments before invoking the handler, which
|
|
1180
1326
|
// just routes back through `dispatchToolCall` — our never-throw firewall that
|
|
1181
1327
|
// applies the rate limit, executor readiness, policy gate, and history log.
|
|
1328
|
+
const policyDropped = [];
|
|
1182
1329
|
for (const d of exports.TOOL_DEFINITIONS) {
|
|
1183
1330
|
if (!isToolEnabled(d.name))
|
|
1184
1331
|
continue;
|
|
1332
|
+
// A tool the policy has switched off can only ever answer POLICY_DENIED.
|
|
1333
|
+
// Don't pay for its schema on every turn, and don't invite a wasted call.
|
|
1334
|
+
if (!isToolPolicyUsable(d.name, policy)) {
|
|
1335
|
+
policyDropped.push(d.name);
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1185
1338
|
server.registerTool(d.name, { description: d.description, inputSchema: d.inputSchema }, async (args) => dispatchToolCall(d.name, args));
|
|
1186
1339
|
}
|
|
1187
1340
|
const advertised = enabledToolNames();
|
|
1188
1341
|
if (advertised.length < exports.TOOL_NAMES.length) {
|
|
1189
1342
|
(0, log_1.logErr)(`--tools: advertising ${advertised.length} of ${exports.TOOL_NAMES.length} tools (${advertised.join(', ')})`);
|
|
1190
1343
|
}
|
|
1344
|
+
if (policyDropped.length > 0) {
|
|
1345
|
+
(0, log_1.logErr)(`policy: ${policyDropped.length} tool(s) not advertised because the capability is off ` +
|
|
1346
|
+
`(${policyDropped.join(', ')}) — enable them with the matching flag to get them back.`);
|
|
1347
|
+
}
|
|
1191
1348
|
}
|
|
1192
1349
|
//# sourceMappingURL=tools.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mehmoodqureshi/chrome-mcp",
|
|
3
|
-
"
|
|
3
|
+
"mcpName": "io.github.Mehmoodqureshi/chrome-mcp",
|
|
4
|
+
"version": "0.9.11",
|
|
4
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.",
|
|
5
6
|
"author": "Mehmood Ur Rehman Qureshi",
|
|
6
7
|
"license": "MIT",
|