@mehmoodqureshi/chrome-mcp 0.7.1 → 0.9.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/README.md +67 -15
- package/SETUP.md +201 -0
- package/dist/src/bridge/auth.d.ts +14 -0
- package/dist/src/bridge/auth.js +36 -0
- package/dist/src/cli.js +39 -0
- package/dist/src/config.d.ts +3 -1
- package/dist/src/config.js +11 -1
- package/dist/src/executor/select.d.ts +1 -1
- package/dist/src/executor/select.js +16 -2
- package/dist/src/extension-install.d.ts +42 -0
- package/dist/src/extension-install.js +86 -0
- package/extension-dist/background.js +27 -2
- package/extension-dist/manifest.json +1 -1
- package/extension-dist/options.html +4 -3
- package/extension-dist/options.js +9 -2
- package/package.json +5 -5
- package/scripts/postinstall.js +0 -56
package/README.md
CHANGED
|
@@ -29,6 +29,21 @@ Distributed as an `npx` CLI (the MCP server) plus a load-unpacked extension.
|
|
|
29
29
|
|
|
30
30
|
## Quickstart
|
|
31
31
|
|
|
32
|
+
### Up and running in one paste
|
|
33
|
+
|
|
34
|
+
Hand this to your AI agent (Claude Code, Cursor, Windsurf, anything MCP) and it
|
|
35
|
+
installs the server, wires it into the client, and walks you through the two
|
|
36
|
+
steps that must happen inside Chrome:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
Set up chrome-mcp on this machine by fetching and following
|
|
40
|
+
https://raw.githubusercontent.com/Mehmoodqureshi/chrome-mcp/main/SETUP.md
|
|
41
|
+
exactly, step by step. Work autonomously and verify each step.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Prefer to read before you run an agent on your machine? [`SETUP.md`](SETUP.md)
|
|
45
|
+
is the exact file the agent follows. The manual steps are below.
|
|
46
|
+
|
|
32
47
|
**1. Register the MCP server** with your host.
|
|
33
48
|
|
|
34
49
|
<details open>
|
|
@@ -93,21 +108,55 @@ explicitly (and is never written to disk).
|
|
|
93
108
|
|
|
94
109
|
**2. Load the extension** — **required**; the server can drive nothing without it.
|
|
95
110
|
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
The extension ships prebuilt inside the npm package, and every time the server
|
|
112
|
+
boots it copies it to a plain folder right under your home directory:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
~/chrome-mcp-extension (macOS / Linux)
|
|
116
|
+
%USERPROFILE%\chrome-mcp-extension (Windows)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
So after step 1 has started the server once (restart your client, or `/mcp` in
|
|
120
|
+
Claude Code), the folder is already there. To create it without a client, or
|
|
121
|
+
to print the exact path:
|
|
98
122
|
|
|
99
123
|
```bash
|
|
100
|
-
|
|
101
|
-
npm root -g # → <root>; the extension is at <root>/@mehmoodqureshi/chrome-mcp/extension-dist
|
|
124
|
+
npx -y @mehmoodqureshi/chrome-mcp --extension-path
|
|
102
125
|
```
|
|
103
126
|
|
|
104
127
|
Then `chrome://extensions` → enable **Developer mode** → **Load unpacked** →
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
pick `chrome-mcp-extension` in your home folder. After upgrading the package the
|
|
129
|
+
server refreshes the files on its next boot; click **Reload** on
|
|
130
|
+
`chrome://extensions` to pick them up. `CHROME_MCP_EXTENSION_DIR` moves the
|
|
131
|
+
folder somewhere else. (Working from a git clone instead? Run
|
|
132
|
+
`npm install && npm run build:ext` first — `extension-dist/` is gitignored, and
|
|
133
|
+
the server mirrors it to the same home folder.)
|
|
134
|
+
|
|
135
|
+
**3. Pair it — usually nothing to do.** Every time the server boots it writes
|
|
136
|
+
`pairing.json` (mode 0600, never shipped in the tarball) into the very
|
|
137
|
+
`chrome-mcp-extension` folder you just loaded. The extension reads that file
|
|
138
|
+
from its own folder on startup and pairs itself, so the toolbar badge turns
|
|
139
|
+
green with no token to paste. Load the extension before the server has ever
|
|
140
|
+
run? It re-checks every 30 seconds and pairs as soon as the file appears.
|
|
141
|
+
|
|
142
|
+
**Where to see the badge:** it sits on the extension's icon in Chrome's
|
|
143
|
+
toolbar, not on the `chrome://extensions` page. Chrome hides new extensions
|
|
144
|
+
behind the puzzle-piece button at the right of the address bar, so click that,
|
|
145
|
+
find **Chrome MCP Bridge**, and click the pin next to it once; the icon then
|
|
146
|
+
stays in the toolbar. Hover it for the status in words.
|
|
147
|
+
|
|
148
|
+
| Badge | Meaning |
|
|
149
|
+
|---|---|
|
|
150
|
+
| green dot | paired and connected |
|
|
151
|
+
| yellow dots | connecting |
|
|
152
|
+
| grey circle | not paired yet (no server has run, or no pairing file) |
|
|
153
|
+
| red exclamation mark | token rejected; the server rotated it, re-pairs by itself in a moment |
|
|
154
|
+
|
|
155
|
+
Manual fallback (a copied folder, a read-only home): run
|
|
156
|
+
`npx -y @mehmoodqureshi/chrome-mcp --print-pairing`, open the extension's
|
|
157
|
+
**Options** page, and paste the `port` + `token` from
|
|
158
|
+
`~/.chrome-mcp/handshake.json`. Values saved there take precedence over the
|
|
159
|
+
bundled file.
|
|
111
160
|
|
|
112
161
|
### Running more than one session
|
|
113
162
|
|
|
@@ -155,7 +204,7 @@ start. Wrap it in `cmd /c`:
|
|
|
155
204
|
|
|
156
205
|
Or from Claude Code: `claude mcp add chrome-mcp -- cmd /c npx -y @mehmoodqureshi/chrome-mcp --allow-domain example.com`
|
|
157
206
|
|
|
158
|
-
Everything else is the same — load
|
|
207
|
+
Everything else is the same — load `%USERPROFILE%\chrome-mcp-extension` and pair
|
|
159
208
|
as above.
|
|
160
209
|
|
|
161
210
|
The tools cover tabs, navigation, interaction (`click`/`type`/`press`/`hover`/
|
|
@@ -400,10 +449,13 @@ RUN_EXT_SMOKE=1 node --test dist/test/extension-smoke.test.js # live, headed
|
|
|
400
449
|
## The extension
|
|
401
450
|
|
|
402
451
|
`extension/` builds (esbuild) to `extension-dist/`, loaded via
|
|
403
|
-
`chrome://extensions` → **Load unpacked** → select
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
452
|
+
`chrome://extensions` → **Load unpacked** → select `~/chrome-mcp-extension`, the
|
|
453
|
+
mirror the server refreshes from `extension-dist/` on every boot (loading
|
|
454
|
+
`extension-dist/` directly also works). It pairs itself from the `pairing.json`
|
|
455
|
+
the server writes into that folder; the **Options** page paste of `port` +
|
|
456
|
+
`token` from `~/.chrome-mcp/handshake.json` (run
|
|
457
|
+
`npx -y @mehmoodqureshi/chrome-mcp --print-pairing` to get the path) is only
|
|
458
|
+
the fallback.
|
|
407
459
|
|
|
408
460
|
> **Reads/interaction use `chrome.scripting`/`chrome.tabs`** — no "is being
|
|
409
461
|
> debugged" banner, CSP-safe reads (isolated world), testable under Playwright.
|
package/SETUP.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# chrome-mcp — agent setup instructions
|
|
2
|
+
|
|
3
|
+
**You are an AI agent. Follow these steps to set up chrome-mcp end-to-end on this
|
|
4
|
+
machine and prove it works.** chrome-mcp lets an MCP client drive the human's
|
|
5
|
+
real Chrome browser: real logins, real cookies, no separate Chromium. It has two
|
|
6
|
+
halves that must be paired: a stdio MCP server (npm package
|
|
7
|
+
`@mehmoodqureshi/chrome-mcp`) and a Manifest V3 extension that ships inside the
|
|
8
|
+
same package. Work autonomously; pause only for the two actions that must happen
|
|
9
|
+
inside Chrome's own UI (loading the extension, pasting the pairing token).
|
|
10
|
+
|
|
11
|
+
## Ground rules
|
|
12
|
+
|
|
13
|
+
- Verify each step's result before moving on. If a command fails, diagnose and
|
|
14
|
+
fix it. Do not proceed on a broken step.
|
|
15
|
+
- The server is **deny-all by default**: no domains, no clicks, no eval, no
|
|
16
|
+
downloads, no uploads. Ask the human which domains they want to automate and
|
|
17
|
+
grant only those with `--allow-domain`. Never reach for `--unsafe-all-domains`
|
|
18
|
+
or `--unsafe-enable-eval` unless the human asks for them by name.
|
|
19
|
+
- Never print or log the pairing token. It lives in a 0600 file; show the human
|
|
20
|
+
its path, not its contents.
|
|
21
|
+
- Requires Node 18 or newer (`node --version`) and Chrome 116 or newer.
|
|
22
|
+
- On Windows, MCP hosts must launch the server through `cmd /c npx ...`. See
|
|
23
|
+
step 3.
|
|
24
|
+
|
|
25
|
+
## Step 1 — Confirm the environment
|
|
26
|
+
|
|
27
|
+
Run and record the output:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
node --version
|
|
31
|
+
npm --version
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If Node is below 18, stop and tell the human to upgrade before anything else.
|
|
35
|
+
|
|
36
|
+
## Step 2 — Install the extension folder
|
|
37
|
+
|
|
38
|
+
No global install is needed. This one command fetches the package, copies the
|
|
39
|
+
extension to a plain folder directly under the home directory, and prints its
|
|
40
|
+
path:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
npx -y @mehmoodqureshi/chrome-mcp --extension-path
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
It prints `~/chrome-mcp-extension` (on Windows `%USERPROFILE%\chrome-mcp-extension`).
|
|
47
|
+
Verify with `ls` (or `dir`) that the folder holds `manifest.json` and
|
|
48
|
+
`background.js`. Record the path; the human needs it in step 4. The install is
|
|
49
|
+
small and downloads no browser: the server drives the Chrome the human already
|
|
50
|
+
has. The server refreshes this folder on every boot, so upgrades need no
|
|
51
|
+
re-copy, only a Reload on `chrome://extensions`.
|
|
52
|
+
|
|
53
|
+
## Step 3 — Wire the server into this MCP client
|
|
54
|
+
|
|
55
|
+
Ask the human which domains they want to automate. Build the argument list from
|
|
56
|
+
their answer: one `--allow-domain <glob>` per domain, plus `--enable-mutations`
|
|
57
|
+
if they want the agent to click, type, and navigate (almost always yes). Add
|
|
58
|
+
`--persist-token` so the pairing survives restarts. Leave every other gate off
|
|
59
|
+
unless they ask.
|
|
60
|
+
|
|
61
|
+
**Claude Code** (registers for every project on this machine):
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
claude mcp add chrome-mcp -s user -- \
|
|
65
|
+
npx -y @mehmoodqureshi/chrome-mcp \
|
|
66
|
+
--allow-domain example.com --enable-mutations --persist-token
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Everything before `--` belongs to Claude Code. Everything after it is the
|
|
70
|
+
server's own command line. Keep the `--` or `--allow-domain` gets read as a
|
|
71
|
+
Claude Code option. Verify with `claude mcp list`: chrome-mcp should be listed.
|
|
72
|
+
It will show as connected once the server boots, even before the extension has
|
|
73
|
+
paired.
|
|
74
|
+
|
|
75
|
+
**Claude Desktop, Cursor, Windsurf, VS Code, and other JSON-configured hosts:**
|
|
76
|
+
add this to the host's MCP config file (the exact path differs per client; VS
|
|
77
|
+
Code uses a top-level `servers` key instead of `mcpServers`).
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"chrome-mcp": {
|
|
83
|
+
"command": "npx",
|
|
84
|
+
"args": ["-y", "@mehmoodqureshi/chrome-mcp",
|
|
85
|
+
"--allow-domain", "example.com",
|
|
86
|
+
"--enable-mutations",
|
|
87
|
+
"--persist-token"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Windows:** the command must be `cmd` with args `["/c", "npx", "-y",
|
|
94
|
+
"@mehmoodqureshi/chrome-mcp", ...]`, or for Claude Code:
|
|
95
|
+
`claude mcp add chrome-mcp -s user -- cmd /c npx -y @mehmoodqureshi/chrome-mcp --allow-domain example.com --enable-mutations --persist-token`.
|
|
96
|
+
|
|
97
|
+
Tell the human to restart the client (or run `/mcp` in Claude Code) so the
|
|
98
|
+
server loads. The first boot writes `~/.chrome-mcp/handshake.json` (mode 0600)
|
|
99
|
+
holding the bridge port and the pairing token, and with `--persist-token` it
|
|
100
|
+
also stores the token at `~/.chrome-mcp/token` for reuse.
|
|
101
|
+
|
|
102
|
+
## Step 4 — Load the extension (human action inside Chrome)
|
|
103
|
+
|
|
104
|
+
You cannot do this step yourself. Give the human these exact instructions:
|
|
105
|
+
|
|
106
|
+
1. Open `chrome://extensions` in Chrome.
|
|
107
|
+
2. Turn on **Developer mode** (top right).
|
|
108
|
+
3. Click **Load unpacked** and choose the `chrome-mcp-extension` folder in
|
|
109
|
+
their home directory (the path from step 2).
|
|
110
|
+
4. Confirm an extension named **Chrome MCP Bridge** now appears in the list.
|
|
111
|
+
|
|
112
|
+
Wait for the human to confirm before continuing.
|
|
113
|
+
|
|
114
|
+
## Step 5 — Pairing (automatic; verify it)
|
|
115
|
+
|
|
116
|
+
Pairing needs no paste. Every time the server boots it writes `pairing.json`
|
|
117
|
+
(mode 0600) into the same `chrome-mcp-extension` folder the human loaded in step 4.
|
|
118
|
+
The extension reads that file from its own folder and pairs itself. So make
|
|
119
|
+
sure a server has booted at least once since step 2, in either of these ways:
|
|
120
|
+
|
|
121
|
+
- the client started one in step 3 (a restart or `/mcp` reconnect is enough), or
|
|
122
|
+
- start one yourself in pairing mode and leave it running in the background:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
npx -y @mehmoodqureshi/chrome-mcp --print-pairing --persist-token
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Verify the file exists (do not print it):
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
ls "$(npx -y @mehmoodqureshi/chrome-mcp --extension-path)/pairing.json"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Then tell the human where to look. The status badge is on the extension's
|
|
135
|
+
icon in Chrome's toolbar, not on the `chrome://extensions` page. Chrome hides
|
|
136
|
+
new extensions behind the puzzle-piece button at the right of the address bar,
|
|
137
|
+
so give them these exact steps:
|
|
138
|
+
|
|
139
|
+
1. Click the puzzle-piece button at the right of the address bar.
|
|
140
|
+
2. Find **Chrome MCP Bridge** in the list and click the pin icon next to it.
|
|
141
|
+
The extension icon now stays in the toolbar.
|
|
142
|
+
3. Look at the small badge on that icon. Hovering it shows the status in words.
|
|
143
|
+
|
|
144
|
+
Badge meanings: a green dot means paired and connected. Yellow dots mean
|
|
145
|
+
connecting. A grey circle means not paired yet, usually because no server has
|
|
146
|
+
run since the extension was loaded. A red exclamation mark means the token was
|
|
147
|
+
rejected, which the extension fixes by itself within a few seconds by re-reading
|
|
148
|
+
the pairing file.
|
|
149
|
+
|
|
150
|
+
A green dot can take up to 30 seconds if the extension was loaded before the
|
|
151
|
+
server first ran. Wait for the human to confirm the green dot.
|
|
152
|
+
|
|
153
|
+
**Manual fallback**, only if the badge stays grey after a minute (a copied
|
|
154
|
+
extension folder or a read-only global install): read the port without
|
|
155
|
+
printing the token,
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
node -e "console.log(require(require('os').homedir()+'/.chrome-mcp/handshake.json').port)"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
then have the human open **Details** on Chrome MCP Bridge, then **Extension
|
|
162
|
+
options**, enter that **Port**, open `~/.chrome-mcp/handshake.json` in a text
|
|
163
|
+
editor and paste its `token` into the **Token** field, leave **Profile** as
|
|
164
|
+
`default`, and click **Save**. The status line should read **connected** within
|
|
165
|
+
a few seconds. Values saved by hand take precedence over the bundled file.
|
|
166
|
+
|
|
167
|
+
If you started a pairing-mode server in this step, stop it now with Ctrl-C. The
|
|
168
|
+
client's own server takes the port over on its next boot, and with
|
|
169
|
+
`--persist-token` the extension reconnects to it with no further pairing.
|
|
170
|
+
|
|
171
|
+
## Step 6 — Prove the chain end-to-end
|
|
172
|
+
|
|
173
|
+
From the restarted client session, call the MCP tools in this order and check
|
|
174
|
+
each result:
|
|
175
|
+
|
|
176
|
+
1. `chrome_status` — reports the backend as the extension and the session as
|
|
177
|
+
connected.
|
|
178
|
+
2. `tabs_list` — returns at least one tab from the human's real Chrome.
|
|
179
|
+
3. `navigate` to a URL on one of the allowlisted domains, then `snapshot` —
|
|
180
|
+
returns interactive elements with `ref` ids.
|
|
181
|
+
4. `navigate` to a domain that is **not** allowlisted — must be refused with a
|
|
182
|
+
policy error. This confirms deny-all is working, which matters more than the
|
|
183
|
+
happy path.
|
|
184
|
+
|
|
185
|
+
If `chrome_status` shows the extension disconnected, the pairing in step 5 did
|
|
186
|
+
not stick. Re-check the port and token before anything else.
|
|
187
|
+
|
|
188
|
+
## Step 7 — Report
|
|
189
|
+
|
|
190
|
+
Summarize for the human:
|
|
191
|
+
|
|
192
|
+
- Node and package versions installed.
|
|
193
|
+
- The absolute `chrome-mcp-extension` path they loaded.
|
|
194
|
+
- Which MCP client was configured, at which scope, with which domains and gates.
|
|
195
|
+
- Whether the badge went green, and whether `tabs_list`, an allowed
|
|
196
|
+
navigation, and a refused navigation each verified.
|
|
197
|
+
- Anything still open on their side, such as restarting the client.
|
|
198
|
+
|
|
199
|
+
For any failure, name the exact symptom, what you tried, and the matching
|
|
200
|
+
section of the README's troubleshooting notes at
|
|
201
|
+
`https://github.com/Mehmoodqureshi/chrome-mcp#readme`.
|
|
@@ -42,6 +42,20 @@ export interface WriteHandshakeFields {
|
|
|
42
42
|
token: string;
|
|
43
43
|
expectedExtensionId?: string;
|
|
44
44
|
}
|
|
45
|
+
/** Name of the auto-pairing file the server drops into its bundled extension folder. */
|
|
46
|
+
export declare const BUNDLED_PAIRING_FILE = "pairing.json";
|
|
47
|
+
/**
|
|
48
|
+
* Write `<extDir>/pairing.json` so an extension loaded unpacked from that very
|
|
49
|
+
* folder can pair itself: its service worker fetches the file from its own
|
|
50
|
+
* package and adopts the port + token with no Options-page paste. Same secret,
|
|
51
|
+
* same 0600 mode, same trust boundary as the handshake (only this user can read
|
|
52
|
+
* it, and the file is neither web-accessible nor shipped in the npm tarball).
|
|
53
|
+
*
|
|
54
|
+
* Best-effort: returns the path on success, or null when the folder is missing
|
|
55
|
+
* or read-only (a locked-down global install). Never throws — manual pairing
|
|
56
|
+
* still works without it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function writeBundledPairing(extDir: string, fields: WriteHandshakeFields): string | null;
|
|
45
59
|
/**
|
|
46
60
|
* Atomically write the handshake at 0600 and verify the mode. Throws (fail
|
|
47
61
|
* closed) if the file ends up group/other-readable — the token is the entire
|
package/dist/src/bridge/auth.js
CHANGED
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
* - The token is NEVER written to stdout/stderr or any log (a test asserts it).
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.BUNDLED_PAIRING_FILE = void 0;
|
|
17
18
|
exports.generateToken = generateToken;
|
|
18
19
|
exports.tokenPath = tokenPath;
|
|
19
20
|
exports.readPersistedToken = readPersistedToken;
|
|
20
21
|
exports.writePersistedToken = writePersistedToken;
|
|
21
22
|
exports.resolveToken = resolveToken;
|
|
22
23
|
exports.tokensMatch = tokensMatch;
|
|
24
|
+
exports.writeBundledPairing = writeBundledPairing;
|
|
23
25
|
exports.writeHandshake = writeHandshake;
|
|
24
26
|
exports.readHandshake = readHandshake;
|
|
25
27
|
exports.removeHandshake = removeHandshake;
|
|
@@ -110,6 +112,40 @@ function tokensMatch(a, b) {
|
|
|
110
112
|
const hb = (0, node_crypto_1.createHash)('sha256').update(b, 'utf8').digest();
|
|
111
113
|
return (0, node_crypto_1.timingSafeEqual)(ha, hb);
|
|
112
114
|
}
|
|
115
|
+
/** Name of the auto-pairing file the server drops into its bundled extension folder. */
|
|
116
|
+
exports.BUNDLED_PAIRING_FILE = 'pairing.json';
|
|
117
|
+
/**
|
|
118
|
+
* Write `<extDir>/pairing.json` so an extension loaded unpacked from that very
|
|
119
|
+
* folder can pair itself: its service worker fetches the file from its own
|
|
120
|
+
* package and adopts the port + token with no Options-page paste. Same secret,
|
|
121
|
+
* same 0600 mode, same trust boundary as the handshake (only this user can read
|
|
122
|
+
* it, and the file is neither web-accessible nor shipped in the npm tarball).
|
|
123
|
+
*
|
|
124
|
+
* Best-effort: returns the path on success, or null when the folder is missing
|
|
125
|
+
* or read-only (a locked-down global install). Never throws — manual pairing
|
|
126
|
+
* still works without it.
|
|
127
|
+
*/
|
|
128
|
+
function writeBundledPairing(extDir, fields) {
|
|
129
|
+
const path = (0, node_path_1.join)(extDir, exports.BUNDLED_PAIRING_FILE);
|
|
130
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
131
|
+
const payload = { v: protocol_1.PROTOCOL_VERSION, port: fields.port, token: fields.token, ts: Date.now() };
|
|
132
|
+
try {
|
|
133
|
+
(0, node_fs_1.writeFileSync)(tmp, JSON.stringify(payload), { mode: 0o600 });
|
|
134
|
+
(0, node_fs_1.chmodSync)(tmp, 0o600);
|
|
135
|
+
(0, node_fs_1.renameSync)(tmp, path);
|
|
136
|
+
(0, node_fs_1.chmodSync)(path, 0o600);
|
|
137
|
+
return path;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
try {
|
|
141
|
+
(0, node_fs_1.unlinkSync)(tmp);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
/* nothing to clean */
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
113
149
|
/**
|
|
114
150
|
* Atomically write the handshake at 0600 and verify the mode. Throws (fail
|
|
115
151
|
* closed) if the file ends up group/other-readable — the token is the entire
|
package/dist/src/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ const datadir_1 = require("./bridge/datadir");
|
|
|
21
21
|
const workspace_1 = require("./bridge/workspace");
|
|
22
22
|
const auth_1 = require("./bridge/auth");
|
|
23
23
|
const server_2 = require("./mcp/server");
|
|
24
|
+
const extension_install_1 = require("./extension-install");
|
|
24
25
|
/** Hard deadline for clean shutdown before we force-exit (a stuck socket must not hang us). */
|
|
25
26
|
const SHUTDOWN_DEADLINE_MS = 3000;
|
|
26
27
|
/**
|
|
@@ -51,6 +52,25 @@ function version() {
|
|
|
51
52
|
return '0.0.0';
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Mirror the bundled extension into the visible install folder
|
|
57
|
+
* (`~/chrome-mcp-extension`) and report where Chrome should load it from. On a
|
|
58
|
+
* read-only home the bundled folder is returned instead; both are loadable.
|
|
59
|
+
*/
|
|
60
|
+
function installExtension() {
|
|
61
|
+
const r = (0, extension_install_1.syncExtension)();
|
|
62
|
+
if (!r.ok) {
|
|
63
|
+
(0, server_2.logDebug)(`extension not mirrored to the home folder (${r.error}); using the bundled copy at ${r.dir}`);
|
|
64
|
+
return r.dir;
|
|
65
|
+
}
|
|
66
|
+
if (r.created) {
|
|
67
|
+
(0, server_2.logErr)(`extension installed at ${r.dir} — chrome://extensions -> Load unpacked -> pick that folder`);
|
|
68
|
+
}
|
|
69
|
+
else if (r.copied.length > 0) {
|
|
70
|
+
(0, server_2.logErr)(`extension files updated at ${r.dir} — click Reload on chrome://extensions to pick up the new version`);
|
|
71
|
+
}
|
|
72
|
+
return r.dir;
|
|
73
|
+
}
|
|
54
74
|
/** Render a byte count as a short human string (1.2 MB, 904 KB, …). */
|
|
55
75
|
function humanBytes(n) {
|
|
56
76
|
if (n < 1024)
|
|
@@ -154,6 +174,10 @@ async function main() {
|
|
|
154
174
|
process.stdout.write(`${version()}\n`);
|
|
155
175
|
return;
|
|
156
176
|
}
|
|
177
|
+
if (cfg.showExtensionPath) {
|
|
178
|
+
process.stdout.write(`${installExtension()}\n`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
157
181
|
const dataDir = (0, datadir_1.ensureDataDir)(cfg.dataDir);
|
|
158
182
|
const token = (0, auth_1.resolveToken)(dataDir, { persist: cfg.persistToken });
|
|
159
183
|
const { allowDomains, allowEval, allowDownloads, allowUploads, allowAllTabs, enableMutations } = cfg.policy;
|
|
@@ -179,6 +203,21 @@ async function main() {
|
|
|
179
203
|
})}`);
|
|
180
204
|
const handshakePath = (0, auth_1.writeHandshake)(dataDir, { port, token });
|
|
181
205
|
(0, server_2.logErr)(`pairing handshake written to ${handshakePath} (mode 0600; token not logged)`);
|
|
206
|
+
// Drop the same port + token into the bundled extension folder so a Load
|
|
207
|
+
// unpacked from there pairs itself. Best-effort: a read-only install just
|
|
208
|
+
// falls back to the Options-page paste.
|
|
209
|
+
const extDir = installExtension();
|
|
210
|
+
const bundled = (0, auth_1.writeBundledPairing)(extDir, { port, token });
|
|
211
|
+
if (bundled) {
|
|
212
|
+
(0, server_2.logErr)(`auto-pairing file written to ${bundled} — Load unpacked from ${extDir} needs no token paste`);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
(0, server_2.logDebug)(`auto-pairing file not written (extension folder missing or read-only at ${extDir})`);
|
|
216
|
+
}
|
|
217
|
+
// Anyone who loaded the extension straight from the package folder (0.8.0
|
|
218
|
+
// docs) keeps pairing too.
|
|
219
|
+
if (extDir !== (0, extension_install_1.bundledExtensionDir)())
|
|
220
|
+
(0, auth_1.writeBundledPairing)((0, extension_install_1.bundledExtensionDir)(), { port, token });
|
|
182
221
|
if (process.env.CHROME_MCP_TOKEN) {
|
|
183
222
|
(0, server_2.logErr)('token: pinned from CHROME_MCP_TOKEN (stable; pair once, never again).');
|
|
184
223
|
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -28,8 +28,10 @@ export interface CliConfig {
|
|
|
28
28
|
prefer: BackendPreference;
|
|
29
29
|
/** Run the CDP-fallback Chromium headless. */
|
|
30
30
|
headless: boolean;
|
|
31
|
-
/** `--print-pairing`: write the handshake
|
|
31
|
+
/** `--print-pairing`: write the handshake, print its path (never the token), and keep the bridge up until Ctrl-C. */
|
|
32
32
|
printPairing: boolean;
|
|
33
|
+
/** `--extension-path`: print the absolute path of the bundled extension (for Load unpacked) and exit. */
|
|
34
|
+
showExtensionPath: boolean;
|
|
33
35
|
/** `--persist-token`: reuse a stable on-disk token so the extension never re-pairs. */
|
|
34
36
|
persistToken: boolean;
|
|
35
37
|
showHelp: boolean;
|
package/dist/src/config.js
CHANGED
|
@@ -80,6 +80,7 @@ function parseArgs(argv) {
|
|
|
80
80
|
let persistToken = false;
|
|
81
81
|
let showHelp = false;
|
|
82
82
|
let showVersion = false;
|
|
83
|
+
let showExtensionPath = false;
|
|
83
84
|
let logLevel = 'info';
|
|
84
85
|
// Policy assembled from flags, layered over an optional file.
|
|
85
86
|
let policyFile;
|
|
@@ -95,6 +96,9 @@ function parseArgs(argv) {
|
|
|
95
96
|
case '--version':
|
|
96
97
|
showVersion = true;
|
|
97
98
|
break;
|
|
99
|
+
case '--extension-path':
|
|
100
|
+
showExtensionPath = true;
|
|
101
|
+
break;
|
|
98
102
|
case '--port':
|
|
99
103
|
wsPort = requireInt(argv[++i], '--port');
|
|
100
104
|
break;
|
|
@@ -214,6 +218,7 @@ function parseArgs(argv) {
|
|
|
214
218
|
persistToken,
|
|
215
219
|
showHelp,
|
|
216
220
|
showVersion,
|
|
221
|
+
showExtensionPath,
|
|
217
222
|
logLevel,
|
|
218
223
|
};
|
|
219
224
|
}
|
|
@@ -267,7 +272,12 @@ Connection:
|
|
|
267
272
|
the extension Options; tools route to the active profile.
|
|
268
273
|
--task <name> Task label (default "default"). Downloads and a meta.json
|
|
269
274
|
land in profiles/<profile>/tasks/<task>/.
|
|
270
|
-
--print-pairing Write the handshake
|
|
275
|
+
--print-pairing Write the handshake, print its path, and keep the bridge
|
|
276
|
+
up until Ctrl-C (manual pairing; never serves MCP)
|
|
277
|
+
--extension-path Install the extension to ~/chrome-mcp-extension (or
|
|
278
|
+
CHROME_MCP_EXTENSION_DIR) and print that path — the folder
|
|
279
|
+
to pick in chrome://extensions -> Load unpacked. The server
|
|
280
|
+
also refreshes it on every boot.
|
|
271
281
|
--persist-token Reuse a stable on-disk token across restarts so the
|
|
272
282
|
extension never has to re-pair (default: fresh per boot).
|
|
273
283
|
CHROME_MCP_TOKEN env, if set, pins the token explicitly.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* extension and CDP executors are cached across calls; only the choice is live.
|
|
8
8
|
*/
|
|
9
9
|
import { type Executor } from './types';
|
|
10
|
-
import {
|
|
10
|
+
import type { CdpOptions } from './cdp-executor';
|
|
11
11
|
import type { BridgeServer } from '../bridge/server';
|
|
12
12
|
import type { BackendPreference } from '../config';
|
|
13
13
|
export interface SelectorDeps {
|
|
@@ -11,10 +11,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
11
11
|
exports.createSelector = createSelector;
|
|
12
12
|
const types_1 = require("./types");
|
|
13
13
|
const extension_executor_1 = require("./extension-executor");
|
|
14
|
-
const cdp_executor_1 = require("./cdp-executor");
|
|
15
14
|
function createSelector(deps) {
|
|
16
15
|
const makeExt = deps.makeExtension ?? ((b) => new extension_executor_1.ExtensionExecutor(b));
|
|
17
|
-
const makeCdp = deps.makeCdp ??
|
|
16
|
+
const makeCdp = deps.makeCdp ??
|
|
17
|
+
((o) => {
|
|
18
|
+
// Loaded lazily: playwright is a devDependency (tests + HITL only). The
|
|
19
|
+
// published CLI is extension-only, so a fresh install never pulls a
|
|
20
|
+
// browser. Reaching this without playwright present is a config error.
|
|
21
|
+
let mod;
|
|
22
|
+
try {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
|
+
mod = require('./cdp-executor');
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
28
|
+
throw new types_1.ExecutorError('NO_BACKEND', `CDP backend requested but this build is extension-only (playwright is not installed: ${why}). Pair the extension instead.`);
|
|
29
|
+
}
|
|
30
|
+
return new mod.CdpExecutor(o);
|
|
31
|
+
});
|
|
18
32
|
const pingMs = deps.pingDeadlineMs ?? 800;
|
|
19
33
|
const pingCacheMs = deps.pingCacheMs ?? 2_000;
|
|
20
34
|
const ext = makeExt(deps.bridge);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/extension-install.ts — put the extension somewhere a human can find.
|
|
3
|
+
*
|
|
4
|
+
* The extension ships inside the npm package, which is the wrong place to send
|
|
5
|
+
* someone with a "Load unpacked" file dialog: a global install buries it five
|
|
6
|
+
* folders deep under `npm root -g`, and an npx run hides it in a cache. So on
|
|
7
|
+
* every boot (and on `--extension-path`) the server mirrors the bundled
|
|
8
|
+
* `extension-dist/` into a plain, visible folder directly under the home
|
|
9
|
+
* directory — `~/chrome-mcp-extension` — and that is the folder the docs, the
|
|
10
|
+
* pairing file, and `--extension-path` all point at.
|
|
11
|
+
*
|
|
12
|
+
* The copy is a one-way mirror of top-level regular files, written only when
|
|
13
|
+
* content differs, so an unchanged boot touches nothing. It never deletes: the
|
|
14
|
+
* target may hold `pairing.json` (ours) and, if the operator pointed
|
|
15
|
+
* CHROME_MCP_EXTENSION_DIR somewhere of their own, files that are not ours to
|
|
16
|
+
* remove. It never throws — a read-only home just falls back to the bundled
|
|
17
|
+
* folder, which still works for Load unpacked.
|
|
18
|
+
*/
|
|
19
|
+
/** Folder name under the home directory. */
|
|
20
|
+
export declare const EXTENSION_DIR_NAME = "chrome-mcp-extension";
|
|
21
|
+
/** The auto-pairing file the server writes; never copied, never overwritten by the mirror. */
|
|
22
|
+
export declare const PAIRING_FILE = "pairing.json";
|
|
23
|
+
/** `dist/src/extension-install.js` -> `<pkg>/extension-dist`, which the tarball ships. */
|
|
24
|
+
export declare function bundledExtensionDir(): string;
|
|
25
|
+
/** Where the extension is mirrored for Load unpacked. `CHROME_MCP_EXTENSION_DIR` overrides. */
|
|
26
|
+
export declare function extensionInstallDir(): string;
|
|
27
|
+
export interface SyncResult {
|
|
28
|
+
/** The folder that now holds the extension (the target on success, the source on failure). */
|
|
29
|
+
dir: string;
|
|
30
|
+
/** Files written this run (new or changed). Empty means the target was already current. */
|
|
31
|
+
copied: string[];
|
|
32
|
+
/** True when the target folder did not exist before this run (first install). */
|
|
33
|
+
created: boolean;
|
|
34
|
+
ok: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Mirror the bundled extension into the install dir. Returns the folder to
|
|
39
|
+
* point Chrome at either way: the target when the mirror succeeded, the source
|
|
40
|
+
* when it did not (so callers always have a loadable folder).
|
|
41
|
+
*/
|
|
42
|
+
export declare function syncExtension(from?: string, to?: string): SyncResult;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/extension-install.ts — put the extension somewhere a human can find.
|
|
4
|
+
*
|
|
5
|
+
* The extension ships inside the npm package, which is the wrong place to send
|
|
6
|
+
* someone with a "Load unpacked" file dialog: a global install buries it five
|
|
7
|
+
* folders deep under `npm root -g`, and an npx run hides it in a cache. So on
|
|
8
|
+
* every boot (and on `--extension-path`) the server mirrors the bundled
|
|
9
|
+
* `extension-dist/` into a plain, visible folder directly under the home
|
|
10
|
+
* directory — `~/chrome-mcp-extension` — and that is the folder the docs, the
|
|
11
|
+
* pairing file, and `--extension-path` all point at.
|
|
12
|
+
*
|
|
13
|
+
* The copy is a one-way mirror of top-level regular files, written only when
|
|
14
|
+
* content differs, so an unchanged boot touches nothing. It never deletes: the
|
|
15
|
+
* target may hold `pairing.json` (ours) and, if the operator pointed
|
|
16
|
+
* CHROME_MCP_EXTENSION_DIR somewhere of their own, files that are not ours to
|
|
17
|
+
* remove. It never throws — a read-only home just falls back to the bundled
|
|
18
|
+
* folder, which still works for Load unpacked.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.PAIRING_FILE = exports.EXTENSION_DIR_NAME = void 0;
|
|
22
|
+
exports.bundledExtensionDir = bundledExtensionDir;
|
|
23
|
+
exports.extensionInstallDir = extensionInstallDir;
|
|
24
|
+
exports.syncExtension = syncExtension;
|
|
25
|
+
const node_os_1 = require("node:os");
|
|
26
|
+
const node_path_1 = require("node:path");
|
|
27
|
+
const node_fs_1 = require("node:fs");
|
|
28
|
+
/** Folder name under the home directory. */
|
|
29
|
+
exports.EXTENSION_DIR_NAME = 'chrome-mcp-extension';
|
|
30
|
+
/** The auto-pairing file the server writes; never copied, never overwritten by the mirror. */
|
|
31
|
+
exports.PAIRING_FILE = 'pairing.json';
|
|
32
|
+
/** `dist/src/extension-install.js` -> `<pkg>/extension-dist`, which the tarball ships. */
|
|
33
|
+
function bundledExtensionDir() {
|
|
34
|
+
return (0, node_path_1.resolve)(__dirname, '..', '..', 'extension-dist');
|
|
35
|
+
}
|
|
36
|
+
/** Where the extension is mirrored for Load unpacked. `CHROME_MCP_EXTENSION_DIR` overrides. */
|
|
37
|
+
function extensionInstallDir() {
|
|
38
|
+
const override = process.env.CHROME_MCP_EXTENSION_DIR;
|
|
39
|
+
return override && override.trim() ? (0, node_path_1.resolve)(override.trim()) : (0, node_path_1.join)((0, node_os_1.homedir)(), exports.EXTENSION_DIR_NAME);
|
|
40
|
+
}
|
|
41
|
+
function sameBytes(a, b) {
|
|
42
|
+
return a.length === b.length && a.equals(b);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Mirror the bundled extension into the install dir. Returns the folder to
|
|
46
|
+
* point Chrome at either way: the target when the mirror succeeded, the source
|
|
47
|
+
* when it did not (so callers always have a loadable folder).
|
|
48
|
+
*/
|
|
49
|
+
function syncExtension(from = bundledExtensionDir(), to = extensionInstallDir()) {
|
|
50
|
+
const copied = [];
|
|
51
|
+
let created = false;
|
|
52
|
+
try {
|
|
53
|
+
const entries = (0, node_fs_1.readdirSync)(from, { withFileTypes: true }).filter((e) => e.isFile() && e.name !== exports.PAIRING_FILE);
|
|
54
|
+
if (!entries.some((e) => e.name === 'manifest.json')) {
|
|
55
|
+
return { dir: from, copied, created, ok: false, error: `no manifest.json in ${from}` };
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
(0, node_fs_1.statSync)(to);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
created = true;
|
|
62
|
+
}
|
|
63
|
+
(0, node_fs_1.mkdirSync)(to, { recursive: true });
|
|
64
|
+
for (const e of entries) {
|
|
65
|
+
const src = (0, node_fs_1.readFileSync)((0, node_path_1.join)(from, e.name));
|
|
66
|
+
const dst = (0, node_path_1.join)(to, e.name);
|
|
67
|
+
let current = null;
|
|
68
|
+
try {
|
|
69
|
+
if ((0, node_fs_1.statSync)(dst).isFile())
|
|
70
|
+
current = (0, node_fs_1.readFileSync)(dst);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* absent */
|
|
74
|
+
}
|
|
75
|
+
if (current && sameBytes(current, src))
|
|
76
|
+
continue;
|
|
77
|
+
(0, node_fs_1.writeFileSync)(dst, src);
|
|
78
|
+
copied.push(e.name);
|
|
79
|
+
}
|
|
80
|
+
return { dir: to, copied, created, ok: true };
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
return { dir: from, copied, created, ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=extension-install.js.map
|
|
@@ -1572,7 +1572,10 @@
|
|
|
1572
1572
|
var executor = new ChromeExecutor(() => currentPolicy);
|
|
1573
1573
|
var ws = new WsClient({
|
|
1574
1574
|
onCommand: (cmd) => void router.dispatch(cmd),
|
|
1575
|
-
onState: (state) =>
|
|
1575
|
+
onState: (state) => {
|
|
1576
|
+
void persistState(state);
|
|
1577
|
+
if (state === "unauthorized") void adoptBundledPairing();
|
|
1578
|
+
},
|
|
1576
1579
|
onPolicy: (policy) => {
|
|
1577
1580
|
currentPolicy = policy;
|
|
1578
1581
|
void syncObserverScript(policy, (m) => console.debug("[chrome-mcp]", m));
|
|
@@ -1585,6 +1588,27 @@
|
|
|
1585
1588
|
getPolicy: () => currentPolicy,
|
|
1586
1589
|
log: (m) => console.debug("[chrome-mcp]", m)
|
|
1587
1590
|
});
|
|
1591
|
+
async function readBundledPairing() {
|
|
1592
|
+
try {
|
|
1593
|
+
const res = await fetch(chrome.runtime.getURL("pairing.json"), { cache: "no-store" });
|
|
1594
|
+
if (!res.ok) return null;
|
|
1595
|
+
const j = await res.json();
|
|
1596
|
+
if (typeof j.port === "number" && j.port > 0 && typeof j.token === "string" && j.token.length > 0) {
|
|
1597
|
+
return { wsPort: j.port, token: j.token };
|
|
1598
|
+
}
|
|
1599
|
+
} catch {
|
|
1600
|
+
}
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
async function adoptBundledPairing() {
|
|
1604
|
+
const { wsPort, token, pairingSource } = await chrome.storage.local.get(["wsPort", "token", "pairingSource"]);
|
|
1605
|
+
if (pairingSource === "manual") return false;
|
|
1606
|
+
const bundled = await readBundledPairing();
|
|
1607
|
+
if (!bundled) return false;
|
|
1608
|
+
if (bundled.wsPort === wsPort && bundled.token === token) return false;
|
|
1609
|
+
await chrome.storage.local.set({ wsPort: bundled.wsPort, token: bundled.token, pairingSource: "auto" });
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1588
1612
|
async function getConfig() {
|
|
1589
1613
|
const { wsPort, token, profile } = await chrome.storage.local.get(["wsPort", "token", "profile"]);
|
|
1590
1614
|
if (typeof wsPort === "number" && wsPort > 0 && typeof token === "string" && token.length > 0) {
|
|
@@ -1620,6 +1644,7 @@
|
|
|
1620
1644
|
}
|
|
1621
1645
|
async function keepalivePulse() {
|
|
1622
1646
|
await chrome.storage.local.get("connState");
|
|
1647
|
+
if (!ws.isConnected() && !await getConfig()) await adoptBundledPairing();
|
|
1623
1648
|
await ensureConnected();
|
|
1624
1649
|
}
|
|
1625
1650
|
chrome.runtime.onInstalled.addListener(() => void bootstrap());
|
|
@@ -1643,7 +1668,7 @@
|
|
|
1643
1668
|
});
|
|
1644
1669
|
async function bootstrap() {
|
|
1645
1670
|
await chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.5 });
|
|
1646
|
-
await ensureConnected();
|
|
1671
|
+
if (!await adoptBundledPairing()) await ensureConnected();
|
|
1647
1672
|
}
|
|
1648
1673
|
void bootstrap();
|
|
1649
1674
|
})();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Chrome MCP Bridge",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"description": "Lets a local chrome-mcp server drive this browser. Pair it with the server's handshake token.",
|
|
6
6
|
"minimum_chrome_version": "116",
|
|
7
7
|
"background": {
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<h1>Pair with chrome-mcp</h1>
|
|
19
|
+
<p class="hint" id="source">Checking pairing source…</p>
|
|
19
20
|
<p class="hint">
|
|
20
|
-
|
|
21
|
-
<code>handshake.json</code>, and paste its <code>port</code>
|
|
22
|
-
<code>token</code> below.
|
|
21
|
+
Manual pairing: run <code>npx -y @mehmoodqureshi/chrome-mcp --print-pairing</code>,
|
|
22
|
+
open the printed <code>handshake.json</code>, and paste its <code>port</code>
|
|
23
|
+
and <code>token</code> below.
|
|
23
24
|
</p>
|
|
24
25
|
<label for="port">Port</label>
|
|
25
26
|
<input id="port" type="number" inputmode="numeric" placeholder="38017" />
|
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
var profileEl = document.getElementById("profile");
|
|
10
10
|
var saveEl = document.getElementById("save");
|
|
11
11
|
var statusEl = document.getElementById("status");
|
|
12
|
+
var sourceEl = document.getElementById("source");
|
|
12
13
|
async function loadExisting() {
|
|
13
|
-
const { wsPort, profile, connState } = await chrome.storage.local.get([
|
|
14
|
+
const { wsPort, profile, connState, pairingSource } = await chrome.storage.local.get([
|
|
15
|
+
"wsPort",
|
|
16
|
+
"profile",
|
|
17
|
+
"connState",
|
|
18
|
+
"pairingSource"
|
|
19
|
+
]);
|
|
20
|
+
sourceEl.textContent = pairingSource === "auto" ? "Paired automatically from the pairing.json the server wrote into this extension folder. Saving here overrides it." : pairingSource === "manual" ? "Paired by hand. Saved values take precedence over the bundled pairing.json." : "Not paired yet. If you loaded this extension from the chrome-mcp package folder, start the server once and it pairs itself; otherwise paste the values below.";
|
|
14
21
|
portEl.value = typeof wsPort === "number" && wsPort > 0 ? String(wsPort) : String(DEFAULT_WS_PORT);
|
|
15
22
|
profileEl.value = typeof profile === "string" ? profile : "";
|
|
16
23
|
render(typeof connState === "string" ? connState : "idle");
|
|
@@ -32,7 +39,7 @@
|
|
|
32
39
|
statusEl.textContent = "Status: enter a valid port (> 0) and token";
|
|
33
40
|
return;
|
|
34
41
|
}
|
|
35
|
-
await chrome.storage.local.set({ wsPort, token, profile });
|
|
42
|
+
await chrome.storage.local.set({ wsPort, token, profile, pairingSource: "manual" });
|
|
36
43
|
await chrome.runtime.sendMessage({ type: "reconnect" }).catch(() => void 0);
|
|
37
44
|
statusEl.textContent = "Status: \u2026 connecting";
|
|
38
45
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mehmoodqureshi/chrome-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Drive your real Chrome browser over MCP — real logins, real cookies. A stdio MCP server (CLI) plus an MV3 extension, driving Chrome via chrome.scripting/chrome.tabs. Multi-tab batch automation, accessibility snapshots, deny-all security by default.",
|
|
5
5
|
"author": "Mehmood Ur Rehman Qureshi",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
"dist/shared/**/*.js",
|
|
25
25
|
"dist/shared/**/*.d.ts",
|
|
26
26
|
"extension-dist",
|
|
27
|
-
"
|
|
27
|
+
"!extension-dist/pairing.json",
|
|
28
28
|
"README.md",
|
|
29
|
+
"SETUP.md",
|
|
29
30
|
"LICENSE",
|
|
30
31
|
"docs/BLUEPRINT.md"
|
|
31
32
|
],
|
|
@@ -55,12 +56,10 @@
|
|
|
55
56
|
"clean": "rimraf dist extension-dist",
|
|
56
57
|
"test": "npm run clean && npm run build && npm run build:ext && node --test dist/test/*.test.js",
|
|
57
58
|
"test:hitl": "npm run build && node dist/hitl/index.js",
|
|
58
|
-
"prepack": "npm run clean && npm run build && npm run build:ext"
|
|
59
|
-
"postinstall": "node scripts/postinstall.js"
|
|
59
|
+
"prepack": "npm run clean && npm run build && npm run build:ext"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
63
|
-
"playwright": "^1.49.1",
|
|
64
63
|
"ws": "^8.18.0",
|
|
65
64
|
"zod": "^4.4.3"
|
|
66
65
|
},
|
|
@@ -69,6 +68,7 @@
|
|
|
69
68
|
"@types/node": "^20.17.10",
|
|
70
69
|
"@types/ws": "^8.5.13",
|
|
71
70
|
"esbuild": "^0.24.0",
|
|
71
|
+
"playwright": "^1.49.1",
|
|
72
72
|
"rimraf": "^5.0.10",
|
|
73
73
|
"typescript": "^5.7.2"
|
|
74
74
|
},
|
package/scripts/postinstall.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* postinstall — install the Chromium that the CDP fallback needs.
|
|
3
|
-
*
|
|
4
|
-
* Skip-guarded and never fatal: a failure here must not break `npm install`
|
|
5
|
-
* (the extension path needs no browser, and CI rarely wants the download).
|
|
6
|
-
* Skips when:
|
|
7
|
-
* - CHROME_MCP_SKIP_BROWSER_DOWNLOAD / PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD is set,
|
|
8
|
-
* - CI is set,
|
|
9
|
-
* - playwright is not installed yet (it is added in phase 3).
|
|
10
|
-
*/
|
|
11
|
-
'use strict';
|
|
12
|
-
|
|
13
|
-
function shouldSkip() {
|
|
14
|
-
return (
|
|
15
|
-
process.env.CHROME_MCP_SKIP_BROWSER_DOWNLOAD === '1' ||
|
|
16
|
-
process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1' ||
|
|
17
|
-
process.env.CI === 'true' ||
|
|
18
|
-
process.env.CI === '1'
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function hasPlaywright() {
|
|
23
|
-
try {
|
|
24
|
-
require.resolve('playwright');
|
|
25
|
-
return true;
|
|
26
|
-
} catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function main() {
|
|
32
|
-
if (shouldSkip()) {
|
|
33
|
-
process.stdout.write('[postinstall] skipping Chromium download (guard set).\n');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (!hasPlaywright()) {
|
|
37
|
-
process.stdout.write('[postinstall] playwright not installed; CDP fallback unavailable until phase 3.\n');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
try {
|
|
41
|
-
const { execFileSync } = require('node:child_process');
|
|
42
|
-
const { dirname, join } = require('node:path');
|
|
43
|
-
// Drive Playwright's CLI through node rather than the `npx`/`playwright` bin
|
|
44
|
-
// shim: on Windows those are .cmd files, which execFileSync cannot spawn
|
|
45
|
-
// without a shell. `cli.js` is playwright's own bin target; resolving it via
|
|
46
|
-
// package.json avoids the exports map, which exposes no './cli' subpath.
|
|
47
|
-
const cli = join(dirname(require.resolve('playwright/package.json')), 'cli.js');
|
|
48
|
-
execFileSync(process.execPath, [cli, 'install', 'chromium'], { stdio: 'inherit' });
|
|
49
|
-
} catch (err) {
|
|
50
|
-
process.stdout.write(
|
|
51
|
-
`[postinstall] Chromium install skipped (non-fatal): ${err && err.message ? err.message : err}\n`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
main();
|