@mehmoodqureshi/chrome-mcp 0.8.0 → 0.9.1
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 +49 -20
- package/SETUP.md +40 -27
- package/dist/src/cli.js +26 -9
- package/dist/src/config.js +4 -2
- package/dist/src/extension-install.d.ts +42 -0
- package/dist/src/extension-install.js +86 -0
- package/extension-dist/background.js +21 -4
- package/extension-dist/icon128.png +0 -0
- package/extension-dist/icon16.png +0 -0
- package/extension-dist/icon32.png +0 -0
- package/extension-dist/icon48.png +0 -0
- package/extension-dist/manifest.json +16 -3
- package/extension-dist/options.html +4 -4
- package/extension-dist/options.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -108,29 +108,55 @@ explicitly (and is never written to disk).
|
|
|
108
108
|
|
|
109
109
|
**2. Load the extension** — **required**; the server can drive nothing without it.
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
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:
|
|
113
122
|
|
|
114
123
|
```bash
|
|
115
|
-
|
|
116
|
-
chrome-mcp --extension-path # prints the absolute path of extension-dist
|
|
124
|
+
npx -y @mehmoodqureshi/chrome-mcp --extension-path
|
|
117
125
|
```
|
|
118
126
|
|
|
119
127
|
Then `chrome://extensions` → enable **Developer mode** → **Load unpacked** →
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
pick `chrome-mcp-extension` in your home folder. After upgrading the package the
|
|
129
|
+
server refreshes the files on its next boot and the extension reloads itself
|
|
130
|
+
within 30 seconds; nothing to click. `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.)
|
|
122
134
|
|
|
123
135
|
**3. Pair it — usually nothing to do.** Every time the server boots it writes
|
|
124
136
|
`pairing.json` (mode 0600, never shipped in the tarball) into the very
|
|
125
|
-
`extension
|
|
126
|
-
own folder on startup and pairs itself, so the toolbar badge turns
|
|
127
|
-
token to paste. Load the extension before the server has ever
|
|
128
|
-
every 30 seconds and pairs as soon as the file appears.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 **MCP Extension for Chrome**, 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.
|
|
134
160
|
|
|
135
161
|
### Running more than one session
|
|
136
162
|
|
|
@@ -178,7 +204,7 @@ start. Wrap it in `cmd /c`:
|
|
|
178
204
|
|
|
179
205
|
Or from Claude Code: `claude mcp add chrome-mcp -- cmd /c npx -y @mehmoodqureshi/chrome-mcp --allow-domain example.com`
|
|
180
206
|
|
|
181
|
-
Everything else is the same — load
|
|
207
|
+
Everything else is the same — load `%USERPROFILE%\chrome-mcp-extension` and pair
|
|
182
208
|
as above.
|
|
183
209
|
|
|
184
210
|
The tools cover tabs, navigation, interaction (`click`/`type`/`press`/`hover`/
|
|
@@ -423,10 +449,13 @@ RUN_EXT_SMOKE=1 node --test dist/test/extension-smoke.test.js # live, headed
|
|
|
423
449
|
## The extension
|
|
424
450
|
|
|
425
451
|
`extension/` builds (esbuild) to `extension-dist/`, loaded via
|
|
426
|
-
`chrome://extensions` → **Load unpacked** → select
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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.
|
|
430
459
|
|
|
431
460
|
> **Reads/interaction use `chrome.scripting`/`chrome.tabs`** — no "is being
|
|
432
461
|
> debugged" banner, CSP-safe reads (isolated world), testable under Playwright.
|
package/SETUP.md
CHANGED
|
@@ -33,25 +33,23 @@ npm --version
|
|
|
33
33
|
|
|
34
34
|
If Node is below 18, stop and tell the human to upgrade before anything else.
|
|
35
35
|
|
|
36
|
-
## Step 2 — Install the
|
|
36
|
+
## Step 2 — Install the extension folder
|
|
37
37
|
|
|
38
|
-
|
|
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:
|
|
39
41
|
|
|
40
42
|
```
|
|
41
|
-
|
|
42
|
-
chrome-mcp --version
|
|
43
|
+
npx -y @mehmoodqureshi/chrome-mcp --extension-path
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`ls` (or `dir`) that the folder holds `manifest.json` and `background.js`.
|
|
53
|
-
Record the absolute path; the human needs it in step 4. The install is small
|
|
54
|
-
and downloads no browser: the server drives the Chrome the human already has.
|
|
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 and the loaded extension
|
|
51
|
+
reloads itself when the version changes, so upgrades need nothing from the
|
|
52
|
+
human.
|
|
55
53
|
|
|
56
54
|
## Step 3 — Wire the server into this MCP client
|
|
57
55
|
|
|
@@ -108,15 +106,16 @@ You cannot do this step yourself. Give the human these exact instructions:
|
|
|
108
106
|
|
|
109
107
|
1. Open `chrome://extensions` in Chrome.
|
|
110
108
|
2. Turn on **Developer mode** (top right).
|
|
111
|
-
3. Click **Load unpacked** and choose the `extension
|
|
112
|
-
|
|
109
|
+
3. Click **Load unpacked** and choose the `chrome-mcp-extension` folder in
|
|
110
|
+
their home directory (the path from step 2).
|
|
111
|
+
4. Confirm an extension named **MCP Extension for Chrome** now appears in the list.
|
|
113
112
|
|
|
114
113
|
Wait for the human to confirm before continuing.
|
|
115
114
|
|
|
116
115
|
## Step 5 — Pairing (automatic; verify it)
|
|
117
116
|
|
|
118
117
|
Pairing needs no paste. Every time the server boots it writes `pairing.json`
|
|
119
|
-
(mode 0600) into the same `extension
|
|
118
|
+
(mode 0600) into the same `chrome-mcp-extension` folder the human loaded in step 4.
|
|
120
119
|
The extension reads that file from its own folder and pairs itself. So make
|
|
121
120
|
sure a server has booted at least once since step 2, in either of these ways:
|
|
122
121
|
|
|
@@ -124,19 +123,33 @@ sure a server has booted at least once since step 2, in either of these ways:
|
|
|
124
123
|
- start one yourself in pairing mode and leave it running in the background:
|
|
125
124
|
|
|
126
125
|
```
|
|
127
|
-
chrome-mcp --print-pairing --persist-token
|
|
126
|
+
npx -y @mehmoodqureshi/chrome-mcp --print-pairing --persist-token
|
|
128
127
|
```
|
|
129
128
|
|
|
130
129
|
Verify the file exists (do not print it):
|
|
131
130
|
|
|
132
131
|
```
|
|
133
|
-
ls "$(chrome-mcp --extension-path)/pairing.json"
|
|
132
|
+
ls "$(npx -y @mehmoodqureshi/chrome-mcp --extension-path)/pairing.json"
|
|
134
133
|
```
|
|
135
134
|
|
|
136
|
-
Then
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
Then tell the human where to look. The status badge is on the extension's
|
|
136
|
+
icon in Chrome's toolbar, not on the `chrome://extensions` page. Chrome hides
|
|
137
|
+
new extensions behind the puzzle-piece button at the right of the address bar,
|
|
138
|
+
so give them these exact steps:
|
|
139
|
+
|
|
140
|
+
1. Click the puzzle-piece button at the right of the address bar.
|
|
141
|
+
2. Find **MCP Extension for Chrome** in the list and click the pin icon next to it.
|
|
142
|
+
The extension icon now stays in the toolbar.
|
|
143
|
+
3. Look at the small badge on that icon. Hovering it shows the status in words.
|
|
144
|
+
|
|
145
|
+
Badge meanings: a green dot means paired and connected. Yellow dots mean
|
|
146
|
+
connecting. A grey circle means not paired yet, usually because no server has
|
|
147
|
+
run since the extension was loaded. A red exclamation mark means the token was
|
|
148
|
+
rejected, which the extension fixes by itself within a few seconds by re-reading
|
|
149
|
+
the pairing file.
|
|
150
|
+
|
|
151
|
+
A green dot can take up to 30 seconds if the extension was loaded before the
|
|
152
|
+
server first ran. Wait for the human to confirm the green dot.
|
|
140
153
|
|
|
141
154
|
**Manual fallback**, only if the badge stays grey after a minute (a copied
|
|
142
155
|
extension folder or a read-only global install): read the port without
|
|
@@ -146,7 +159,7 @@ printing the token,
|
|
|
146
159
|
node -e "console.log(require(require('os').homedir()+'/.chrome-mcp/handshake.json').port)"
|
|
147
160
|
```
|
|
148
161
|
|
|
149
|
-
then have the human open **Details** on
|
|
162
|
+
then have the human open **Details** on MCP Extension for Chrome, then **Extension
|
|
150
163
|
options**, enter that **Port**, open `~/.chrome-mcp/handshake.json` in a text
|
|
151
164
|
editor and paste its `token` into the **Token** field, leave **Profile** as
|
|
152
165
|
`default`, and click **Save**. The status line should read **connected** within
|
|
@@ -178,10 +191,10 @@ not stick. Re-check the port and token before anything else.
|
|
|
178
191
|
Summarize for the human:
|
|
179
192
|
|
|
180
193
|
- Node and package versions installed.
|
|
181
|
-
- The absolute `extension
|
|
194
|
+
- The absolute `chrome-mcp-extension` path they loaded.
|
|
182
195
|
- Which MCP client was configured, at which scope, with which domains and gates.
|
|
183
|
-
- Whether
|
|
184
|
-
each verified
|
|
196
|
+
- Whether the badge went green, and whether `tabs_list`, an allowed
|
|
197
|
+
navigation, and a refused navigation each verified.
|
|
185
198
|
- Anything still open on their side, such as restarting the client.
|
|
186
199
|
|
|
187
200
|
For any failure, name the exact symptom, what you tried, and the matching
|
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
|
/**
|
|
@@ -52,12 +53,23 @@ function version() {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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.
|
|
58
59
|
*/
|
|
59
|
-
function
|
|
60
|
-
|
|
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} — a loaded extension reloads itself within 30 s`);
|
|
71
|
+
}
|
|
72
|
+
return r.dir;
|
|
61
73
|
}
|
|
62
74
|
/** Render a byte count as a short human string (1.2 MB, 904 KB, …). */
|
|
63
75
|
function humanBytes(n) {
|
|
@@ -163,7 +175,7 @@ async function main() {
|
|
|
163
175
|
return;
|
|
164
176
|
}
|
|
165
177
|
if (cfg.showExtensionPath) {
|
|
166
|
-
process.stdout.write(`${
|
|
178
|
+
process.stdout.write(`${installExtension()}\n`);
|
|
167
179
|
return;
|
|
168
180
|
}
|
|
169
181
|
const dataDir = (0, datadir_1.ensureDataDir)(cfg.dataDir);
|
|
@@ -194,13 +206,18 @@ async function main() {
|
|
|
194
206
|
// Drop the same port + token into the bundled extension folder so a Load
|
|
195
207
|
// unpacked from there pairs itself. Best-effort: a read-only install just
|
|
196
208
|
// falls back to the Options-page paste.
|
|
197
|
-
const
|
|
209
|
+
const extDir = installExtension();
|
|
210
|
+
const bundled = (0, auth_1.writeBundledPairing)(extDir, { port, token });
|
|
198
211
|
if (bundled) {
|
|
199
|
-
(0, server_2.logErr)(`auto-pairing file written to ${bundled} — Load unpacked from
|
|
212
|
+
(0, server_2.logErr)(`auto-pairing file written to ${bundled} — Load unpacked from ${extDir} needs no token paste`);
|
|
200
213
|
}
|
|
201
214
|
else {
|
|
202
|
-
(0, server_2.logDebug)(`auto-pairing file not written (extension folder missing or read-only at ${
|
|
215
|
+
(0, server_2.logDebug)(`auto-pairing file not written (extension folder missing or read-only at ${extDir})`);
|
|
203
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 });
|
|
204
221
|
if (process.env.CHROME_MCP_TOKEN) {
|
|
205
222
|
(0, server_2.logErr)('token: pinned from CHROME_MCP_TOKEN (stable; pair once, never again).');
|
|
206
223
|
}
|
package/dist/src/config.js
CHANGED
|
@@ -274,8 +274,10 @@ Connection:
|
|
|
274
274
|
land in profiles/<profile>/tasks/<task>/.
|
|
275
275
|
--print-pairing Write the handshake, print its path, and keep the bridge
|
|
276
276
|
up until Ctrl-C (manual pairing; never serves MCP)
|
|
277
|
-
--extension-path
|
|
278
|
-
|
|
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.
|
|
279
281
|
--persist-token Reuse a stable on-disk token across restarts so the
|
|
280
282
|
extension never has to re-pair (default: fresh per boot).
|
|
281
283
|
CHROME_MCP_TOKEN env, if set, pins the token explicitly.
|
|
@@ -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
|
|
@@ -1609,6 +1609,21 @@
|
|
|
1609
1609
|
await chrome.storage.local.set({ wsPort: bundled.wsPort, token: bundled.token, pairingSource: "auto" });
|
|
1610
1610
|
return true;
|
|
1611
1611
|
}
|
|
1612
|
+
async function reloadIfFilesChanged() {
|
|
1613
|
+
try {
|
|
1614
|
+
const res = await fetch(chrome.runtime.getURL("manifest.json"), { cache: "no-store" });
|
|
1615
|
+
if (!res.ok) return;
|
|
1616
|
+
const onDisk = await res.json();
|
|
1617
|
+
const loaded = chrome.runtime.getManifest().version;
|
|
1618
|
+
if (typeof onDisk.version !== "string" || onDisk.version === loaded) return;
|
|
1619
|
+
const { reloadedFor } = await chrome.storage.local.get("reloadedFor");
|
|
1620
|
+
if (reloadedFor === onDisk.version) return;
|
|
1621
|
+
await chrome.storage.local.set({ reloadedFor: onDisk.version });
|
|
1622
|
+
console.info(`[chrome-mcp] extension files updated on disk (${loaded} -> ${onDisk.version}); reloading`);
|
|
1623
|
+
chrome.runtime.reload();
|
|
1624
|
+
} catch {
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1612
1627
|
async function getConfig() {
|
|
1613
1628
|
const { wsPort, token, profile } = await chrome.storage.local.get(["wsPort", "token", "profile"]);
|
|
1614
1629
|
if (typeof wsPort === "number" && wsPort > 0 && typeof token === "string" && token.length > 0) {
|
|
@@ -1618,10 +1633,10 @@
|
|
|
1618
1633
|
return null;
|
|
1619
1634
|
}
|
|
1620
1635
|
var BADGE = {
|
|
1621
|
-
connected: { text: "\u25CF", color: "#16a34a", title: "Chrome
|
|
1622
|
-
connecting: { text: "\u2026", color: "#ca8a04", title: "Chrome
|
|
1623
|
-
unauthorized: { text: "!", color: "#dc2626", title: "Chrome
|
|
1624
|
-
idle: { text: "\u25CB", color: "#6b7280", title: "Chrome
|
|
1636
|
+
connected: { text: "\u25CF", color: "#16a34a", title: "MCP Extension for Chrome \u2014 connected" },
|
|
1637
|
+
connecting: { text: "\u2026", color: "#ca8a04", title: "MCP Extension for Chrome \u2014 connecting" },
|
|
1638
|
+
unauthorized: { text: "!", color: "#dc2626", title: "MCP Extension for Chrome \u2014 rejected (bad/stale token; re-pair)" },
|
|
1639
|
+
idle: { text: "\u25CB", color: "#6b7280", title: "MCP Extension for Chrome \u2014 not connected (open options to pair)" }
|
|
1625
1640
|
};
|
|
1626
1641
|
function reflectBadge(state) {
|
|
1627
1642
|
const b = BADGE[state] ?? BADGE.idle;
|
|
@@ -1644,6 +1659,7 @@
|
|
|
1644
1659
|
}
|
|
1645
1660
|
async function keepalivePulse() {
|
|
1646
1661
|
await chrome.storage.local.get("connState");
|
|
1662
|
+
await reloadIfFilesChanged();
|
|
1647
1663
|
if (!ws.isConnected() && !await getConfig()) await adoptBundledPairing();
|
|
1648
1664
|
await ensureConnected();
|
|
1649
1665
|
}
|
|
@@ -1668,6 +1684,7 @@
|
|
|
1668
1684
|
});
|
|
1669
1685
|
async function bootstrap() {
|
|
1670
1686
|
await chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.5 });
|
|
1687
|
+
await reloadIfFilesChanged();
|
|
1671
1688
|
if (!await adoptBundledPairing()) await ensureConnected();
|
|
1672
1689
|
}
|
|
1673
1690
|
void bootstrap();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
|
-
"name": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"name": "MCP Extension for Chrome",
|
|
4
|
+
"version": "0.9.1",
|
|
5
5
|
"description": "Lets a local chrome-mcp server drive this browser. Pair it with the server's handshake token.",
|
|
6
|
+
"homepage_url": "https://github.com/Mehmoodqureshi/chrome-mcp",
|
|
6
7
|
"minimum_chrome_version": "116",
|
|
8
|
+
"icons": {
|
|
9
|
+
"16": "icon16.png",
|
|
10
|
+
"32": "icon32.png",
|
|
11
|
+
"48": "icon48.png",
|
|
12
|
+
"128": "icon128.png"
|
|
13
|
+
},
|
|
7
14
|
"background": {
|
|
8
15
|
"service_worker": "background.js"
|
|
9
16
|
},
|
|
@@ -22,6 +29,12 @@
|
|
|
22
29
|
],
|
|
23
30
|
"options_page": "options.html",
|
|
24
31
|
"action": {
|
|
25
|
-
"default_title": "Chrome
|
|
32
|
+
"default_title": "MCP Extension for Chrome — open options to pair",
|
|
33
|
+
"default_icon": {
|
|
34
|
+
"16": "icon16.png",
|
|
35
|
+
"32": "icon32.png",
|
|
36
|
+
"48": "icon48.png",
|
|
37
|
+
"128": "icon128.png"
|
|
38
|
+
}
|
|
26
39
|
}
|
|
27
40
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
|
-
<title>Chrome
|
|
5
|
+
<title>MCP Extension for Chrome — Pairing</title>
|
|
6
6
|
<style>
|
|
7
7
|
body { font: 14px/1.5 system-ui, sans-serif; max-width: 32rem; margin: 2rem auto; padding: 0 1rem; }
|
|
8
8
|
h1 { font-size: 1.2rem; }
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
<h1>Pair with chrome-mcp</h1>
|
|
19
19
|
<p class="hint" id="source">Checking pairing source…</p>
|
|
20
20
|
<p class="hint">
|
|
21
|
-
Manual pairing: run <code>npx chrome-mcp --print-pairing</code>,
|
|
22
|
-
printed <code>handshake.json</code>, and paste its <code>port</code>
|
|
23
|
-
<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.
|
|
24
24
|
</p>
|
|
25
25
|
<label for="port">Port</label>
|
|
26
26
|
<input id="port" type="number" inputmode="numeric" placeholder="38017" />
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"connState",
|
|
18
18
|
"pairingSource"
|
|
19
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.";
|
|
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." : typeof wsPort === "number" && wsPort > 0 && connState !== "idle" ? "Paired with values saved by an earlier version. Saving here keeps them manual." : "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.";
|
|
21
21
|
portEl.value = typeof wsPort === "number" && wsPort > 0 ? String(wsPort) : String(DEFAULT_WS_PORT);
|
|
22
22
|
profileEl.value = typeof profile === "string" ? profile : "";
|
|
23
23
|
render(typeof connState === "string" ? connState : "idle");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mehmoodqureshi/chrome-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsc -p tsconfig.json",
|
|
53
53
|
"build:ext": "node extension/scripts/build-ext.mjs",
|
|
54
|
+
"pack:ext": "npm run build:ext && rm -f chrome-mcp-extension-$npm_package_version.zip && cd extension-dist && zip -q -r ../chrome-mcp-extension-$npm_package_version.zip . -x pairing.json '.*' && cd .. && ls -la chrome-mcp-extension-$npm_package_version.zip",
|
|
54
55
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
56
|
"typecheck:ext": "tsc -p tsconfig.ext.json",
|
|
56
57
|
"clean": "rimraf dist extension-dist",
|