@mehmoodqureshi/chrome-mcp 0.6.0 → 0.6.3
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 +4 -0
- package/dist/shared/policy.js +25 -4
- package/dist/src/config.js +16 -7
- package/dist/src/mcp/helpers.d.ts +6 -0
- package/dist/src/mcp/helpers.js +40 -11
- package/dist/src/mcp/server.d.ts +2 -2
- package/dist/src/mcp/server.js +4 -3
- package/dist/src/mcp/tools.d.ts +10 -7
- package/dist/src/mcp/tools.js +59 -62
- package/extension-dist/background.js +11 -3
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# chrome-mcp
|
|
2
2
|
|
|
3
|
+
[](https://github.com/Mehmoodqureshi/chrome-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@mehmoodqureshi/chrome-mcp)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
Drive a **real Chrome browser** from Claude (or any MCP host). One pluggable
|
|
4
8
|
`Executor` interface, two backends:
|
|
5
9
|
|
package/dist/shared/policy.js
CHANGED
|
@@ -80,17 +80,38 @@ function hostOf(url) {
|
|
|
80
80
|
function isAboutBlank(url) {
|
|
81
81
|
return url === 'about:blank' || url === '' || url.startsWith('about:');
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Reduce an allowlist entry to the bare host it constrains. Users routinely paste
|
|
85
|
+
* a full URL ("https://example.com/app") or a "host:port/path" instead of a bare
|
|
86
|
+
* host; those forms would never equal a hostname and so silently match nothing.
|
|
87
|
+
* We strip the scheme, userinfo, port, and path/query/fragment, preserving a
|
|
88
|
+
* leading "*." wildcard and the two catch-all forms. Returns '' for a pattern
|
|
89
|
+
* that carries no host (which then matches nothing).
|
|
90
|
+
*/
|
|
91
|
+
function normalizeDomainPattern(pattern) {
|
|
92
|
+
let p = pattern.trim().toLowerCase();
|
|
93
|
+
if (p === '*' || p === '*://*/*')
|
|
94
|
+
return '*';
|
|
95
|
+
// Strip a leading scheme ("https://", "http://", any "scheme://").
|
|
96
|
+
p = p.replace(/^[a-z][a-z0-9+.-]*:\/\//, '');
|
|
97
|
+
// Drop everything from the first path/query/fragment separator onward.
|
|
98
|
+
p = p.replace(/[/?#].*$/, '');
|
|
99
|
+
// Strip userinfo ("user:pass@") then a trailing port (":8080").
|
|
100
|
+
p = p.replace(/^[^@]*@/, '').replace(/:\d+$/, '');
|
|
101
|
+
return p;
|
|
102
|
+
}
|
|
83
103
|
/** Convert a single domain glob to a predicate. '*' matches everything;
|
|
84
|
-
* '*.example.com' matches example.com and any subdomain; otherwise exact host.
|
|
104
|
+
* '*.example.com' matches example.com and any subdomain; otherwise exact host.
|
|
105
|
+
* URL/port/path forms are normalized to their bare host first. */
|
|
85
106
|
function globMatches(host, pattern) {
|
|
86
|
-
const p = pattern
|
|
87
|
-
if (p === '*'
|
|
107
|
+
const p = normalizeDomainPattern(pattern);
|
|
108
|
+
if (p === '*')
|
|
88
109
|
return true;
|
|
89
110
|
if (p.startsWith('*.')) {
|
|
90
111
|
const base = p.slice(2);
|
|
91
112
|
return host === base || host.endsWith('.' + base);
|
|
92
113
|
}
|
|
93
|
-
return host === p;
|
|
114
|
+
return p !== '' && host === p;
|
|
94
115
|
}
|
|
95
116
|
function isDomainAllowed(url, policy) {
|
|
96
117
|
const host = hostOf(url);
|
package/dist/src/config.js
CHANGED
|
@@ -162,6 +162,14 @@ function parseArgs(argv) {
|
|
|
162
162
|
throw new Error(`unknown argument: ${arg}`);
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
+
// Fallback permanently removed — this build is EXTENSION-ONLY. It NEVER
|
|
166
|
+
// launches or attaches a Chromium of its own; it only ever drives the user's
|
|
167
|
+
// real Chrome through the paired extension. Any CDP flags (--cdp-fallback,
|
|
168
|
+
// --cdp-endpoint, --prefer cdp) are still accepted for back-compat but are
|
|
169
|
+
// hard-overridden here so no separate/"fallback" browser can ever open.
|
|
170
|
+
cdpFallback = false;
|
|
171
|
+
cdpEndpoint = undefined;
|
|
172
|
+
prefer = 'extension';
|
|
165
173
|
// File first, then flags win.
|
|
166
174
|
const policy = (0, policy_1.resolvePolicy)({ ...policyFile, ...policyFlags });
|
|
167
175
|
// Uploads must be confined to a directory — refuse to start with uploads enabled
|
|
@@ -242,13 +250,14 @@ Connection:
|
|
|
242
250
|
CHROME_MCP_TOKEN env, if set, pins the token explicitly.
|
|
243
251
|
|
|
244
252
|
Backend:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
--
|
|
249
|
-
--cdp-
|
|
250
|
-
--
|
|
251
|
-
--
|
|
253
|
+
This build is EXTENSION-ONLY — it drives ONLY your real Chrome via the paired
|
|
254
|
+
extension and never launches or attaches a Chromium of its own. The CDP flags
|
|
255
|
+
below are accepted for back-compat but IGNORED (there is no fallback).
|
|
256
|
+
--cdp-fallback (ignored — fallback permanently removed)
|
|
257
|
+
--no-cdp-fallback (ignored — extension-only is always on)
|
|
258
|
+
--cdp-endpoint <url> (ignored — no CDP attach)
|
|
259
|
+
--prefer <which> (ignored — always "extension")
|
|
260
|
+
--headless (ignored — no CDP Chromium to run headless)
|
|
252
261
|
|
|
253
262
|
Security (default: deny-all safe mode):
|
|
254
263
|
--policy <file> Load a JSON policy file
|
|
@@ -13,10 +13,16 @@ export interface LinkOut {
|
|
|
13
13
|
/**
|
|
14
14
|
* Collect anchors from the page (or a subtree). Implemented as a single page
|
|
15
15
|
* eval so it is one round-trip; falls back to parsing getHtml if eval is denied.
|
|
16
|
+
*
|
|
17
|
+
* `dedupe` collapses anchors that share an href (nav/footer repetition is common
|
|
18
|
+
* noise when crawling); `limit` caps the number of links returned. Both are
|
|
19
|
+
* applied server-side after collection, so they work on either code path.
|
|
16
20
|
*/
|
|
17
21
|
export declare function extractLinks(ex: Executor, args: {
|
|
18
22
|
selector?: string;
|
|
19
23
|
sameOriginOnly?: boolean;
|
|
24
|
+
dedupe?: boolean;
|
|
25
|
+
limit?: number;
|
|
20
26
|
tabId?: string;
|
|
21
27
|
}): Promise<{
|
|
22
28
|
links: LinkOut[];
|
package/dist/src/mcp/helpers.js
CHANGED
|
@@ -13,6 +13,10 @@ const markdown_extract_1 = require("./markdown-extract");
|
|
|
13
13
|
/**
|
|
14
14
|
* Collect anchors from the page (or a subtree). Implemented as a single page
|
|
15
15
|
* eval so it is one round-trip; falls back to parsing getHtml if eval is denied.
|
|
16
|
+
*
|
|
17
|
+
* `dedupe` collapses anchors that share an href (nav/footer repetition is common
|
|
18
|
+
* noise when crawling); `limit` caps the number of links returned. Both are
|
|
19
|
+
* applied server-side after collection, so they work on either code path.
|
|
16
20
|
*/
|
|
17
21
|
async function extractLinks(ex, args) {
|
|
18
22
|
const root = args.selector ? JSON.stringify(args.selector) : 'null';
|
|
@@ -24,21 +28,46 @@ async function extractLinks(ex, args) {
|
|
|
24
28
|
href: a.href, text: (a.textContent || '').trim().slice(0, 200),
|
|
25
29
|
})).filter(l => l.href && (${args.sameOriginOnly ? 'l.href.startsWith(here)' : 'true'}));
|
|
26
30
|
})()`;
|
|
31
|
+
let links;
|
|
27
32
|
const res = await ex.eval(expr, { tabId: args.tabId });
|
|
28
33
|
if (res.ok && Array.isArray(res.value)) {
|
|
29
|
-
|
|
34
|
+
links = res.value;
|
|
30
35
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
else {
|
|
37
|
+
// Fallback: parse hrefs out of the HTML (e.g. when eval is policy-denied).
|
|
38
|
+
const { html } = await ex.getHtml(args.selector ? { selector: args.selector } : undefined, {
|
|
39
|
+
tabId: args.tabId,
|
|
40
|
+
});
|
|
41
|
+
links = [];
|
|
42
|
+
const re = /<a[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = re.exec(html)) !== null) {
|
|
45
|
+
links.push({ href: m[1], text: m[2].replace(/<[^>]+>/g, '').trim().slice(0, 200) });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { links: refineLinks(links, args) };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Collapse links that share an href (keeping the first, but preferring a
|
|
52
|
+
* non-empty label) when `dedupe` is set, then cap to `limit`. Order is
|
|
53
|
+
* preserved so the first occurrence of each href wins.
|
|
54
|
+
*/
|
|
55
|
+
function refineLinks(links, opts) {
|
|
56
|
+
let out = links;
|
|
57
|
+
if (opts.dedupe) {
|
|
58
|
+
const byHref = new Map();
|
|
59
|
+
for (const l of links) {
|
|
60
|
+
const existing = byHref.get(l.href);
|
|
61
|
+
if (!existing)
|
|
62
|
+
byHref.set(l.href, { ...l });
|
|
63
|
+
else if (!existing.text && l.text)
|
|
64
|
+
existing.text = l.text;
|
|
65
|
+
}
|
|
66
|
+
out = [...byHref.values()];
|
|
40
67
|
}
|
|
41
|
-
|
|
68
|
+
if (opts.limit !== undefined && out.length > opts.limit)
|
|
69
|
+
out = out.slice(0, opts.limit);
|
|
70
|
+
return out;
|
|
42
71
|
}
|
|
43
72
|
/** Read a page (or subtree) as readable markdown. */
|
|
44
73
|
async function readAsMarkdown(ex, args) {
|
package/dist/src/mcp/server.d.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* NOTHING may be written to stdout except the JSON-RPC stream — all diagnostics
|
|
7
7
|
* go to stderr via `logErr`.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
10
|
/** stderr only — never stdout in stdio mode. */
|
|
11
11
|
export declare function logErr(message: string): void;
|
|
12
12
|
/** Build a fresh `Server` with the full tool surface registered (no transport). */
|
|
13
|
-
export declare function createServer(version?: string):
|
|
13
|
+
export declare function createServer(version?: string): McpServer;
|
|
14
14
|
/** Start over stdio. Idempotent. */
|
|
15
15
|
export declare function startMcpServer(version?: string): Promise<void>;
|
|
16
16
|
/** Stop and release the transport. Idempotent, best-effort. */
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -13,7 +13,7 @@ exports.createServer = createServer;
|
|
|
13
13
|
exports.startMcpServer = startMcpServer;
|
|
14
14
|
exports.stopMcpServer = stopMcpServer;
|
|
15
15
|
exports.isMcpServerRunning = isMcpServerRunning;
|
|
16
|
-
const
|
|
16
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
17
17
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
18
18
|
const tools_1 = require("./tools");
|
|
19
19
|
const SERVER_NAME = 'chrome-mcp';
|
|
@@ -28,9 +28,10 @@ function logErr(message) {
|
|
|
28
28
|
}
|
|
29
29
|
/** Build a fresh `Server` with the full tool surface registered (no transport). */
|
|
30
30
|
function createServer(version = DEFAULT_VERSION) {
|
|
31
|
-
const srv = new
|
|
31
|
+
const srv = new mcp_js_1.McpServer({ name: SERVER_NAME, version }, { capabilities: { tools: {} } });
|
|
32
32
|
(0, tools_1.registerTools)(srv);
|
|
33
|
-
|
|
33
|
+
// `McpServer` wraps the low-level `Server`, which owns the `onerror` hook.
|
|
34
|
+
srv.server.onerror = (err) => {
|
|
34
35
|
logErr(`server error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
35
36
|
};
|
|
36
37
|
return srv;
|
package/dist/src/mcp/tools.d.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/mcp/tools.ts — the MCP tool surface: the advertised catalog
|
|
3
|
-
* (`TOOL_DEFINITIONS`), the name→handler dispatch
|
|
4
|
-
* never-throw firewall (`dispatchToolCall`), and
|
|
5
|
-
*
|
|
3
|
+
* (`TOOL_DEFINITIONS`, each with a zod `inputSchema`), the name→handler dispatch
|
|
4
|
+
* (`TOOL_HANDLERS`), the never-throw firewall (`dispatchToolCall`), and
|
|
5
|
+
* `registerTools()` which registers every tool on an `McpServer` via
|
|
6
|
+
* `registerTool` — the SDK validates the zod schema before dispatch runs.
|
|
6
7
|
*
|
|
7
8
|
* Each handler: validate args → **policy-gate against the relevant URL** → call
|
|
8
9
|
* the active Executor (or a server-side helper) → serialize via an envelope.
|
|
9
10
|
* Nothing here throws to the transport: `dispatchToolCall` renders any thrown
|
|
10
11
|
* `Error` as an `isError` result.
|
|
11
12
|
*/
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
13
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
import { z } from 'zod';
|
|
14
16
|
import type { Executor } from '../executor/types';
|
|
15
17
|
import { type Policy } from '../security/policy';
|
|
16
18
|
export interface ToolDefinition {
|
|
17
19
|
name: string;
|
|
18
20
|
description: string;
|
|
19
|
-
|
|
21
|
+
/** zod raw shape passed to `McpServer.registerTool`; the SDK validates it before dispatch. */
|
|
22
|
+
inputSchema: z.ZodRawShape;
|
|
20
23
|
}
|
|
21
24
|
export declare const TOOL_DEFINITIONS: ToolDefinition[];
|
|
22
25
|
interface ToolCtx {
|
|
@@ -30,5 +33,5 @@ export declare function resetRateLimiter(): void;
|
|
|
30
33
|
export declare function dispatchToolCall(name: string, rawArgs: unknown): Promise<CallToolResult>;
|
|
31
34
|
/** Assert the catalog and the dispatch table describe the same tool set. */
|
|
32
35
|
export declare function assertNoDrift(): void;
|
|
33
|
-
export declare function registerTools(server:
|
|
36
|
+
export declare function registerTools(server: McpServer): void;
|
|
34
37
|
export {};
|
package/dist/src/mcp/tools.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* src/mcp/tools.ts — the MCP tool surface: the advertised catalog
|
|
4
|
-
* (`TOOL_DEFINITIONS`), the name→handler dispatch
|
|
5
|
-
* never-throw firewall (`dispatchToolCall`), and
|
|
6
|
-
*
|
|
4
|
+
* (`TOOL_DEFINITIONS`, each with a zod `inputSchema`), the name→handler dispatch
|
|
5
|
+
* (`TOOL_HANDLERS`), the never-throw firewall (`dispatchToolCall`), and
|
|
6
|
+
* `registerTools()` which registers every tool on an `McpServer` via
|
|
7
|
+
* `registerTool` — the SDK validates the zod schema before dispatch runs.
|
|
7
8
|
*
|
|
8
9
|
* Each handler: validate args → **policy-gate against the relevant URL** → call
|
|
9
10
|
* the active Executor (or a server-side helper) → serialize via an envelope.
|
|
@@ -17,7 +18,7 @@ exports.dispatchToolCall = dispatchToolCall;
|
|
|
17
18
|
exports.assertNoDrift = assertNoDrift;
|
|
18
19
|
exports.registerTools = registerTools;
|
|
19
20
|
const node_path_1 = require("node:path");
|
|
20
|
-
const
|
|
21
|
+
const zod_1 = require("zod");
|
|
21
22
|
const types_1 = require("../executor/types");
|
|
22
23
|
const manager_1 = require("../executor/manager");
|
|
23
24
|
const policy_1 = require("../security/policy");
|
|
@@ -27,62 +28,57 @@ const helpers_1 = require("./helpers");
|
|
|
27
28
|
const tasks_1 = require("../bridge/tasks");
|
|
28
29
|
const workspace_1 = require("../bridge/workspace");
|
|
29
30
|
const validators_1 = require("./validators");
|
|
31
|
+
/** Shared selector|ref target — both optional; a handler that needs one calls `requireTarget`. */
|
|
30
32
|
const TARGET_PROPS = {
|
|
31
|
-
selector:
|
|
32
|
-
ref:
|
|
33
|
+
selector: zod_1.z.string().describe('CSS selector (exactly one of selector|ref)').optional(),
|
|
34
|
+
ref: zod_1.z.string().describe('Element ref from a prior read (exactly one of selector|ref)').optional(),
|
|
33
35
|
};
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
properties,
|
|
37
|
-
required,
|
|
38
|
-
additionalProperties: false,
|
|
39
|
-
});
|
|
36
|
+
const tabIdField = zod_1.z.string().describe('Target tab id (defaults to the active tab)').optional();
|
|
37
|
+
const waitUntilField = zod_1.z.enum(['load', 'domcontentloaded', 'networkidle']).describe('When to consider navigation done').optional();
|
|
40
38
|
exports.TOOL_DEFINITIONS = [
|
|
41
|
-
{ name: 'tabs_list', description: 'List open browser tabs.', inputSchema:
|
|
42
|
-
{ name: 'tab_select', description: 'Make a tab active by tabId.', inputSchema:
|
|
43
|
-
{ name: 'tab_new', description: 'Open a NEW tab (optionally at a URL) and focus it. Prefer this over `navigate` when the user says "open"/"go to" a site — `navigate` REPLACES the current tab. Pass active:false to open in the background (used by parallel batches).', inputSchema:
|
|
44
|
-
{ name: 'tab_close', description: 'Close a tab by tabId.', inputSchema:
|
|
45
|
-
{ name: 'navigate', description: 'Navigate a tab to a URL, REPLACING its current page. Acts on the active tab unless tabId is given — to open a site without losing the current page, use `tab_new` instead.', inputSchema:
|
|
46
|
-
{ name: 'back', description: 'Go back in history.', inputSchema:
|
|
47
|
-
{ name: 'forward', description: 'Go forward in history.', inputSchema:
|
|
48
|
-
{ name: 'reload', description: 'Reload the active (or given) tab.', inputSchema:
|
|
49
|
-
{ name: 'click', description: 'Click an element (target by selector or a snapshot ref). trusted=true uses real OS-level input.', inputSchema:
|
|
50
|
-
{ name: 'type', description: 'Type text into an element. trusted=true sends real keystrokes (works on React/Vue controlled inputs).', inputSchema:
|
|
51
|
-
{ name: 'select_option', description: 'Select option(s) of a <select> by value or visible label.', inputSchema:
|
|
52
|
-
{ name: 'press', description: 'Press a key (with optional modifiers).', inputSchema:
|
|
53
|
-
{ name: 'hover', description: 'Hover over an element.', inputSchema:
|
|
54
|
-
{ name: 'scroll', description: 'Scroll the page or to an element.', inputSchema:
|
|
55
|
-
{ name: 'screenshot', description: 'Capture a PNG screenshot (page or element).', inputSchema:
|
|
56
|
-
{ name: 'get_text', description: 'Get visible text of the page or an element.', inputSchema:
|
|
57
|
-
{ name: 'get_html', description: 'Get HTML of the page or an element.', inputSchema:
|
|
58
|
-
{ name: 'snapshot', description: 'Accessibility snapshot: interactive elements with stable refs to target by `ref` (more reliable than guessing CSS selectors).', inputSchema:
|
|
59
|
-
{ name: 'get_cookies', description: "Read cookies visible to the tab's URL (or a given url).", inputSchema:
|
|
60
|
-
{ name: 'storage', description: 'Read/write localStorage (or sessionStorage). op: get|set|remove|clear.', inputSchema:
|
|
61
|
-
{ name: 'eval', description: 'Evaluate JavaScript in the page (disabled in safe-mode).', inputSchema:
|
|
62
|
-
{ name: 'wait_for', description: 'Wait for a selector or text to appear/disappear.', inputSchema:
|
|
63
|
-
{ name: 'extract_links', description: 'Extract anchors from the page or a subtree.', inputSchema:
|
|
64
|
-
{ name: 'read_as_markdown', description: 'Read the page (or subtree) as readable markdown.', inputSchema:
|
|
65
|
-
{ name: 'fill_form', description: 'Fill multiple fields (keyed by selector) and optionally submit.', inputSchema:
|
|
66
|
-
{ name: 'download_file', description: 'Download a file by URL or from a link element.', inputSchema:
|
|
67
|
-
{ name: 'upload_file', description: 'Set local file(s) on a file <input> (target by selector or ref) — uploads without the OS dialog. Requires --enable-uploads. `files` are absolute local paths.', inputSchema:
|
|
68
|
-
{ name: 'chrome_status', description: 'Report backend/session status.', inputSchema:
|
|
69
|
-
{ 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:
|
|
70
|
-
{ name: 'task_new', description: 'Start a new task (run) under the active profile. Creates profiles/<profile>/tasks/<name>/ with downloads/, results/, screenshots/ and makes it the active task so all captured artifacts land there.', inputSchema:
|
|
71
|
-
{ name: 'tasks_list', description: 'List every task across all profiles under the data dir, with sizes and download counts.', inputSchema:
|
|
72
|
-
{ name: 'task_status', description: 'Report the active profile/task and the folder paths where this run\'s artifacts are stored.', inputSchema:
|
|
39
|
+
{ name: 'tabs_list', description: 'List open browser tabs.', inputSchema: {} },
|
|
40
|
+
{ name: 'tab_select', description: 'Make a tab active by tabId.', inputSchema: { tabId: zod_1.z.string() } },
|
|
41
|
+
{ name: 'tab_new', description: 'Open a NEW tab (optionally at a URL) and focus it. Prefer this over `navigate` when the user says "open"/"go to" a site — `navigate` REPLACES the current tab. Pass active:false to open in the background (used by parallel batches).', inputSchema: { url: zod_1.z.string().optional(), active: zod_1.z.boolean().optional() } },
|
|
42
|
+
{ name: 'tab_close', description: 'Close a tab by tabId.', inputSchema: { tabId: zod_1.z.string() } },
|
|
43
|
+
{ name: 'navigate', description: 'Navigate a tab to a URL, REPLACING its current page. Acts on the active tab unless tabId is given — to open a site without losing the current page, use `tab_new` instead.', inputSchema: { url: zod_1.z.string(), tabId: tabIdField, waitUntil: waitUntilField } },
|
|
44
|
+
{ name: 'back', description: 'Go back in history.', inputSchema: { tabId: tabIdField } },
|
|
45
|
+
{ name: 'forward', description: 'Go forward in history.', inputSchema: { tabId: tabIdField } },
|
|
46
|
+
{ name: 'reload', description: 'Reload the active (or given) tab.', inputSchema: { tabId: tabIdField, waitUntil: waitUntilField } },
|
|
47
|
+
{ name: 'click', description: 'Click an element (target by selector or a snapshot ref). trusted=true uses real OS-level input.', inputSchema: { ...TARGET_PROPS, tabId: tabIdField, button: zod_1.z.enum(['left', 'right', 'middle']).optional(), clickCount: zod_1.z.number().optional(), trusted: zod_1.z.boolean().optional() } },
|
|
48
|
+
{ name: 'type', description: 'Type text into an element. trusted=true sends real keystrokes (works on React/Vue controlled inputs).', inputSchema: { ...TARGET_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() } },
|
|
49
|
+
{ name: 'select_option', description: 'Select option(s) of a <select> by value or visible label.', inputSchema: { ...TARGET_PROPS, values: zod_1.z.array(zod_1.z.string()), tabId: tabIdField } },
|
|
50
|
+
{ 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 } },
|
|
51
|
+
{ name: 'hover', description: 'Hover over an element.', inputSchema: { ...TARGET_PROPS, tabId: tabIdField } },
|
|
52
|
+
{ name: 'scroll', description: 'Scroll the page or to an element.', inputSchema: { ...TARGET_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 } },
|
|
53
|
+
{ name: 'screenshot', description: 'Capture a PNG screenshot (page or element).', inputSchema: { ...TARGET_PROPS, fullPage: zod_1.z.boolean().optional(), tabId: tabIdField } },
|
|
54
|
+
{ name: 'get_text', description: 'Get visible text of the page or an element.', inputSchema: { ...TARGET_PROPS, tabId: tabIdField } },
|
|
55
|
+
{ name: 'get_html', description: 'Get HTML of the page or an element.', inputSchema: { ...TARGET_PROPS, outer: zod_1.z.boolean().optional(), tabId: tabIdField } },
|
|
56
|
+
{ name: 'snapshot', description: 'Accessibility snapshot: interactive elements with stable refs to target by `ref` (more reliable than guessing CSS selectors).', inputSchema: { interactiveOnly: zod_1.z.boolean().optional(), max: zod_1.z.number().optional(), tabId: tabIdField } },
|
|
57
|
+
{ name: 'get_cookies', description: "Read cookies visible to the tab's URL (or a given url).", inputSchema: { url: zod_1.z.string().optional(), tabId: tabIdField } },
|
|
58
|
+
{ name: 'storage', description: 'Read/write localStorage (or sessionStorage). op: get|set|remove|clear.', inputSchema: { op: zod_1.z.enum(['get', 'set', 'remove', 'clear']), key: zod_1.z.string().optional(), value: zod_1.z.string().optional(), session: zod_1.z.boolean().optional(), tabId: tabIdField } },
|
|
59
|
+
{ name: 'eval', description: 'Evaluate JavaScript in the page (disabled in safe-mode).', inputSchema: { expression: zod_1.z.string(), awaitPromise: zod_1.z.boolean().optional(), tabId: tabIdField } },
|
|
60
|
+
{ name: 'wait_for', description: 'Wait for a selector or text to appear/disappear.', inputSchema: { selector: zod_1.z.string().optional(), textContains: zod_1.z.string().optional(), gone: zod_1.z.boolean().optional(), timeoutMs: zod_1.z.number().optional(), tabId: tabIdField } },
|
|
61
|
+
{ name: 'extract_links', description: 'Extract anchors from the page or a subtree. dedupe=true collapses links sharing an href (nav/footer noise); limit caps the count.', inputSchema: { selector: zod_1.z.string().optional(), sameOriginOnly: zod_1.z.boolean().optional(), dedupe: zod_1.z.boolean().optional(), limit: zod_1.z.number().optional(), tabId: tabIdField } },
|
|
62
|
+
{ name: 'read_as_markdown', description: 'Read the page (or subtree) as readable markdown.', inputSchema: { selector: zod_1.z.string().optional(), tabId: tabIdField } },
|
|
63
|
+
{ name: 'fill_form', description: 'Fill multiple fields (keyed by selector) and optionally submit.', inputSchema: { fields: zod_1.z.record(zod_1.z.string(), zod_1.z.union([zod_1.z.string(), zod_1.z.boolean()])), submitSelector: zod_1.z.string().optional(), tabId: tabIdField } },
|
|
64
|
+
{ name: 'download_file', description: 'Download a file by URL or from a link element.', inputSchema: { url: zod_1.z.string().optional(), ...TARGET_PROPS, suggestedName: zod_1.z.string().optional(), tabId: tabIdField } },
|
|
65
|
+
{ name: 'upload_file', description: 'Set local file(s) on a file <input> (target by selector or ref) — uploads without the OS dialog. Requires --enable-uploads. `files` are absolute local paths.', inputSchema: { ...TARGET_PROPS, files: zod_1.z.array(zod_1.z.string()), tabId: tabIdField } },
|
|
66
|
+
{ name: 'chrome_status', description: 'Report backend/session status.', inputSchema: {} },
|
|
67
|
+
{ 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).') } },
|
|
68
|
+
{ name: 'task_new', description: 'Start a new task (run) under the active profile. Creates profiles/<profile>/tasks/<name>/ with downloads/, results/, screenshots/ and makes it the active task so all captured artifacts land there.', inputSchema: { name: zod_1.z.string().describe('Task name (becomes a folder; sanitized to a safe path segment).') } },
|
|
69
|
+
{ name: 'tasks_list', description: 'List every task across all profiles under the data dir, with sizes and download counts.', inputSchema: {} },
|
|
70
|
+
{ name: 'task_status', description: 'Report the active profile/task and the folder paths where this run\'s artifacts are stored.', inputSchema: {} },
|
|
73
71
|
{
|
|
74
72
|
name: 'batch',
|
|
75
73
|
description: 'Run multiple tool calls in one request — parallel (default) or serial. Each op is { tool, args } and goes through the same policy gate, rate limit, and error handling as a direct call. In parallel mode, tab-scoped ops MUST pass an explicit tabId (the active-tab default is unsafe under concurrency). Use to drive several tabs at once (e.g. open tabs, then batch get_text across them). Cannot be nested.',
|
|
76
|
-
inputSchema:
|
|
77
|
-
ops:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
maxConcurrency: { type: 'number', description: 'Parallel mode: max ops in flight at once (default 6).' },
|
|
85
|
-
}, ['ops']),
|
|
74
|
+
inputSchema: {
|
|
75
|
+
ops: zod_1.z
|
|
76
|
+
.array(zod_1.z.object({ tool: zod_1.z.string(), args: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional() }))
|
|
77
|
+
.describe('Operations to run; each is a tool name + its args.'),
|
|
78
|
+
mode: zod_1.z.enum(['parallel', 'serial']).describe('Default "parallel".').optional(),
|
|
79
|
+
stopOnError: zod_1.z.boolean().describe('Serial mode only: stop after the first failing op (the rest are skipped).').optional(),
|
|
80
|
+
maxConcurrency: zod_1.z.number().describe('Parallel mode: max ops in flight at once (default 6).').optional(),
|
|
81
|
+
},
|
|
86
82
|
},
|
|
87
83
|
];
|
|
88
84
|
/** Resolve the URL the policy should be evaluated against (the active tab). */
|
|
@@ -280,6 +276,8 @@ exports.TOOL_HANDLERS = {
|
|
|
280
276
|
const res = await (0, helpers_1.extractLinks)(ctx.ex, {
|
|
281
277
|
selector: (0, validators_1.optionalString)(a, 'selector'),
|
|
282
278
|
sameOriginOnly: (0, validators_1.optionalBoolean)(a, 'sameOriginOnly'),
|
|
279
|
+
dedupe: (0, validators_1.optionalBoolean)(a, 'dedupe'),
|
|
280
|
+
limit: (0, validators_1.optionalNumber)(a, 'limit', { min: 1, max: 10_000 }),
|
|
283
281
|
tabId: tabId(a),
|
|
284
282
|
});
|
|
285
283
|
(0, workspace_1.saveResult)('extract_links', 'json', JSON.stringify(res, null, 2));
|
|
@@ -441,13 +439,12 @@ function assertNoDrift() {
|
|
|
441
439
|
// ---------------------------------------------------------------------------
|
|
442
440
|
function registerTools(server) {
|
|
443
441
|
assertNoDrift();
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}))
|
|
450
|
-
}
|
|
451
|
-
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => dispatchToolCall(req.params.name, req.params.arguments));
|
|
442
|
+
// Register each tool with its zod `inputSchema`. The SDK advertises it in
|
|
443
|
+
// `tools/list` and validates arguments before invoking the handler, which
|
|
444
|
+
// just routes back through `dispatchToolCall` — our never-throw firewall that
|
|
445
|
+
// applies the rate limit, executor readiness, policy gate, and history log.
|
|
446
|
+
for (const d of exports.TOOL_DEFINITIONS) {
|
|
447
|
+
server.registerTool(d.name, { description: d.description, inputSchema: d.inputSchema }, async (args) => dispatchToolCall(d.name, args));
|
|
448
|
+
}
|
|
452
449
|
}
|
|
453
450
|
//# sourceMappingURL=tools.js.map
|
|
@@ -159,14 +159,22 @@
|
|
|
159
159
|
function isAboutBlank(url) {
|
|
160
160
|
return url === "about:blank" || url === "" || url.startsWith("about:");
|
|
161
161
|
}
|
|
162
|
+
function normalizeDomainPattern(pattern) {
|
|
163
|
+
let p = pattern.trim().toLowerCase();
|
|
164
|
+
if (p === "*" || p === "*://*/*") return "*";
|
|
165
|
+
p = p.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
166
|
+
p = p.replace(/[/?#].*$/, "");
|
|
167
|
+
p = p.replace(/^[^@]*@/, "").replace(/:\d+$/, "");
|
|
168
|
+
return p;
|
|
169
|
+
}
|
|
162
170
|
function globMatches(host, pattern) {
|
|
163
|
-
const p = pattern
|
|
164
|
-
if (p === "*"
|
|
171
|
+
const p = normalizeDomainPattern(pattern);
|
|
172
|
+
if (p === "*") return true;
|
|
165
173
|
if (p.startsWith("*.")) {
|
|
166
174
|
const base = p.slice(2);
|
|
167
175
|
return host === base || host.endsWith("." + base);
|
|
168
176
|
}
|
|
169
|
-
return host === p;
|
|
177
|
+
return p !== "" && host === p;
|
|
170
178
|
}
|
|
171
179
|
function isDomainAllowed(url, policy) {
|
|
172
180
|
const host = hostOf(url);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mehmoodqureshi/chrome-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Drive a real Chrome browser over MCP. A stdio MCP server (CLI) plus an MV3 extension, behind one pluggable Executor (extension via chrome.scripting, or a Playwright CDP fallback).",
|
|
5
5
|
"author": "Mehmood Ur Rehman Qureshi",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,15 +45,16 @@
|
|
|
45
45
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
46
46
|
"typecheck:ext": "tsc -p tsconfig.ext.json",
|
|
47
47
|
"clean": "rimraf dist extension-dist",
|
|
48
|
-
"test": "npm run clean && npm run build && npm run build:ext && node --test dist/test",
|
|
48
|
+
"test": "npm run clean && npm run build && npm run build:ext && node --test dist/test/*.test.js",
|
|
49
49
|
"test:hitl": "npm run build && node dist/hitl/index.js",
|
|
50
50
|
"prepack": "npm run clean && npm run build && npm run build:ext",
|
|
51
51
|
"postinstall": "node scripts/postinstall.js"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@modelcontextprotocol/sdk": "^1.0
|
|
54
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
55
55
|
"playwright": "^1.49.1",
|
|
56
|
-
"ws": "^8.18.0"
|
|
56
|
+
"ws": "^8.18.0",
|
|
57
|
+
"zod": "^4.4.3"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@types/chrome": "^0.0.287",
|