@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/shared/download.d.ts +15 -0
  4. package/dist/shared/download.js +0 -0
  5. package/dist/shared/protocol.d.ts +114 -0
  6. package/dist/shared/protocol.js +55 -0
  7. package/dist/src/bridge/auth.d.ts +32 -0
  8. package/dist/src/bridge/auth.js +76 -0
  9. package/dist/src/bridge/connection.d.ts +48 -0
  10. package/dist/src/bridge/connection.js +192 -0
  11. package/dist/src/bridge/datadir.d.ts +8 -0
  12. package/dist/src/bridge/datadir.js +22 -0
  13. package/dist/src/bridge/server.d.ts +58 -0
  14. package/dist/src/bridge/server.js +178 -0
  15. package/dist/src/cli.d.ts +11 -0
  16. package/dist/src/cli.js +93 -0
  17. package/dist/src/config.d.ts +42 -0
  18. package/dist/src/config.js +188 -0
  19. package/dist/src/executor/cdp-executor.d.ts +131 -0
  20. package/dist/src/executor/cdp-executor.js +422 -0
  21. package/dist/src/executor/extension-executor.d.ts +102 -0
  22. package/dist/src/executor/extension-executor.js +124 -0
  23. package/dist/src/executor/manager.d.ts +43 -0
  24. package/dist/src/executor/manager.js +94 -0
  25. package/dist/src/executor/select.d.ts +23 -0
  26. package/dist/src/executor/select.js +53 -0
  27. package/dist/src/executor/stub-executor.d.ts +60 -0
  28. package/dist/src/executor/stub-executor.js +118 -0
  29. package/dist/src/executor/types.d.ts +192 -0
  30. package/dist/src/executor/types.js +24 -0
  31. package/dist/src/mcp/envelopes.d.ts +13 -0
  32. package/dist/src/mcp/envelopes.js +30 -0
  33. package/dist/src/mcp/helpers.d.ts +37 -0
  34. package/dist/src/mcp/helpers.js +71 -0
  35. package/dist/src/mcp/markdown-extract.d.ts +9 -0
  36. package/dist/src/mcp/markdown-extract.js +61 -0
  37. package/dist/src/mcp/server.d.ts +18 -0
  38. package/dist/src/mcp/server.js +82 -0
  39. package/dist/src/mcp/tools.d.ts +32 -0
  40. package/dist/src/mcp/tools.js +267 -0
  41. package/dist/src/mcp/validators.d.ts +32 -0
  42. package/dist/src/mcp/validators.js +104 -0
  43. package/dist/src/security/policy.d.ts +48 -0
  44. package/dist/src/security/policy.js +155 -0
  45. package/docs/BLUEPRINT.md +596 -0
  46. package/extension-dist/background.js +567 -0
  47. package/extension-dist/manifest.json +12 -0
  48. package/extension-dist/options.html +32 -0
  49. package/extension-dist/options.js +37 -0
  50. package/package.json +69 -0
  51. 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