@mehmoodqureshi/chrome-mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/shared/download.d.ts +15 -0
- package/dist/shared/download.js +0 -0
- package/dist/shared/protocol.d.ts +114 -0
- package/dist/shared/protocol.js +55 -0
- package/dist/src/bridge/auth.d.ts +32 -0
- package/dist/src/bridge/auth.js +76 -0
- package/dist/src/bridge/connection.d.ts +48 -0
- package/dist/src/bridge/connection.js +192 -0
- package/dist/src/bridge/datadir.d.ts +8 -0
- package/dist/src/bridge/datadir.js +22 -0
- package/dist/src/bridge/server.d.ts +58 -0
- package/dist/src/bridge/server.js +178 -0
- package/dist/src/cli.d.ts +11 -0
- package/dist/src/cli.js +93 -0
- package/dist/src/config.d.ts +42 -0
- package/dist/src/config.js +188 -0
- package/dist/src/executor/cdp-executor.d.ts +131 -0
- package/dist/src/executor/cdp-executor.js +422 -0
- package/dist/src/executor/extension-executor.d.ts +102 -0
- package/dist/src/executor/extension-executor.js +124 -0
- package/dist/src/executor/manager.d.ts +43 -0
- package/dist/src/executor/manager.js +94 -0
- package/dist/src/executor/select.d.ts +23 -0
- package/dist/src/executor/select.js +53 -0
- package/dist/src/executor/stub-executor.d.ts +60 -0
- package/dist/src/executor/stub-executor.js +118 -0
- package/dist/src/executor/types.d.ts +192 -0
- package/dist/src/executor/types.js +24 -0
- package/dist/src/mcp/envelopes.d.ts +13 -0
- package/dist/src/mcp/envelopes.js +30 -0
- package/dist/src/mcp/helpers.d.ts +37 -0
- package/dist/src/mcp/helpers.js +71 -0
- package/dist/src/mcp/markdown-extract.d.ts +9 -0
- package/dist/src/mcp/markdown-extract.js +61 -0
- package/dist/src/mcp/server.d.ts +18 -0
- package/dist/src/mcp/server.js +82 -0
- package/dist/src/mcp/tools.d.ts +32 -0
- package/dist/src/mcp/tools.js +267 -0
- package/dist/src/mcp/validators.d.ts +32 -0
- package/dist/src/mcp/validators.js +104 -0
- package/dist/src/security/policy.d.ts +48 -0
- package/dist/src/security/policy.js +155 -0
- package/docs/BLUEPRINT.md +596 -0
- package/extension-dist/background.js +567 -0
- package/extension-dist/manifest.json +12 -0
- package/extension-dist/options.html +32 -0
- package/extension-dist/options.js +37 -0
- package/package.json +69 -0
- package/scripts/postinstall.js +50 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/mcp/validators.ts — lightweight, dependency-free runtime guards for tool
|
|
4
|
+
* arguments. The JSON Schema in `tools.ts` is the advertised contract; these
|
|
5
|
+
* guards defend each handler from malformed input and throw `McpToolError` with
|
|
6
|
+
* an actionable message (rendered as a structured `isError` result upstream).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.McpToolError = void 0;
|
|
10
|
+
exports.asArgs = asArgs;
|
|
11
|
+
exports.requireString = requireString;
|
|
12
|
+
exports.optionalString = optionalString;
|
|
13
|
+
exports.optionalBoolean = optionalBoolean;
|
|
14
|
+
exports.optionalNumber = optionalNumber;
|
|
15
|
+
exports.optionalStringArray = optionalStringArray;
|
|
16
|
+
exports.requireTarget = requireTarget;
|
|
17
|
+
exports.optionalTarget = optionalTarget;
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when a tool request can't be fulfilled for a caller-actionable reason
|
|
20
|
+
* (bad args, exactly-one-of violation, …). The dispatch firewall converts it to
|
|
21
|
+
* an `isError` result, so it never tears down the transport.
|
|
22
|
+
*/
|
|
23
|
+
class McpToolError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'McpToolError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.McpToolError = McpToolError;
|
|
30
|
+
/** Coerce raw tool args into a plain object, rejecting non-objects. */
|
|
31
|
+
function asArgs(raw) {
|
|
32
|
+
if (raw === undefined || raw === null)
|
|
33
|
+
return {};
|
|
34
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
35
|
+
throw new McpToolError('tool arguments must be a JSON object');
|
|
36
|
+
}
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
39
|
+
function requireString(args, key) {
|
|
40
|
+
const v = args[key];
|
|
41
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
42
|
+
throw new McpToolError(`"${key}" is required and must be a non-empty string`);
|
|
43
|
+
}
|
|
44
|
+
return v;
|
|
45
|
+
}
|
|
46
|
+
function optionalString(args, key) {
|
|
47
|
+
const v = args[key];
|
|
48
|
+
if (v === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (typeof v !== 'string')
|
|
51
|
+
throw new McpToolError(`"${key}" must be a string`);
|
|
52
|
+
return v;
|
|
53
|
+
}
|
|
54
|
+
function optionalBoolean(args, key) {
|
|
55
|
+
const v = args[key];
|
|
56
|
+
if (v === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (typeof v !== 'boolean')
|
|
59
|
+
throw new McpToolError(`"${key}" must be a boolean`);
|
|
60
|
+
return v;
|
|
61
|
+
}
|
|
62
|
+
function optionalNumber(args, key, bounds) {
|
|
63
|
+
const v = args[key];
|
|
64
|
+
if (v === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
67
|
+
throw new McpToolError(`"${key}" must be a finite number`);
|
|
68
|
+
}
|
|
69
|
+
if (bounds?.min !== undefined && v < bounds.min)
|
|
70
|
+
throw new McpToolError(`"${key}" must be >= ${bounds.min}`);
|
|
71
|
+
if (bounds?.max !== undefined && v > bounds.max)
|
|
72
|
+
throw new McpToolError(`"${key}" must be <= ${bounds.max}`);
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
function optionalStringArray(args, key) {
|
|
76
|
+
const v = args[key];
|
|
77
|
+
if (v === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string')) {
|
|
80
|
+
throw new McpToolError(`"${key}" must be an array of strings`);
|
|
81
|
+
}
|
|
82
|
+
return v;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Require EXACTLY ONE of `selector` | `ref`. Returns a normalized `Target`.
|
|
86
|
+
* Both-present and neither-present are both errors — the contract is one-of.
|
|
87
|
+
*/
|
|
88
|
+
function requireTarget(args) {
|
|
89
|
+
const hasSel = typeof args.selector === 'string' && args.selector.length > 0;
|
|
90
|
+
const hasRef = typeof args.ref === 'string' && args.ref.length > 0;
|
|
91
|
+
if (hasSel === hasRef) {
|
|
92
|
+
throw new McpToolError('provide exactly one of "selector" or "ref"');
|
|
93
|
+
}
|
|
94
|
+
return hasSel ? { selector: args.selector } : { ref: args.ref };
|
|
95
|
+
}
|
|
96
|
+
/** Like `requireTarget` but the target is optional (whole-page reads). */
|
|
97
|
+
function optionalTarget(args) {
|
|
98
|
+
const hasSel = typeof args.selector === 'string' && args.selector.length > 0;
|
|
99
|
+
const hasRef = typeof args.ref === 'string' && args.ref.length > 0;
|
|
100
|
+
if (!hasSel && !hasRef)
|
|
101
|
+
return undefined;
|
|
102
|
+
return requireTarget(args);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=validators.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/security/policy.ts — the default-deny domain policy and capability gate.
|
|
3
|
+
*
|
|
4
|
+
* This is the real exfiltration firewall. Because this tool feeds untrusted page
|
|
5
|
+
* text to an LLM that can call `eval`, prompt-injection-to-exfil is a PRIMARY
|
|
6
|
+
* threat, so the policy:
|
|
7
|
+
* - is ON by default with a SAFE default (empty allowlist, no eval, no
|
|
8
|
+
* downloads, mutations disabled),
|
|
9
|
+
* - gates READS as well as writes (reads are the exfil payload),
|
|
10
|
+
* - is enforced at BOTH ends — the executor dispatch (server) AND the
|
|
11
|
+
* extension router — before any attach/navigate/eval/read.
|
|
12
|
+
*
|
|
13
|
+
* `assertUrlAllowed(url, method, policy)` is the single chokepoint. It throws an
|
|
14
|
+
* `ExecutorError('POLICY_DENIED', …)` which the never-throw dispatch firewall
|
|
15
|
+
* renders as a structured MCP error.
|
|
16
|
+
*/
|
|
17
|
+
import type { WireMethod } from '../../shared/protocol';
|
|
18
|
+
export interface Policy {
|
|
19
|
+
/** Glob domain allowlist, e.g. ['example.com', '*.example.com', '*']. Empty = deny all. */
|
|
20
|
+
allowDomains: string[];
|
|
21
|
+
/** Allow the `eval` primitive at all. */
|
|
22
|
+
allowEval: boolean;
|
|
23
|
+
/** Allow `download_file`. */
|
|
24
|
+
allowDownloads: boolean;
|
|
25
|
+
/** Allow acting on / reading tabs whose URL is not in `allowDomains` is governed
|
|
26
|
+
* by `allowDomains`; this flag instead relaxes tab *management* (list/select)
|
|
27
|
+
* to all tabs regardless of their URL. Default false. */
|
|
28
|
+
allowAllTabs: boolean;
|
|
29
|
+
/** Safe-mode master switch for the mutating tool set (click/type/navigate/…). */
|
|
30
|
+
enableMutations: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** The SAFE default: deny everything until the user opts in. */
|
|
33
|
+
export declare const DEFAULT_POLICY: Readonly<Policy>;
|
|
34
|
+
/** Merge a partial (from a policy file and/or CLI flags) over the safe default. */
|
|
35
|
+
export declare function resolvePolicy(partial?: Partial<Policy>): Policy;
|
|
36
|
+
export declare function isReadMethod(method: WireMethod): boolean;
|
|
37
|
+
/** Everything in the mutating tool set that safe-mode disables. `eval` is gated
|
|
38
|
+
* separately via `allowEval`; `download_file` separately via `allowDownloads`. */
|
|
39
|
+
export declare function isMutatingMethod(method: WireMethod): boolean;
|
|
40
|
+
/** Parse a host out of a URL string; '' if it has none (about:blank, data:, …). */
|
|
41
|
+
export declare function hostOf(url: string): string;
|
|
42
|
+
export declare function isDomainAllowed(url: string, policy: Policy): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Throw `POLICY_DENIED` unless `method` against `url` is permitted by `policy`.
|
|
45
|
+
* `url` is the DESTINATION for navigation, otherwise the current tab URL.
|
|
46
|
+
* Pure and side-effect-free so it can run identically on server and extension.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assertUrlAllowed(url: string, method: WireMethod, policy: Policy): void;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/security/policy.ts — the default-deny domain policy and capability gate.
|
|
4
|
+
*
|
|
5
|
+
* This is the real exfiltration firewall. Because this tool feeds untrusted page
|
|
6
|
+
* text to an LLM that can call `eval`, prompt-injection-to-exfil is a PRIMARY
|
|
7
|
+
* threat, so the policy:
|
|
8
|
+
* - is ON by default with a SAFE default (empty allowlist, no eval, no
|
|
9
|
+
* downloads, mutations disabled),
|
|
10
|
+
* - gates READS as well as writes (reads are the exfil payload),
|
|
11
|
+
* - is enforced at BOTH ends — the executor dispatch (server) AND the
|
|
12
|
+
* extension router — before any attach/navigate/eval/read.
|
|
13
|
+
*
|
|
14
|
+
* `assertUrlAllowed(url, method, policy)` is the single chokepoint. It throws an
|
|
15
|
+
* `ExecutorError('POLICY_DENIED', …)` which the never-throw dispatch firewall
|
|
16
|
+
* renders as a structured MCP error.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.DEFAULT_POLICY = void 0;
|
|
20
|
+
exports.resolvePolicy = resolvePolicy;
|
|
21
|
+
exports.isReadMethod = isReadMethod;
|
|
22
|
+
exports.isMutatingMethod = isMutatingMethod;
|
|
23
|
+
exports.hostOf = hostOf;
|
|
24
|
+
exports.isDomainAllowed = isDomainAllowed;
|
|
25
|
+
exports.assertUrlAllowed = assertUrlAllowed;
|
|
26
|
+
const types_1 = require("../executor/types");
|
|
27
|
+
/** The SAFE default: deny everything until the user opts in. */
|
|
28
|
+
exports.DEFAULT_POLICY = Object.freeze({
|
|
29
|
+
allowDomains: [],
|
|
30
|
+
allowEval: false,
|
|
31
|
+
allowDownloads: false,
|
|
32
|
+
allowAllTabs: false,
|
|
33
|
+
enableMutations: false,
|
|
34
|
+
});
|
|
35
|
+
/** Merge a partial (from a policy file and/or CLI flags) over the safe default. */
|
|
36
|
+
function resolvePolicy(partial) {
|
|
37
|
+
return {
|
|
38
|
+
allowDomains: partial?.allowDomains ?? [...exports.DEFAULT_POLICY.allowDomains],
|
|
39
|
+
allowEval: partial?.allowEval ?? exports.DEFAULT_POLICY.allowEval,
|
|
40
|
+
allowDownloads: partial?.allowDownloads ?? exports.DEFAULT_POLICY.allowDownloads,
|
|
41
|
+
allowAllTabs: partial?.allowAllTabs ?? exports.DEFAULT_POLICY.allowAllTabs,
|
|
42
|
+
enableMutations: partial?.enableMutations ?? exports.DEFAULT_POLICY.enableMutations,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Method classification
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/** Methods that read page CONTENT (the exfil payload) — URL-gated. */
|
|
49
|
+
const READ_CONTENT = new Set([
|
|
50
|
+
'get_text',
|
|
51
|
+
'get_html',
|
|
52
|
+
'screenshot',
|
|
53
|
+
'wait_for',
|
|
54
|
+
]);
|
|
55
|
+
/** Content-mutating actions — URL-gated AND mutation-gated. */
|
|
56
|
+
const MUTATE_CONTENT = new Set([
|
|
57
|
+
'click',
|
|
58
|
+
'type',
|
|
59
|
+
'press',
|
|
60
|
+
'hover',
|
|
61
|
+
'scroll',
|
|
62
|
+
]);
|
|
63
|
+
/** Navigation — URL-gated by the DESTINATION url, and mutation-gated. */
|
|
64
|
+
const NAVIGATION = new Set([
|
|
65
|
+
'navigate',
|
|
66
|
+
'back',
|
|
67
|
+
'forward',
|
|
68
|
+
'reload',
|
|
69
|
+
]);
|
|
70
|
+
/** Tab management — mutation-gated, but not content-URL-gated (unless allowAllTabs is off). */
|
|
71
|
+
const TAB_MUTATE = new Set([
|
|
72
|
+
'tab_select',
|
|
73
|
+
'tab_new',
|
|
74
|
+
'tab_close',
|
|
75
|
+
]);
|
|
76
|
+
function isReadMethod(method) {
|
|
77
|
+
return READ_CONTENT.has(method) || method === 'tabs_list';
|
|
78
|
+
}
|
|
79
|
+
/** Everything in the mutating tool set that safe-mode disables. `eval` is gated
|
|
80
|
+
* separately via `allowEval`; `download_file` separately via `allowDownloads`. */
|
|
81
|
+
function isMutatingMethod(method) {
|
|
82
|
+
return MUTATE_CONTENT.has(method) || NAVIGATION.has(method) || TAB_MUTATE.has(method);
|
|
83
|
+
}
|
|
84
|
+
/** Whether the method touches a specific URL that must be allowlisted. */
|
|
85
|
+
function isUrlGated(method) {
|
|
86
|
+
return READ_CONTENT.has(method) || MUTATE_CONTENT.has(method) || NAVIGATION.has(method) || method === 'eval';
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Domain matching
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
/** Parse a host out of a URL string; '' if it has none (about:blank, data:, …). */
|
|
92
|
+
function hostOf(url) {
|
|
93
|
+
try {
|
|
94
|
+
return new URL(url).hostname.toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function isAboutBlank(url) {
|
|
101
|
+
return url === 'about:blank' || url === '' || url.startsWith('about:');
|
|
102
|
+
}
|
|
103
|
+
/** Convert a single domain glob to a predicate. '*' matches everything;
|
|
104
|
+
* '*.example.com' matches example.com and any subdomain; otherwise exact host. */
|
|
105
|
+
function globMatches(host, pattern) {
|
|
106
|
+
const p = pattern.trim().toLowerCase();
|
|
107
|
+
if (p === '*' || p === '*://*/*')
|
|
108
|
+
return true;
|
|
109
|
+
if (p.startsWith('*.')) {
|
|
110
|
+
const base = p.slice(2);
|
|
111
|
+
return host === base || host.endsWith('.' + base);
|
|
112
|
+
}
|
|
113
|
+
return host === p;
|
|
114
|
+
}
|
|
115
|
+
function isDomainAllowed(url, policy) {
|
|
116
|
+
const host = hostOf(url);
|
|
117
|
+
if (!host)
|
|
118
|
+
return false;
|
|
119
|
+
return policy.allowDomains.some((pat) => globMatches(host, pat));
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// The gate
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
/**
|
|
125
|
+
* Throw `POLICY_DENIED` unless `method` against `url` is permitted by `policy`.
|
|
126
|
+
* `url` is the DESTINATION for navigation, otherwise the current tab URL.
|
|
127
|
+
* Pure and side-effect-free so it can run identically on server and extension.
|
|
128
|
+
*/
|
|
129
|
+
function assertUrlAllowed(url, method, policy) {
|
|
130
|
+
// -- capability gates (independent of URL) --
|
|
131
|
+
if (method === 'eval' && !policy.allowEval) {
|
|
132
|
+
throw new types_1.ExecutorError('POLICY_DENIED', 'eval is disabled (safe-mode). Pass --unsafe-enable-eval to allow it.');
|
|
133
|
+
}
|
|
134
|
+
if (method === 'download_file' && !policy.allowDownloads) {
|
|
135
|
+
throw new types_1.ExecutorError('POLICY_DENIED', 'downloads are disabled. Pass --enable-downloads or set allowDownloads.');
|
|
136
|
+
}
|
|
137
|
+
if (isMutatingMethod(method) && !policy.enableMutations) {
|
|
138
|
+
throw new types_1.ExecutorError('POLICY_DENIED', `mutating tool "${method}" is disabled (safe-mode). Pass --enable-mutations to allow it.`);
|
|
139
|
+
}
|
|
140
|
+
// -- tab management without allowAllTabs still needs the target tab's URL allowlisted,
|
|
141
|
+
// but list/select/close don't carry a content URL here; treat them as allowed once
|
|
142
|
+
// the mutation gate above has passed (URL-gating of their effect happens on the
|
|
143
|
+
// subsequent content op). --
|
|
144
|
+
if (!isUrlGated(method))
|
|
145
|
+
return;
|
|
146
|
+
// Navigating to a blank page is always fine.
|
|
147
|
+
if (isAboutBlank(url) && NAVIGATION.has(method))
|
|
148
|
+
return;
|
|
149
|
+
if (!isDomainAllowed(url, policy)) {
|
|
150
|
+
const host = hostOf(url) || url;
|
|
151
|
+
throw new types_1.ExecutorError('POLICY_DENIED', `"${method}" denied: ${host} is not in the domain allowlist. ` +
|
|
152
|
+
`Add it to allowDomains, or pass --unsafe-all-domains.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=policy.js.map
|