@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,192 @@
1
+ "use strict";
2
+ /**
3
+ * src/bridge/connection.ts — one authenticated extension connection.
4
+ *
5
+ * Owns the pending-request table: each `sendCommand` mints an id, sends a
6
+ * CommandFrame, and parks a {resolve,reject,timer} until the matching
7
+ * result/error frame arrives. Guarantees:
8
+ * - method-aware per-request timeout that rejects the ONE call (never closes
9
+ * the socket),
10
+ * - reject-ALL-pending with EXTENSION_DISCONNECTED on close,
11
+ * - backpressure rejection (screenshots are large; never queue unboundedly),
12
+ * - app-level ping/pong heartbeat (optional; disabled when heartbeatMs<=0).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ExtensionConnection = void 0;
16
+ const protocol_1 = require("../../shared/protocol");
17
+ const types_1 = require("../executor/types");
18
+ const MAX_BUFFERED_BYTES = 8 * 1024 * 1024;
19
+ const LONG_METHODS = new Set(['screenshot', 'wait_for', 'navigate', 'download_file']);
20
+ function defaultTimeoutFor(method) {
21
+ return LONG_METHODS.has(method) ? 60_000 : 30_000;
22
+ }
23
+ /** Map a wire error code onto a local ExecutorError code (the wire enum is a
24
+ * near-superset; unknown codes degrade to CDP_ERROR while keeping the message). */
25
+ function mapWireErrorCode(code) {
26
+ const known = {
27
+ TIMEOUT: 'TIMEOUT',
28
+ POLICY_DENIED: 'POLICY_DENIED',
29
+ DETACHED: 'DETACHED',
30
+ DEVTOOLS_OPEN: 'DEVTOOLS_OPEN',
31
+ TARGET_GONE: 'TARGET_GONE',
32
+ SELECTOR_NOT_FOUND: 'SELECTOR_NOT_FOUND',
33
+ REF_EXPIRED: 'REF_EXPIRED',
34
+ DOWNLOAD_FAILED: 'DOWNLOAD_FAILED',
35
+ EVAL_THREW: 'EVAL_FAILED',
36
+ };
37
+ return known[code] ?? 'TARGET_GONE';
38
+ }
39
+ class ExtensionConnection {
40
+ extId;
41
+ sessionId;
42
+ ws;
43
+ pending = new Map();
44
+ seq = 0;
45
+ closed = false;
46
+ heartbeat = null;
47
+ missedPongs = 0;
48
+ onEvent;
49
+ onClose;
50
+ onLog;
51
+ constructor(deps) {
52
+ this.ws = deps.ws;
53
+ this.extId = deps.extId;
54
+ this.sessionId = deps.sessionId;
55
+ this.onEvent = deps.onEvent;
56
+ this.onClose = deps.onClose;
57
+ this.onLog = deps.onLog;
58
+ this.ws.on('message', (raw) => this.handleMessage(raw));
59
+ this.ws.on('close', (code) => this.handleClose(code));
60
+ this.ws.on('error', () => this.handleClose(1006));
61
+ if (deps.heartbeatMs > 0)
62
+ this.startHeartbeat(deps.heartbeatMs);
63
+ }
64
+ /** Send a command and await its result (or reject on error/timeout/disconnect). */
65
+ sendCommand(method, params, opts) {
66
+ if (this.closed || this.ws.readyState !== this.ws.OPEN) {
67
+ return Promise.reject(new types_1.ExecutorError('EXTENSION_DISCONNECTED', 'extension is not connected'));
68
+ }
69
+ if (this.ws.bufferedAmount > MAX_BUFFERED_BYTES) {
70
+ return Promise.reject(new types_1.ExecutorError('BACKPRESSURE', 'bridge send buffer is full; try again'));
71
+ }
72
+ const id = String(++this.seq);
73
+ const timeoutMs = opts?.timeoutMs ?? defaultTimeoutFor(method);
74
+ const frame = {
75
+ type: 'command',
76
+ v: protocol_1.PROTOCOL_VERSION,
77
+ id,
78
+ method,
79
+ params,
80
+ tabId: opts?.tabId,
81
+ timeoutMs,
82
+ };
83
+ return new Promise((resolve, reject) => {
84
+ const timer = setTimeout(() => {
85
+ this.pending.delete(id);
86
+ reject(new types_1.ExecutorError('TIMEOUT', `"${method}" timed out after ${timeoutMs}ms`));
87
+ }, timeoutMs);
88
+ timer.unref?.();
89
+ this.pending.set(id, { resolve, reject, timer, method });
90
+ try {
91
+ this.ws.send(JSON.stringify(frame));
92
+ }
93
+ catch (err) {
94
+ clearTimeout(timer);
95
+ this.pending.delete(id);
96
+ reject(new types_1.ExecutorError('EXTENSION_DISCONNECTED', `send failed: ${String(err)}`));
97
+ }
98
+ });
99
+ }
100
+ close(code, reason) {
101
+ if (this.closed)
102
+ return;
103
+ try {
104
+ this.ws.close(code, reason);
105
+ }
106
+ catch {
107
+ /* ignore */
108
+ }
109
+ this.handleClose(code);
110
+ }
111
+ isOpen() {
112
+ return !this.closed && this.ws.readyState === this.ws.OPEN;
113
+ }
114
+ // -- internals ----------------------------------------------------------
115
+ handleMessage(raw) {
116
+ let frame;
117
+ try {
118
+ frame = JSON.parse(raw.toString());
119
+ }
120
+ catch {
121
+ this.onLog?.('dropped a non-JSON frame from the extension');
122
+ return;
123
+ }
124
+ switch (frame.type) {
125
+ case 'result':
126
+ this.settle(frame.id, frame);
127
+ break;
128
+ case 'error':
129
+ this.settle(frame.id, frame);
130
+ break;
131
+ case 'event': {
132
+ const ev = frame;
133
+ this.onEvent?.(ev.event, ev.data);
134
+ break;
135
+ }
136
+ case 'pong':
137
+ this.missedPongs = 0;
138
+ break;
139
+ default:
140
+ // hello arrives only pre-auth (handled by the server); ignore here.
141
+ break;
142
+ }
143
+ }
144
+ settle(id, frame) {
145
+ const p = this.pending.get(id);
146
+ if (!p)
147
+ return; // already timed out / unknown id
148
+ clearTimeout(p.timer);
149
+ this.pending.delete(id);
150
+ if (frame.type === 'result') {
151
+ p.resolve(frame.data);
152
+ }
153
+ else {
154
+ p.reject(new types_1.ExecutorError(mapWireErrorCode(frame.error.code), frame.error.message));
155
+ }
156
+ }
157
+ handleClose(code) {
158
+ if (this.closed)
159
+ return;
160
+ this.closed = true;
161
+ if (this.heartbeat)
162
+ clearInterval(this.heartbeat);
163
+ const pendings = [...this.pending.values()];
164
+ this.pending.clear();
165
+ for (const p of pendings) {
166
+ clearTimeout(p.timer);
167
+ p.reject(new types_1.ExecutorError('EXTENSION_DISCONNECTED', `connection closed (code ${code})`));
168
+ }
169
+ this.onClose?.(code);
170
+ }
171
+ startHeartbeat(ms) {
172
+ this.heartbeat = setInterval(() => {
173
+ if (this.closed)
174
+ return;
175
+ if (this.missedPongs >= 2) {
176
+ this.onLog?.('extension missed 2 heartbeats; terminating connection');
177
+ this.close(1001, 'heartbeat lost');
178
+ return;
179
+ }
180
+ this.missedPongs++;
181
+ try {
182
+ this.ws.send(JSON.stringify({ type: 'ping', v: protocol_1.PROTOCOL_VERSION, ts: Date.now() }));
183
+ }
184
+ catch {
185
+ this.close(1006, 'heartbeat send failed');
186
+ }
187
+ }, ms);
188
+ this.heartbeat.unref?.();
189
+ }
190
+ }
191
+ exports.ExtensionConnection = ExtensionConnection;
192
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * src/bridge/datadir.ts — the Electron-free data dir (the LinkedIn repo resolved
3
+ * this via `app.getPath`; we use `$CHROME_MCP_DATA` || `~/.chrome-mcp`). Holds
4
+ * the 0600 handshake and, later, the CDP-fallback profile.
5
+ */
6
+ /** Create (if needed) and return the data dir, 0700 so only the user can read it. */
7
+ export declare function ensureDataDir(dir?: string): string;
8
+ export declare function handshakePath(dir: string): string;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * src/bridge/datadir.ts — the Electron-free data dir (the LinkedIn repo resolved
4
+ * this via `app.getPath`; we use `$CHROME_MCP_DATA` || `~/.chrome-mcp`). Holds
5
+ * the 0600 handshake and, later, the CDP-fallback profile.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.ensureDataDir = ensureDataDir;
9
+ exports.handshakePath = handshakePath;
10
+ const node_fs_1 = require("node:fs");
11
+ const node_path_1 = require("node:path");
12
+ const config_1 = require("../config");
13
+ /** Create (if needed) and return the data dir, 0700 so only the user can read it. */
14
+ function ensureDataDir(dir) {
15
+ const d = dir ?? (0, config_1.resolveDataDir)();
16
+ (0, node_fs_1.mkdirSync)(d, { recursive: true, mode: 0o700 });
17
+ return d;
18
+ }
19
+ function handshakePath(dir) {
20
+ return (0, node_path_1.join)(dir, 'handshake.json');
21
+ }
22
+ //# sourceMappingURL=datadir.js.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * src/bridge/server.ts — the localhost WebSocket bridge.
3
+ *
4
+ * The server is the WS SERVER; the extension dials in as the single privileged
5
+ * CLIENT. The token is the ONLY trust boundary (the loopback bind is merely
6
+ * defense-in-depth; Origin is NOT a gate). Flow:
7
+ * 1. Accept any loopback upgrade.
8
+ * 2. Require a valid `hello` (matching version + token) within HELLO_TIMEOUT;
9
+ * otherwise send `unauthorized` and close 4401.
10
+ * 3. On success, send `welcome`, promote to the single ACTIVE connection
11
+ * (superseding any prior one — a security-relevant displacement event).
12
+ */
13
+ import { type WireEvent, type WireMethod } from '../../shared/protocol';
14
+ export interface DisplacementInfo {
15
+ oldExtId: string;
16
+ newExtId: string;
17
+ /** True when a DIFFERENT extension id supplanted the active one (suspicious). */
18
+ differentId: boolean;
19
+ }
20
+ export interface BridgeOptions {
21
+ token: string;
22
+ serverVersion: string;
23
+ port?: number;
24
+ host?: string;
25
+ heartbeatMs?: number;
26
+ /** Diagnostics — MUST never receive the token (a test asserts this). */
27
+ onLog?: (message: string) => void;
28
+ onDisplacement?: (info: DisplacementInfo) => void;
29
+ onEvent?: (event: WireEvent, data: Record<string, unknown>) => void;
30
+ }
31
+ export declare class BridgeServer {
32
+ private readonly opts;
33
+ private wss;
34
+ private active;
35
+ private boundPort;
36
+ private readonly heartbeatMs;
37
+ constructor(opts: BridgeOptions);
38
+ /** Bind and start listening. Returns the actual port (useful with port 0). */
39
+ start(): Promise<number>;
40
+ stop(): Promise<void>;
41
+ get port(): number;
42
+ hasActiveExtension(): boolean;
43
+ /** Send a command to the active extension, or reject if none is connected. */
44
+ sendCommand(method: WireMethod, params: Record<string, unknown>, opts?: {
45
+ tabId?: string;
46
+ timeoutMs?: number;
47
+ }): Promise<unknown>;
48
+ status(): {
49
+ extensionConnected: boolean;
50
+ port: number;
51
+ sessionId: string | null;
52
+ };
53
+ private handleConnection;
54
+ private reject;
55
+ private promote;
56
+ private send;
57
+ private log;
58
+ }
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ /**
3
+ * src/bridge/server.ts — the localhost WebSocket bridge.
4
+ *
5
+ * The server is the WS SERVER; the extension dials in as the single privileged
6
+ * CLIENT. The token is the ONLY trust boundary (the loopback bind is merely
7
+ * defense-in-depth; Origin is NOT a gate). Flow:
8
+ * 1. Accept any loopback upgrade.
9
+ * 2. Require a valid `hello` (matching version + token) within HELLO_TIMEOUT;
10
+ * otherwise send `unauthorized` and close 4401.
11
+ * 3. On success, send `welcome`, promote to the single ACTIVE connection
12
+ * (superseding any prior one — a security-relevant displacement event).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.BridgeServer = void 0;
16
+ const ws_1 = require("ws");
17
+ const node_crypto_1 = require("node:crypto");
18
+ const protocol_1 = require("../../shared/protocol");
19
+ const types_1 = require("../executor/types");
20
+ const connection_1 = require("./connection");
21
+ const auth_1 = require("./auth");
22
+ const HELLO_TIMEOUT_MS = 5_000;
23
+ const DEFAULT_HEARTBEAT_MS = 15_000;
24
+ class BridgeServer {
25
+ opts;
26
+ wss = null;
27
+ active = null;
28
+ boundPort = 0;
29
+ heartbeatMs;
30
+ constructor(opts) {
31
+ this.opts = opts;
32
+ this.heartbeatMs = opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
33
+ }
34
+ /** Bind and start listening. Returns the actual port (useful with port 0). */
35
+ async start() {
36
+ if (this.wss)
37
+ return this.boundPort;
38
+ const wss = new ws_1.WebSocketServer({ host: this.opts.host ?? protocol_1.BRIDGE_HOST, port: this.opts.port ?? 0 });
39
+ wss.on('connection', (ws) => this.handleConnection(ws));
40
+ await new Promise((resolve, reject) => {
41
+ wss.once('listening', resolve);
42
+ wss.once('error', reject);
43
+ });
44
+ const addr = wss.address();
45
+ this.boundPort = typeof addr === 'object' && addr ? addr.port : (this.opts.port ?? 0);
46
+ this.wss = wss;
47
+ this.log(`bridge listening on ${this.opts.host ?? protocol_1.BRIDGE_HOST}:${this.boundPort}`);
48
+ return this.boundPort;
49
+ }
50
+ async stop() {
51
+ this.active?.close(1001, 'server stopping');
52
+ this.active = null;
53
+ const wss = this.wss;
54
+ this.wss = null;
55
+ if (wss)
56
+ await new Promise((resolve) => wss.close(() => resolve()));
57
+ }
58
+ get port() {
59
+ return this.boundPort;
60
+ }
61
+ hasActiveExtension() {
62
+ return this.active?.isOpen() ?? false;
63
+ }
64
+ /** Send a command to the active extension, or reject if none is connected. */
65
+ async sendCommand(method, params, opts) {
66
+ if (!this.active || !this.active.isOpen()) {
67
+ throw new types_1.ExecutorError('EXTENSION_DISCONNECTED', 'no extension is paired');
68
+ }
69
+ return this.active.sendCommand(method, params, opts);
70
+ }
71
+ status() {
72
+ return {
73
+ extensionConnected: this.hasActiveExtension(),
74
+ port: this.boundPort,
75
+ sessionId: this.active?.sessionId ?? null,
76
+ };
77
+ }
78
+ // -- internals ----------------------------------------------------------
79
+ handleConnection(ws) {
80
+ let authed = false;
81
+ const helloTimer = setTimeout(() => {
82
+ if (authed)
83
+ return;
84
+ this.reject(ws, 'timeout');
85
+ }, HELLO_TIMEOUT_MS);
86
+ helloTimer.unref?.();
87
+ const onMessage = (raw) => {
88
+ if (authed)
89
+ return;
90
+ let frame;
91
+ try {
92
+ frame = JSON.parse(raw.toString());
93
+ }
94
+ catch {
95
+ clearTimeout(helloTimer);
96
+ this.reject(ws, 'bad_token');
97
+ return;
98
+ }
99
+ if (frame.type !== 'hello')
100
+ return; // ignore noise until a hello arrives
101
+ if (frame.v !== protocol_1.PROTOCOL_VERSION) {
102
+ clearTimeout(helloTimer);
103
+ this.reject(ws, 'bad_version');
104
+ return;
105
+ }
106
+ if (typeof frame.token !== 'string' || !(0, auth_1.tokensMatch)(frame.token, this.opts.token)) {
107
+ clearTimeout(helloTimer);
108
+ this.reject(ws, 'bad_token');
109
+ return;
110
+ }
111
+ // Authenticated. Hand the socket to an ExtensionConnection.
112
+ authed = true;
113
+ clearTimeout(helloTimer);
114
+ ws.off('message', onMessage);
115
+ this.promote(ws, frame.ext ?? { id: 'unknown', version: '0', chrome: '0' });
116
+ };
117
+ ws.on('message', onMessage);
118
+ ws.on('error', () => {
119
+ /* pre-auth socket errors are non-fatal; the close will clean up */
120
+ });
121
+ }
122
+ reject(ws, reason) {
123
+ this.send(ws, { type: 'unauthorized', v: protocol_1.PROTOCOL_VERSION, reason });
124
+ this.log(`rejected a connection: ${reason}`);
125
+ try {
126
+ ws.close(protocol_1.CLOSE_UNAUTHORIZED, reason);
127
+ }
128
+ catch {
129
+ /* ignore */
130
+ }
131
+ }
132
+ promote(ws, ext) {
133
+ const sessionId = (0, node_crypto_1.randomUUID)();
134
+ if (this.active && this.active.isOpen()) {
135
+ const prev = this.active;
136
+ const differentId = prev.extId !== ext.id;
137
+ this.log(`extension "${ext.id}" superseded active connection "${prev.extId}"` +
138
+ (differentId ? ' (DIFFERENT id — possible hijack; surfaced to status)' : ''));
139
+ this.opts.onDisplacement?.({ oldExtId: prev.extId, newExtId: ext.id, differentId });
140
+ prev.close(protocol_1.CLOSE_SUPERSEDED, 'superseded');
141
+ }
142
+ const conn = new connection_1.ExtensionConnection({
143
+ ws,
144
+ extId: ext.id,
145
+ sessionId,
146
+ heartbeatMs: this.heartbeatMs,
147
+ onEvent: this.opts.onEvent,
148
+ onLog: (m) => this.log(m),
149
+ onClose: () => {
150
+ if (this.active?.sessionId === sessionId)
151
+ this.active = null;
152
+ },
153
+ });
154
+ this.active = conn;
155
+ const welcome = {
156
+ type: 'welcome',
157
+ v: protocol_1.PROTOCOL_VERSION,
158
+ serverVersion: this.opts.serverVersion,
159
+ sessionId,
160
+ heartbeatMs: this.heartbeatMs,
161
+ };
162
+ this.send(ws, welcome);
163
+ this.log(`extension paired (session ${sessionId}, id "${ext.id}")`);
164
+ }
165
+ send(ws, frame) {
166
+ try {
167
+ ws.send(JSON.stringify(frame));
168
+ }
169
+ catch {
170
+ /* socket already gone */
171
+ }
172
+ }
173
+ log(message) {
174
+ this.opts.onLog?.(message);
175
+ }
176
+ }
177
+ exports.BridgeServer = BridgeServer;
178
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * src/cli.ts — `npx chrome-mcp` entrypoint.
4
+ *
5
+ * Boot order: resolve config → ensure data dir → generate a per-boot token →
6
+ * start the loopback bridge → write the 0600 handshake → configure the manager
7
+ * with the backend selector → serve MCP over stdio. `--help`/`--version` print
8
+ * and exit before stdio is claimed; `--print-pairing` runs only the bridge and
9
+ * prints the handshake path (for manual pairing), never serving MCP.
10
+ */
11
+ export {};
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * src/cli.ts — `npx chrome-mcp` entrypoint.
5
+ *
6
+ * Boot order: resolve config → ensure data dir → generate a per-boot token →
7
+ * start the loopback bridge → write the 0600 handshake → configure the manager
8
+ * with the backend selector → serve MCP over stdio. `--help`/`--version` print
9
+ * and exit before stdio is claimed; `--print-pairing` runs only the bridge and
10
+ * prints the handshake path (for manual pairing), never serving MCP.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const node_fs_1 = require("node:fs");
14
+ const node_path_1 = require("node:path");
15
+ const config_1 = require("./config");
16
+ const manager_1 = require("./executor/manager");
17
+ const select_1 = require("./executor/select");
18
+ const server_1 = require("./bridge/server");
19
+ const datadir_1 = require("./bridge/datadir");
20
+ const auth_1 = require("./bridge/auth");
21
+ const server_2 = require("./mcp/server");
22
+ function version() {
23
+ try {
24
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', '..', 'package.json'), 'utf8'));
25
+ return pkg.version ?? '0.0.0';
26
+ }
27
+ catch {
28
+ return '0.0.0';
29
+ }
30
+ }
31
+ async function main() {
32
+ const cfg = (0, config_1.parseArgs)(process.argv.slice(2));
33
+ if (cfg.showHelp) {
34
+ process.stdout.write(config_1.HELP_TEXT);
35
+ return;
36
+ }
37
+ if (cfg.showVersion) {
38
+ process.stdout.write(`${version()}\n`);
39
+ return;
40
+ }
41
+ const dataDir = (0, datadir_1.ensureDataDir)(cfg.dataDir);
42
+ const token = (0, auth_1.generateToken)();
43
+ const bridge = new server_1.BridgeServer({
44
+ token,
45
+ serverVersion: version(),
46
+ port: cfg.wsPort,
47
+ onLog: (m) => (0, server_2.logErr)(m),
48
+ onDisplacement: (d) => (0, server_2.logErr)(`SECURITY: extension connection displaced (different id: ${d.differentId})`),
49
+ });
50
+ const port = await bridge.start();
51
+ const handshakePath = (0, auth_1.writeHandshake)(dataDir, { port, token });
52
+ (0, server_2.logErr)(`pairing handshake written to ${handshakePath} (mode 0600; token not logged)`);
53
+ const cleanup = () => {
54
+ (0, auth_1.removeHandshake)(dataDir);
55
+ };
56
+ // Manual pairing helper: run the bridge, print the path, keep alive. NOT MCP.
57
+ if (cfg.printPairing) {
58
+ process.stdout.write(`${handshakePath}\n`);
59
+ (0, server_2.logErr)('pairing mode — bridge is up; open the extension and pair, then Ctrl-C.');
60
+ process.on('SIGINT', () => {
61
+ cleanup();
62
+ void bridge.stop().finally(() => process.exit(0));
63
+ });
64
+ return;
65
+ }
66
+ (0, manager_1.configureManager)({
67
+ policy: cfg.policy,
68
+ select: (0, select_1.createSelector)({
69
+ bridge,
70
+ cdpFallback: cfg.cdpFallback,
71
+ prefer: cfg.prefer,
72
+ cdp: {
73
+ mode: cfg.cdpEndpoint ? 'connect' : 'launch',
74
+ cdpEndpoint: cfg.cdpEndpoint,
75
+ userDataDir: dataDir,
76
+ headless: cfg.headless,
77
+ },
78
+ }),
79
+ });
80
+ (0, server_2.logErr)(`backend: extension-if-paired else ${cfg.cdpFallback ? 'CDP fallback' : 'none'} (prefer: ${cfg.prefer})`);
81
+ const shutdown = () => {
82
+ cleanup();
83
+ void Promise.allSettled([(0, server_2.stopMcpServer)(), bridge.stop()]).finally(() => process.exit(0));
84
+ };
85
+ process.on('SIGINT', shutdown);
86
+ process.on('SIGTERM', shutdown);
87
+ await (0, server_2.startMcpServer)();
88
+ }
89
+ main().catch((err) => {
90
+ (0, server_2.logErr)(`fatal: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
91
+ process.exit(1);
92
+ });
93
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * src/config.ts — the single source for runtime configuration.
3
+ *
4
+ * Resolves the WebSocket port, the data dir (where the 0600 handshake lives),
5
+ * and the security `Policy` from one place: defaults < env < CLI flags < policy
6
+ * file. Nothing else in the codebase should read `process.argv` or invent its
7
+ * own port/policy — they take a `CliConfig`.
8
+ */
9
+ import { type Policy } from './security/policy';
10
+ export type LogLevel = 'silent' | 'info' | 'debug';
11
+ export type BackendPreference = 'extension' | 'cdp';
12
+ export interface CliConfig {
13
+ /** Port the bridge binds; 0 = ephemeral (written to the handshake file). */
14
+ wsPort: number;
15
+ /** Directory holding handshake.json and the CDP-fallback profile. */
16
+ dataDir: string;
17
+ /** Resolved, fully-defaulted security policy. */
18
+ policy: Policy;
19
+ /** Whether to fall back to a Playwright-driven Chromium when no extension is paired. */
20
+ cdpFallback: boolean;
21
+ /** Optional CDP endpoint to attach to instead of launching (e.g. http://127.0.0.1:9222). */
22
+ cdpEndpoint?: string;
23
+ /** Which backend to prefer when both are available (testing knob). */
24
+ prefer: BackendPreference;
25
+ /** Run the CDP-fallback Chromium headless. */
26
+ headless: boolean;
27
+ /** `--print-pairing`: write the handshake and print its path (never the token). */
28
+ printPairing: boolean;
29
+ showHelp: boolean;
30
+ showVersion: boolean;
31
+ logLevel: LogLevel;
32
+ }
33
+ /** Resolve the data dir: `$CHROME_MCP_DATA` or `~/.chrome-mcp`. */
34
+ export declare function resolveDataDir(): string;
35
+ /**
36
+ * Parse argv (the slice AFTER `node script`, i.e. `process.argv.slice(2)`) plus
37
+ * env into a fully-resolved `CliConfig`. Pure except for the optional policy-file
38
+ * read triggered by `--policy <path>`.
39
+ */
40
+ export declare function parseArgs(argv: string[]): CliConfig;
41
+ /** Help text for `--help`. */
42
+ export declare const HELP_TEXT: string;