mcp-browser-extension 0.9.25 → 0.9.26
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 +32 -35
- package/SETUP.md +20 -20
- package/dist/shared/protocol.d.ts +1 -1
- package/dist/shared/tab-border.d.ts +1 -1
- package/dist/shared/tab-border.js +1 -1
- package/dist/src/access.js +2 -1
- package/dist/src/auto-update.d.ts +6 -6
- package/dist/src/auto-update.js +9 -8
- package/dist/src/bridge/auth.d.ts +3 -3
- package/dist/src/bridge/auth.js +5 -4
- package/dist/src/bridge/datadir.d.ts +1 -1
- package/dist/src/bridge/datadir.js +1 -1
- package/dist/src/bridge/evict.d.ts +7 -7
- package/dist/src/bridge/evict.js +13 -13
- package/dist/src/bridge/peer.d.ts +4 -4
- package/dist/src/bridge/peer.js +4 -4
- package/dist/src/bridge/server.d.ts +6 -6
- package/dist/src/bridge/server.js +11 -11
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +13 -12
- package/dist/src/compat.d.ts +4 -0
- package/dist/src/compat.js +28 -0
- package/dist/src/config.d.ts +4 -4
- package/dist/src/config.js +27 -26
- package/dist/src/extension-command.js +3 -3
- package/dist/src/extension-install.d.ts +4 -4
- package/dist/src/extension-install.js +8 -6
- package/dist/src/mcp/log.js +2 -2
- package/dist/src/mcp/server.d.ts +1 -1
- package/dist/src/mcp/server.js +1 -1
- package/dist/src/mcp/tools.js +1 -1
- package/dist/src/native-host.d.ts +1 -1
- package/dist/src/native-host.js +1 -1
- package/dist/src/telemetry.d.ts +3 -3
- package/dist/src/telemetry.js +7 -6
- package/docs/BLUEPRINT.md +11 -11
- package/extension-dist/background.js +6 -6
- package/extension-dist/manifest.json +1 -1
- package/extension-dist/options.html +1 -1
- package/package.json +24 -5
package/dist/src/bridge/evict.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* src/bridge/evict.ts — take a pinned port back from a stale
|
|
3
|
+
* src/bridge/evict.ts — take a pinned port back from a stale mcp-browser-extension.
|
|
4
4
|
*
|
|
5
|
-
* A second session normally JOINS the
|
|
5
|
+
* A second session normally JOINS the mcp-browser-extension holding the port as a peer
|
|
6
6
|
* (see ./peer) and nobody is stopped. This is the fallback for when joining
|
|
7
|
-
* fails: an older
|
|
7
|
+
* fails: an older mcp-browser-extension that can't share the port, or a hung one. It will
|
|
8
8
|
* never release the port on its own, so rather than fail with EADDRINUSE and
|
|
9
9
|
* make the user kill it by hand, we do that for them.
|
|
10
10
|
*
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
* - the handshake names that port (so it describes *this* listener), and
|
|
14
14
|
* - the pid is not us, and
|
|
15
15
|
* - the pid is alive, and
|
|
16
|
-
* - its command line mentions
|
|
16
|
+
* - its command line mentions mcp-browser-extension.
|
|
17
17
|
* If any check fails we leave the process alone and let the caller surface the
|
|
18
18
|
* plain-English port-busy error instead. Refusing to evict is always safe; a
|
|
19
19
|
* wrong kill is not.
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.
|
|
22
|
+
exports.looksLikeMcpBrowserExtension = looksLikeMcpBrowserExtension;
|
|
23
23
|
exports.evictPortOwner = evictPortOwner;
|
|
24
24
|
const node_child_process_1 = require("node:child_process");
|
|
25
25
|
const auth_1 = require("./auth");
|
|
@@ -42,7 +42,7 @@ function isAlive(pid) {
|
|
|
42
42
|
* Full command line of `pid`, or null if it can't be read.
|
|
43
43
|
*
|
|
44
44
|
* Windows needs PowerShell/CIM: `tasklist` reports only the image name
|
|
45
|
-
* ("node.exe"), which can't distinguish
|
|
45
|
+
* ("node.exe"), which can't distinguish mcp-browser-extension from any other Node process
|
|
46
46
|
* — far too weak a signal to kill on.
|
|
47
47
|
*/
|
|
48
48
|
function commandLine(pid) {
|
|
@@ -68,12 +68,12 @@ function commandLine(pid) {
|
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
/** Does this command line belong to a
|
|
72
|
-
function
|
|
73
|
-
return /chrome-mcp/i.test(cmdline);
|
|
71
|
+
/** Does this command line belong to a mcp-browser-extension server? */
|
|
72
|
+
function looksLikeMcpBrowserExtension(cmdline) {
|
|
73
|
+
return /(?:mcp-browser-extension|chrome-mcp)/i.test(cmdline);
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
* Try to free `port` by terminating the
|
|
76
|
+
* Try to free `port` by terminating the mcp-browser-extension that owns it.
|
|
77
77
|
* Returns true only if an owner was found, killed, and confirmed gone.
|
|
78
78
|
*/
|
|
79
79
|
async function evictPortOwner(dataDir, port, log, deps = {}) {
|
|
@@ -100,12 +100,12 @@ async function evictPortOwner(dataDir, port, log, deps = {}) {
|
|
|
100
100
|
if (!alive(pid))
|
|
101
101
|
return false;
|
|
102
102
|
const cmdline = cmdOf(pid);
|
|
103
|
-
if (!cmdline || !
|
|
104
|
-
log(`port ${port} is held by pid ${pid}, which does not look like
|
|
103
|
+
if (!cmdline || !looksLikeMcpBrowserExtension(cmdline)) {
|
|
104
|
+
log(`port ${port} is held by pid ${pid}, which does not look like mcp-browser-extension — ` +
|
|
105
105
|
`leaving it alone. Stop it yourself, or use a different --port.`);
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
|
-
log(`port ${port} is held by
|
|
108
|
+
log(`port ${port} is held by mcp-browser-extension pid ${pid} (another session) — taking the port over`);
|
|
109
109
|
try {
|
|
110
110
|
kill(pid, 'SIGTERM');
|
|
111
111
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* src/bridge/peer.ts — let several
|
|
2
|
+
* src/bridge/peer.ts — let several mcp-browser-extension servers share one Chrome.
|
|
3
3
|
*
|
|
4
|
-
* Every MCP host session (each Claude terminal/tab) spawns its own
|
|
4
|
+
* Every MCP host session (each Claude terminal/tab) spawns its own mcp-browser-extension,
|
|
5
5
|
* but a browser's extension dials exactly one port. So the first server to bind
|
|
6
6
|
* the port is the HUB: it owns the extension connections. A later server that
|
|
7
|
-
* finds the port held by a live
|
|
7
|
+
* finds the port held by a live mcp-browser-extension joins it as a PEER over the same
|
|
8
8
|
* port and relays its commands through the hub, instead of killing it. If the
|
|
9
9
|
* hub exits, its peers race to bind the port; the winner becomes the new hub
|
|
10
10
|
* (with the same token, so the extension re-pairs on its own) and the rest join
|
|
@@ -93,7 +93,7 @@ export declare class HubLink {
|
|
|
93
93
|
private constructor();
|
|
94
94
|
/**
|
|
95
95
|
* Join the hub on `port` with `token`. Resolves null when nothing answers as a
|
|
96
|
-
*
|
|
96
|
+
* mcp-browser-extension hub in time: a wrong token, an older server that doesn't speak
|
|
97
97
|
* the peer protocol (it ignores the frame and times us out), or no listener.
|
|
98
98
|
*/
|
|
99
99
|
static join(host: string, port: number, token: string, onClose: () => void, onGrant?: (host: string, allow: boolean) => void): Promise<HubLink | null>;
|
package/dist/src/bridge/peer.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* src/bridge/peer.ts — let several
|
|
3
|
+
* src/bridge/peer.ts — let several mcp-browser-extension servers share one Chrome.
|
|
4
4
|
*
|
|
5
|
-
* Every MCP host session (each Claude terminal/tab) spawns its own
|
|
5
|
+
* Every MCP host session (each Claude terminal/tab) spawns its own mcp-browser-extension,
|
|
6
6
|
* but a browser's extension dials exactly one port. So the first server to bind
|
|
7
7
|
* the port is the HUB: it owns the extension connections. A later server that
|
|
8
|
-
* finds the port held by a live
|
|
8
|
+
* finds the port held by a live mcp-browser-extension joins it as a PEER over the same
|
|
9
9
|
* port and relays its commands through the hub, instead of killing it. If the
|
|
10
10
|
* hub exits, its peers race to bind the port; the winner becomes the new hub
|
|
11
11
|
* (with the same token, so the extension re-pairs on its own) and the rest join
|
|
@@ -64,7 +64,7 @@ class HubLink {
|
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Join the hub on `port` with `token`. Resolves null when nothing answers as a
|
|
67
|
-
*
|
|
67
|
+
* mcp-browser-extension hub in time: a wrong token, an older server that doesn't speak
|
|
68
68
|
* the peer protocol (it ignores the frame and times us out), or no listener.
|
|
69
69
|
*/
|
|
70
70
|
static join(host, port, token, onClose, onGrant) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 3. On success, send `welcome` and file the connection under its profile
|
|
11
11
|
* (superseding only that profile's prior one — a security-relevant
|
|
12
12
|
* displacement event).
|
|
13
|
-
* Other
|
|
13
|
+
* Other mcp-browser-extension servers (one per Claude session) may join the same port as
|
|
14
14
|
* peers with `peer_hello` and relay their commands through us; see ./peer.
|
|
15
15
|
*/
|
|
16
16
|
import { type WireEvent, type WireMethod, type WirePolicy } from '../../shared/protocol';
|
|
@@ -65,7 +65,7 @@ export interface BridgeOptions {
|
|
|
65
65
|
host?: string;
|
|
66
66
|
heartbeatMs?: number;
|
|
67
67
|
/** Data dir holding the handshake. Enables reclaiming a pinned port from a
|
|
68
|
-
* stale
|
|
68
|
+
* stale mcp-browser-extension (see ./evict). Omit to disable eviction entirely. */
|
|
69
69
|
dataDir?: string;
|
|
70
70
|
/** Diagnostics — MUST never receive the token (a test asserts this). */
|
|
71
71
|
onLog?: (message: string) => void;
|
|
@@ -74,7 +74,7 @@ export interface BridgeOptions {
|
|
|
74
74
|
/**
|
|
75
75
|
* Called whenever this server takes a role on the port: 'hub' when it binds
|
|
76
76
|
* it (publish the handshake + pairing files then), 'peer' when it joins the
|
|
77
|
-
*
|
|
77
|
+
* mcp-browser-extension already holding it. Fires again after a failover.
|
|
78
78
|
*/
|
|
79
79
|
onRole?: (role: BridgeRole, info: {
|
|
80
80
|
port: number;
|
|
@@ -103,7 +103,7 @@ export declare class BridgeServer {
|
|
|
103
103
|
private token;
|
|
104
104
|
/** Peer role: the link to the hub that owns the browsers. */
|
|
105
105
|
private hub;
|
|
106
|
-
/** Hub role: the other
|
|
106
|
+
/** Hub role: the other mcp-browser-extension servers relaying through us. */
|
|
107
107
|
private readonly peers;
|
|
108
108
|
private stopped;
|
|
109
109
|
/** The last extension hello this hub turned away, for telling a stale token
|
|
@@ -133,12 +133,12 @@ export declare class BridgeServer {
|
|
|
133
133
|
get role(): BridgeRole | null;
|
|
134
134
|
/**
|
|
135
135
|
* Bind and start listening, or — when a pinned port is held by another live
|
|
136
|
-
*
|
|
136
|
+
* mcp-browser-extension — join it as a peer. Returns the port (useful with port 0).
|
|
137
137
|
*/
|
|
138
138
|
start(): Promise<number>;
|
|
139
139
|
private claim;
|
|
140
140
|
/**
|
|
141
|
-
* Join the
|
|
141
|
+
* Join the mcp-browser-extension holding `port` as a peer. Tries the token in the
|
|
142
142
|
* handshake it published first (sessions without --persist-token each mint
|
|
143
143
|
* their own), then ours.
|
|
144
144
|
*/
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 3. On success, send `welcome` and file the connection under its profile
|
|
12
12
|
* (superseding only that profile's prior one — a security-relevant
|
|
13
13
|
* displacement event).
|
|
14
|
-
* Other
|
|
14
|
+
* Other mcp-browser-extension servers (one per Claude session) may join the same port as
|
|
15
15
|
* peers with `peer_hello` and relay their commands through us; see ./peer.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -105,7 +105,7 @@ class BridgeServer {
|
|
|
105
105
|
token;
|
|
106
106
|
/** Peer role: the link to the hub that owns the browsers. */
|
|
107
107
|
hub = null;
|
|
108
|
-
/** Hub role: the other
|
|
108
|
+
/** Hub role: the other mcp-browser-extension servers relaying through us. */
|
|
109
109
|
peers = new Set();
|
|
110
110
|
stopped = false;
|
|
111
111
|
/** The last extension hello this hub turned away, for telling a stale token
|
|
@@ -171,7 +171,7 @@ class BridgeServer {
|
|
|
171
171
|
}
|
|
172
172
|
/**
|
|
173
173
|
* Bind and start listening, or — when a pinned port is held by another live
|
|
174
|
-
*
|
|
174
|
+
* mcp-browser-extension — join it as a peer. Returns the port (useful with port 0).
|
|
175
175
|
*/
|
|
176
176
|
async start() {
|
|
177
177
|
if (this.wss || this.hub)
|
|
@@ -205,16 +205,16 @@ class BridgeServer {
|
|
|
205
205
|
const inUse = err?.code === 'EADDRINUSE';
|
|
206
206
|
if (!inUse || port === 0)
|
|
207
207
|
throw inUse ? new Error(portBusyMessage(host, port)) : err;
|
|
208
|
-
// Another Claude session's
|
|
208
|
+
// Another Claude session's mcp-browser-extension owns the port: share it rather
|
|
209
209
|
// than kill it, so every session keeps working at once.
|
|
210
210
|
if (!triedJoin) {
|
|
211
211
|
triedJoin = true;
|
|
212
212
|
if (await this.joinHub(host, port))
|
|
213
213
|
return this.boundPort;
|
|
214
214
|
}
|
|
215
|
-
// Still busy after a failed join: an older
|
|
215
|
+
// Still busy after a failed join: an older mcp-browser-extension that can't share,
|
|
216
216
|
// or a hung one. It will never release the port on its own, so take it
|
|
217
|
-
// over (once). Only a verified
|
|
217
|
+
// over (once). Only a verified mcp-browser-extension is ever killed; anything else
|
|
218
218
|
// falls through to the wait-and-retry below.
|
|
219
219
|
if (!evicted && this.opts.dataDir) {
|
|
220
220
|
evicted = true;
|
|
@@ -231,7 +231,7 @@ class BridgeServer {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
/**
|
|
234
|
-
* Join the
|
|
234
|
+
* Join the mcp-browser-extension holding `port` as a peer. Tries the token in the
|
|
235
235
|
* handshake it published first (sessions without --persist-token each mint
|
|
236
236
|
* their own), then ours.
|
|
237
237
|
*/
|
|
@@ -252,7 +252,7 @@ class BridgeServer {
|
|
|
252
252
|
this.hub = link;
|
|
253
253
|
this.token = token;
|
|
254
254
|
this.boundPort = port;
|
|
255
|
-
this.log(`port ${host}:${port} is held by
|
|
255
|
+
this.log(`port ${host}:${port} is held by mcp-browser-extension pid ${link.hubPid || '?'} — sharing its browsers as a peer`);
|
|
256
256
|
this.opts.onRole?.('peer', { port, token, hubPid: link.hubPid });
|
|
257
257
|
return true;
|
|
258
258
|
}
|
|
@@ -275,7 +275,7 @@ class BridgeServer {
|
|
|
275
275
|
this.hub = null;
|
|
276
276
|
if (this.stopped)
|
|
277
277
|
return;
|
|
278
|
-
this.log('the
|
|
278
|
+
this.log('the mcp-browser-extension sharing its browsers with us went away — taking over the port');
|
|
279
279
|
void this.failover();
|
|
280
280
|
}
|
|
281
281
|
async failover() {
|
|
@@ -565,7 +565,7 @@ class BridgeServer {
|
|
|
565
565
|
return;
|
|
566
566
|
}
|
|
567
567
|
if (frame.type === 'peer_hello') {
|
|
568
|
-
// Another
|
|
568
|
+
// Another mcp-browser-extension (a second Claude session) joining to share our browsers.
|
|
569
569
|
const peer = frame;
|
|
570
570
|
clearTimeout(helloTimer);
|
|
571
571
|
if (peer.v !== protocol_1.PROTOCOL_VERSION)
|
|
@@ -632,7 +632,7 @@ class BridgeServer {
|
|
|
632
632
|
/** Hub side: serve a joined peer's relayed commands against our browsers. */
|
|
633
633
|
acceptPeer(ws, pid) {
|
|
634
634
|
this.peers.add(ws);
|
|
635
|
-
this.log(`
|
|
635
|
+
this.log(`mcp-browser-extension pid ${pid ?? '?'} joined as a peer (${this.peers.size} sharing these browsers)`);
|
|
636
636
|
this.sendPeer(ws, { type: 'peer_welcome', hubPid: process.pid });
|
|
637
637
|
this.sendPeer(ws, { type: 'peer_state', profiles: this.pairedProfiles() });
|
|
638
638
|
ws.on('message', (raw) => this.handlePeerFrame(ws, raw.toString()));
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* src/cli.ts — `npx
|
|
3
|
+
* src/cli.ts — `npx mcp-browser-extension` entrypoint.
|
|
4
4
|
*
|
|
5
5
|
* Boot order: resolve config → ensure data dir → generate a per-boot token →
|
|
6
6
|
* start the loopback bridge → write the 0600 handshake → configure the manager
|
package/dist/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
* src/cli.ts — `npx
|
|
4
|
+
* src/cli.ts — `npx mcp-browser-extension` entrypoint.
|
|
5
5
|
*
|
|
6
6
|
* Boot order: resolve config → ensure data dir → generate a per-boot token →
|
|
7
7
|
* start the loopback bridge → write the 0600 handshake → configure the manager
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* prints the handshake path (for manual pairing), never serving MCP.
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
const compat_1 = require("./compat");
|
|
13
14
|
const node_fs_1 = require("node:fs");
|
|
14
15
|
const node_path_1 = require("node:path");
|
|
15
16
|
const config_1 = require("./config");
|
|
@@ -79,7 +80,7 @@ function bundledExtensionVersion() {
|
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
82
|
* Mirror the bundled extension into the visible install folder
|
|
82
|
-
* (`~/
|
|
83
|
+
* (`~/mcp-browser-extension`) and report where Chrome should load it from. On a
|
|
83
84
|
* read-only home the bundled folder is returned instead; both are loadable.
|
|
84
85
|
*/
|
|
85
86
|
function installExtension() {
|
|
@@ -122,11 +123,11 @@ function flag(args, name) {
|
|
|
122
123
|
function hasFlag(args, name) {
|
|
123
124
|
return args.includes(name);
|
|
124
125
|
}
|
|
125
|
-
const TASKS_HELP = `
|
|
126
|
+
const TASKS_HELP = `mcp-browser-extension tasks — inspect and prune per-profile task workspaces.
|
|
126
127
|
|
|
127
128
|
Usage:
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
mcp-browser-extension tasks list [--json] [--data-dir <path>]
|
|
130
|
+
mcp-browser-extension tasks gc [--older-than <days>] [--keep <n>] [--profile <name>]
|
|
130
131
|
[--dry-run] [--data-dir <path>]
|
|
131
132
|
|
|
132
133
|
gc removes a task only when it is NOT among the newest --keep AND is older than
|
|
@@ -143,7 +144,7 @@ function runTasksCommand(argv) {
|
|
|
143
144
|
const sub = args[0];
|
|
144
145
|
const dataDirFlag = flag(args, '--data-dir');
|
|
145
146
|
if (dataDirFlag)
|
|
146
|
-
process.env.
|
|
147
|
+
process.env.MCP_BROWSER_EXTENSION_DATA = dataDirFlag;
|
|
147
148
|
const dataDir = (0, config_1.resolveDataDir)();
|
|
148
149
|
if (sub === 'list') {
|
|
149
150
|
const tasks = (0, tasks_1.listTasks)(dataDir);
|
|
@@ -238,7 +239,7 @@ async function main() {
|
|
|
238
239
|
process.stdout.write(`${installExtension()}\n`);
|
|
239
240
|
return;
|
|
240
241
|
}
|
|
241
|
-
// Before anything binds the port: if npm has a newer
|
|
242
|
+
// Before anything binds the port: if npm has a newer mcp-browser-extension, run that
|
|
242
243
|
// instead (stdio passes straight through), so no install stays on an old copy.
|
|
243
244
|
const handedOff = await (0, auto_update_1.maybeHandOff)({
|
|
244
245
|
current: version(),
|
|
@@ -275,7 +276,7 @@ async function main() {
|
|
|
275
276
|
if (role === 'hub')
|
|
276
277
|
publishPairing(info.port, info.token);
|
|
277
278
|
else {
|
|
278
|
-
(0, server_2.logErr)(`sharing the browsers of the
|
|
279
|
+
(0, server_2.logErr)(`sharing the browsers of the mcp-browser-extension already on port ${info.port} (pid ${info.hubPid || '?'}) — several sessions can drive Chrome at once`);
|
|
279
280
|
// Refresh the unpacked extension folder from this session too: the hub
|
|
280
281
|
// may be an old process from a window left open for days, and it would
|
|
281
282
|
// keep the folder on its old files. The mirror never downgrades.
|
|
@@ -293,11 +294,11 @@ async function main() {
|
|
|
293
294
|
// Drop the same port + token into the bundled extension folder so a Load
|
|
294
295
|
// unpacked from there pairs itself. Best-effort: a read-only install just
|
|
295
296
|
// falls back to the Options-page paste. Never take over a pairing file that
|
|
296
|
-
// a different, still-running
|
|
297
|
+
// a different, still-running mcp-browser-extension published (a second server with its
|
|
297
298
|
// own data dir or port would otherwise re-point the user's extension).
|
|
298
299
|
const extDir = installExtension();
|
|
299
300
|
if (await (0, auth_1.bundledPairingHeldByOther)(extDir, port)) {
|
|
300
|
-
(0, server_2.logErr)(`auto-pairing file at ${extDir} belongs to another running
|
|
301
|
+
(0, server_2.logErr)(`auto-pairing file at ${extDir} belongs to another running mcp-browser-extension — left as is; pair this server from the extension Options page if you want it`);
|
|
301
302
|
return;
|
|
302
303
|
}
|
|
303
304
|
const bundled = (0, auth_1.writeBundledPairing)(extDir, { port, token: pairToken });
|
|
@@ -349,8 +350,8 @@ async function main() {
|
|
|
349
350
|
policy: cfg.policy,
|
|
350
351
|
})}`);
|
|
351
352
|
const handshakePath = (0, datadir_1.handshakePath)(dataDir);
|
|
352
|
-
if (
|
|
353
|
-
(0, server_2.logErr)('token: pinned from
|
|
353
|
+
if ((0, compat_1.runtimeEnv)('TOKEN')) {
|
|
354
|
+
(0, server_2.logErr)('token: pinned from MCP_BROWSER_EXTENSION_TOKEN (stable; pair once, never again).');
|
|
354
355
|
}
|
|
355
356
|
else if (cfg.persistToken) {
|
|
356
357
|
(0, server_2.logErr)('token: persisted across restarts (--persist-token; pair once, never again).');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** New configuration names take precedence; existing installations still work. */
|
|
2
|
+
export declare function runtimeEnv(key: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
3
|
+
/** Expose the new path without moving files that a running browser still uses. */
|
|
4
|
+
export declare function renamedDirectory(home: string, name: string, legacyName: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runtimeEnv = runtimeEnv;
|
|
4
|
+
exports.renamedDirectory = renamedDirectory;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
/** New configuration names take precedence; existing installations still work. */
|
|
8
|
+
function runtimeEnv(key, env = process.env) {
|
|
9
|
+
return env[`MCP_BROWSER_EXTENSION_${key}`] ?? env[`CHROME_MCP_${key}`];
|
|
10
|
+
}
|
|
11
|
+
/** Expose the new path without moving files that a running browser still uses. */
|
|
12
|
+
function renamedDirectory(home, name, legacyName) {
|
|
13
|
+
const current = (0, node_path_1.join)(home, name);
|
|
14
|
+
const legacy = (0, node_path_1.join)(home, legacyName);
|
|
15
|
+
if ((0, node_fs_1.existsSync)(current) || !(0, node_fs_1.existsSync)(legacy))
|
|
16
|
+
return current;
|
|
17
|
+
try {
|
|
18
|
+
if (!(0, node_fs_1.statSync)(legacy).isDirectory())
|
|
19
|
+
return current;
|
|
20
|
+
(0, node_fs_1.symlinkSync)(legacy, current, process.platform === 'win32' ? 'junction' : 'dir');
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Another process may have created the alias, or the home may be read-only.
|
|
25
|
+
return (0, node_fs_1.existsSync)(current) ? current : legacy;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=compat.js.map
|
package/dist/src/config.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface CliConfig {
|
|
|
41
41
|
/** `--no-site-grants`: the allowlist is exactly what the flags say; the
|
|
42
42
|
* extension's Options page cannot add sites at runtime. */
|
|
43
43
|
noSiteGrants: boolean;
|
|
44
|
-
/** `--no-auto-update`: run this copy even when npm has a newer
|
|
44
|
+
/** `--no-auto-update`: run this copy even when npm has a newer mcp-browser-extension. */
|
|
45
45
|
noAutoUpdate: boolean;
|
|
46
46
|
/** `--no-native-host`: don't register the one-click pairing helper with Chrome. */
|
|
47
47
|
noNativeHost: boolean;
|
|
@@ -63,7 +63,7 @@ export interface CliConfig {
|
|
|
63
63
|
showVersion: boolean;
|
|
64
64
|
logLevel: LogLevel;
|
|
65
65
|
}
|
|
66
|
-
/** Resolve the data dir: `$
|
|
66
|
+
/** Resolve the data dir: `$MCP_BROWSER_EXTENSION_DATA` or `~/.mcp-browser-extension`. */
|
|
67
67
|
export declare function resolveDataDir(): string;
|
|
68
68
|
/**
|
|
69
69
|
* Reduce an arbitrary label to a single safe path segment: no separators, no
|
|
@@ -71,9 +71,9 @@ export declare function resolveDataDir(): string;
|
|
|
71
71
|
* the data dir, so they must never escape it.
|
|
72
72
|
*/
|
|
73
73
|
export declare function sanitizeName(name: string, kind: 'profile' | 'task'): string;
|
|
74
|
-
/** Active profile: `$
|
|
74
|
+
/** Active profile: `$MCP_BROWSER_EXTENSION_PROFILE` (set by `--profile`) or "default". */
|
|
75
75
|
export declare function resolveProfile(): string;
|
|
76
|
-
/** Active task: `$
|
|
76
|
+
/** Active task: `$MCP_BROWSER_EXTENSION_TASK` (set by `--task`) or "default". */
|
|
77
77
|
export declare function resolveTask(): string;
|
|
78
78
|
/** `profiles/<profile>/` — holds the Chrome user-data dir and this profile's tasks. */
|
|
79
79
|
export declare function resolveProfileDir(dataDir: string, profile: string): string;
|
package/dist/src/config.js
CHANGED
|
@@ -21,11 +21,12 @@ const node_path_1 = require("node:path");
|
|
|
21
21
|
const node_fs_1 = require("node:fs");
|
|
22
22
|
const protocol_1 = require("../shared/protocol");
|
|
23
23
|
const policy_1 = require("./security/policy");
|
|
24
|
+
const compat_1 = require("./compat");
|
|
24
25
|
/** Local port for `--http` (one above the bridge's). */
|
|
25
26
|
exports.DEFAULT_HTTP_PORT = 38018;
|
|
26
|
-
/** Resolve the data dir: `$
|
|
27
|
+
/** Resolve the data dir: `$MCP_BROWSER_EXTENSION_DATA` or `~/.mcp-browser-extension`. */
|
|
27
28
|
function resolveDataDir() {
|
|
28
|
-
return
|
|
29
|
+
return (0, compat_1.runtimeEnv)('DATA') ?? (0, compat_1.renamedDirectory)((0, node_os_1.homedir)(), '.mcp-browser-extension', '.chrome-mcp');
|
|
29
30
|
}
|
|
30
31
|
const DEFAULT_PROFILE = 'default';
|
|
31
32
|
const DEFAULT_TASK = 'default';
|
|
@@ -41,13 +42,13 @@ function sanitizeName(name, kind) {
|
|
|
41
42
|
}
|
|
42
43
|
return clean;
|
|
43
44
|
}
|
|
44
|
-
/** Active profile: `$
|
|
45
|
+
/** Active profile: `$MCP_BROWSER_EXTENSION_PROFILE` (set by `--profile`) or "default". */
|
|
45
46
|
function resolveProfile() {
|
|
46
|
-
return sanitizeName(
|
|
47
|
+
return sanitizeName((0, compat_1.runtimeEnv)('PROFILE') ?? DEFAULT_PROFILE, 'profile');
|
|
47
48
|
}
|
|
48
|
-
/** Active task: `$
|
|
49
|
+
/** Active task: `$MCP_BROWSER_EXTENSION_TASK` (set by `--task`) or "default". */
|
|
49
50
|
function resolveTask() {
|
|
50
|
-
return sanitizeName(
|
|
51
|
+
return sanitizeName((0, compat_1.runtimeEnv)('TASK') ?? DEFAULT_TASK, 'task');
|
|
51
52
|
}
|
|
52
53
|
/** `profiles/<profile>/` — holds the Chrome user-data dir and this profile's tasks. */
|
|
53
54
|
function resolveProfileDir(dataDir, profile) {
|
|
@@ -71,7 +72,7 @@ function readPolicyFile(path) {
|
|
|
71
72
|
* read triggered by `--policy <path>`.
|
|
72
73
|
*/
|
|
73
74
|
function parseArgs(argv) {
|
|
74
|
-
let wsPort = envInt('
|
|
75
|
+
let wsPort = envInt('MCP_BROWSER_EXTENSION_WS_PORT') ?? protocol_1.DEFAULT_WS_PORT;
|
|
75
76
|
// Extension-only by default: never launch/attach a Chromium of our own unless
|
|
76
77
|
// the operator explicitly opts in with --cdp-fallback (or --prefer cdp / --cdp-endpoint).
|
|
77
78
|
let cdpFallback = false;
|
|
@@ -85,7 +86,7 @@ function parseArgs(argv) {
|
|
|
85
86
|
let noAutoUpdate = false;
|
|
86
87
|
let noNativeHost = false;
|
|
87
88
|
let http = false;
|
|
88
|
-
let httpPort = envInt('
|
|
89
|
+
let httpPort = envInt('MCP_BROWSER_EXTENSION_HTTP_PORT') ?? exports.DEFAULT_HTTP_PORT;
|
|
89
90
|
let tunnel = false;
|
|
90
91
|
let rotateRemoteKey = false;
|
|
91
92
|
let showHelp = false;
|
|
@@ -117,13 +118,13 @@ function parseArgs(argv) {
|
|
|
117
118
|
wsPort = requireInt(argv[++i], '--port');
|
|
118
119
|
break;
|
|
119
120
|
case '--data-dir':
|
|
120
|
-
process.env.
|
|
121
|
+
process.env.MCP_BROWSER_EXTENSION_DATA = requireValue(argv[++i], '--data-dir');
|
|
121
122
|
break;
|
|
122
123
|
case '--profile':
|
|
123
|
-
process.env.
|
|
124
|
+
process.env.MCP_BROWSER_EXTENSION_PROFILE = requireValue(argv[++i], '--profile');
|
|
124
125
|
break;
|
|
125
126
|
case '--task':
|
|
126
|
-
process.env.
|
|
127
|
+
process.env.MCP_BROWSER_EXTENSION_TASK = requireValue(argv[++i], '--task');
|
|
127
128
|
break;
|
|
128
129
|
case '--policy':
|
|
129
130
|
policyFile = readPolicyFile(requireValue(argv[++i], '--policy'));
|
|
@@ -288,7 +289,7 @@ function parseArgs(argv) {
|
|
|
288
289
|
// Small parsing helpers
|
|
289
290
|
// ---------------------------------------------------------------------------
|
|
290
291
|
function envInt(name) {
|
|
291
|
-
const raw =
|
|
292
|
+
const raw = (0, compat_1.runtimeEnv)(name.replace(/^MCP_BROWSER_EXTENSION_/, ''));
|
|
292
293
|
if (raw === undefined)
|
|
293
294
|
return undefined;
|
|
294
295
|
const n = Number.parseInt(raw, 10);
|
|
@@ -325,17 +326,17 @@ function requireLogLevel(value) {
|
|
|
325
326
|
throw new Error('--log-level must be "silent", "info", or "debug"');
|
|
326
327
|
}
|
|
327
328
|
/** Help text for `--help`. */
|
|
328
|
-
exports.HELP_TEXT = `
|
|
329
|
+
exports.HELP_TEXT = `mcp-browser-extension — the MCP Browser Extension server: drive your real Chrome over MCP.
|
|
329
330
|
|
|
330
|
-
Usage:
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
331
|
+
Usage: mcp-browser-extension [options]
|
|
332
|
+
mcp-browser-extension extension doctor [--json] [--user-data-dir <browser directory>]
|
|
333
|
+
mcp-browser-extension extension repair [--apply] [--user-data-dir <browser directory>]
|
|
334
|
+
mcp-browser-extension tasks list [--json]
|
|
335
|
+
mcp-browser-extension tasks gc [--older-than <days>] [--keep <n>] [--profile <name>] [--dry-run]
|
|
335
336
|
|
|
336
337
|
Connection:
|
|
337
338
|
--port <n> WebSocket bridge port (default ${protocol_1.DEFAULT_WS_PORT}; 0 = ephemeral)
|
|
338
|
-
--data-dir <path> Override the data dir (default ~/.
|
|
339
|
+
--data-dir <path> Override the data dir (default ~/.mcp-browser-extension)
|
|
339
340
|
--profile <name> Default browser profile / identity (default "default").
|
|
340
341
|
Artifacts live under profiles/<name>/. At runtime, switch
|
|
341
342
|
with the profile_use tool. Several browsers can pair to the
|
|
@@ -347,31 +348,31 @@ Connection:
|
|
|
347
348
|
land in profiles/<profile>/tasks/<task>/.
|
|
348
349
|
--print-pairing Write the handshake, print its path, and keep the bridge
|
|
349
350
|
up until Ctrl-C (manual pairing; never serves MCP)
|
|
350
|
-
--extension-path Install the extension to ~/
|
|
351
|
-
|
|
351
|
+
--extension-path Install the extension to ~/mcp-browser-extension (or
|
|
352
|
+
MCP_BROWSER_EXTENSION_EXTENSION_DIR) and print that path — the folder
|
|
352
353
|
to pick in chrome://extensions -> Load unpacked. The server
|
|
353
354
|
also refreshes it on every boot.
|
|
354
355
|
--persist-token Reuse a stable on-disk token across restarts so the
|
|
355
356
|
extension never has to re-pair (default: fresh per boot).
|
|
356
|
-
|
|
357
|
+
MCP_BROWSER_EXTENSION_TOKEN env, if set, pins the token explicitly.
|
|
357
358
|
--no-native-host Don't register the helper behind the extension's one-click
|
|
358
359
|
Connect button (pairing is then by pasting port and token)
|
|
359
360
|
--http Serve MCP over HTTP on 127.0.0.1 instead of stdio, so a
|
|
360
361
|
client elsewhere can connect. Runs until Ctrl-C. Every
|
|
361
362
|
request needs the remote key (kept in <data-dir>/remote-key):
|
|
362
363
|
in the path, /mcp/<key>, or as Authorization: Bearer <key>.
|
|
363
|
-
--http-port <n> Port for --http (default 38018; env
|
|
364
|
+
--http-port <n> Port for --http (default 38018; env MCP_BROWSER_EXTENSION_HTTP_PORT)
|
|
364
365
|
--tunnel --http plus a Cloudflare quick tunnel, and print a public
|
|
365
366
|
connector URL for claude.ai, ChatGPT or any MCP client.
|
|
366
367
|
Needs cloudflared. The URL changes on every start.
|
|
367
368
|
--rotate-remote-key Make a new remote key: every remote client is cut off and
|
|
368
369
|
needs the new URL.
|
|
369
|
-
--no-auto-update Run this copy even when a newer
|
|
370
|
+
--no-auto-update Run this copy even when a newer mcp-browser-extension is published.
|
|
370
371
|
By default an installed copy (npx or global) checks npm at
|
|
371
372
|
startup and hands off to the latest release, so you never
|
|
372
|
-
run a stale version. Same as
|
|
373
|
+
run a stale version. Same as MCP_BROWSER_EXTENSION_AUTO_UPDATE=0.
|
|
373
374
|
--no-telemetry Send no anonymous usage statistics. Same as
|
|
374
|
-
|
|
375
|
+
MCP_BROWSER_EXTENSION_TELEMETRY=0 or DO_NOT_TRACK=1. Only counts are
|
|
375
376
|
ever sent (version, OS, tool calls, error codes) — never
|
|
376
377
|
URLs, page content or arguments.
|
|
377
378
|
|
|
@@ -4,10 +4,10 @@ exports.runExtensionCommand = runExtensionCommand;
|
|
|
4
4
|
const node_path_1 = require("node:path");
|
|
5
5
|
const config_1 = require("./config");
|
|
6
6
|
const extension_repair_1 = require("./extension-repair");
|
|
7
|
-
const HELP = `
|
|
7
|
+
const HELP = `mcp-browser-extension extension — diagnose an extension missing after a browser restart.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
mcp-browser-extension extension doctor [--json] [--user-data-dir <browser directory>]
|
|
10
|
+
mcp-browser-extension extension repair [--apply] [--json] [--user-data-dir <browser directory>]
|
|
11
11
|
[--data-dir <backup parent>]
|
|
12
12
|
|
|
13
13
|
doctor is read-only and reports the actual Store/unpacked registration.
|
|
@@ -6,23 +6,23 @@
|
|
|
6
6
|
* folders deep under `npm root -g`, and an npx run hides it in a cache. So on
|
|
7
7
|
* every boot (and on `--extension-path`) the server mirrors the bundled
|
|
8
8
|
* `extension-dist/` into a plain, visible folder directly under the home
|
|
9
|
-
* directory — `~/
|
|
9
|
+
* directory — `~/mcp-browser-extension` — and that is the folder the docs, the
|
|
10
10
|
* pairing file, and `--extension-path` all point at.
|
|
11
11
|
*
|
|
12
12
|
* The copy is a one-way mirror of top-level regular files, written only when
|
|
13
13
|
* content differs, so an unchanged boot touches nothing. It never deletes: the
|
|
14
14
|
* target may hold `pairing.json` (ours) and, if the operator pointed
|
|
15
|
-
*
|
|
15
|
+
* MCP_BROWSER_EXTENSION_EXTENSION_DIR somewhere of their own, files that are not ours to
|
|
16
16
|
* remove. It never throws — a read-only home just falls back to the bundled
|
|
17
17
|
* folder, which still works for Load unpacked.
|
|
18
18
|
*/
|
|
19
19
|
/** Folder name under the home directory. */
|
|
20
|
-
export declare const EXTENSION_DIR_NAME = "
|
|
20
|
+
export declare const EXTENSION_DIR_NAME = "mcp-browser-extension";
|
|
21
21
|
/** The auto-pairing file the server writes; never copied, never overwritten by the mirror. */
|
|
22
22
|
export declare const PAIRING_FILE = "pairing.json";
|
|
23
23
|
/** `dist/src/extension-install.js` -> `<pkg>/extension-dist`, which the tarball ships. */
|
|
24
24
|
export declare function bundledExtensionDir(): string;
|
|
25
|
-
/** Where the extension is mirrored for Load unpacked. `
|
|
25
|
+
/** Where the extension is mirrored for Load unpacked. `MCP_BROWSER_EXTENSION_EXTENSION_DIR` overrides. */
|
|
26
26
|
export declare function extensionInstallDir(): string;
|
|
27
27
|
export interface SyncResult {
|
|
28
28
|
/** The folder that now holds the extension (the target on success, the source on failure). */
|