@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 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
- `extension-dist/` ships prebuilt inside the npm package, so there is nothing to
112
- compile. Install globally to get a stable path to it:
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
- npm install -g @mehmoodqureshi/chrome-mcp
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
- select that `extension-dist/` directory. (Working from a git clone instead? Run
121
- `npm install && npm run build:ext` first `extension-dist/` is gitignored.)
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-dist/` folder you just loaded. The extension reads that file from its
126
- own folder on startup and pairs itself, so the toolbar badge turns green with no
127
- token to paste. Load the extension before the server has ever run? It re-checks
128
- every 30 seconds and pairs as soon as the file appears.
129
-
130
- Manual fallback (a copied folder, a read-only install): run
131
- `npx chrome-mcp --print-pairing`, open the extension's **Options** page, and
132
- paste the `port` + `token` from `~/.chrome-mcp/handshake.json`. Values saved
133
- there take precedence over the bundled file.
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 the folder `chrome-mcp --extension-path` prints and pair
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 `extension-dist/`. It pairs
427
- itself from the `pairing.json` the server writes into that folder on boot; the
428
- **Options** page paste of `port` + `token` from `~/.chrome-mcp/handshake.json`
429
- (run `npx chrome-mcp --print-pairing` to get the path) is only the fallback.
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 package globally
36
+ ## Step 2 — Install the extension folder
37
37
 
38
- A global install gives the extension a stable path that Chrome can load from.
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
- npm install -g @mehmoodqureshi/chrome-mcp
42
- chrome-mcp --version
43
+ npx -y @mehmoodqureshi/chrome-mcp --extension-path
43
44
  ```
44
45
 
45
- Verify: the second command prints a version. Then locate the extension folder:
46
-
47
- ```
48
- chrome-mcp --extension-path
49
- ```
50
-
51
- It prints the absolute path of the bundled `extension-dist` folder. Verify with
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-dist` folder from step 2.
112
- 4. Confirm an extension named **Chrome MCP Bridge** now appears in the list.
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-dist` folder the human loaded in step 4.
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 ask the human to look at the extension's toolbar icon on
137
- `chrome://extensions` or in the toolbar: a green dot badge means paired and
138
- connected. It can take up to 30 seconds if the extension was loaded before the
139
- server first ran. Wait for the human to confirm.
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 Chrome MCP Bridge, then **Extension
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-dist` path they loaded.
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 pairing, `tabs_list`, an allowed navigation, and a refused navigation
184
- each verified green.
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
- * Absolute path of the bundled extension. `dist/src/cli.js` -> `<pkg>/extension-dist`,
56
- * which `files` in package.json ships in the tarball, so this is right for a global
57
- * install, an npx cache entry, and a git checkout alike.
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 extensionPath() {
60
- return (0, node_path_1.resolve)(__dirname, '..', '..', 'extension-dist');
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(`${extensionPath()}\n`);
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 bundled = (0, auth_1.writeBundledPairing)(extensionPath(), { port, token });
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 that folder needs no token paste`);
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 ${extensionPath()})`);
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
  }
@@ -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 Print the absolute path of the bundled extension folder
278
- (what to pick in chrome://extensions -> Load unpacked)
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 MCP \u2014 connected" },
1622
- connecting: { text: "\u2026", color: "#ca8a04", title: "Chrome MCP \u2014 connecting" },
1623
- unauthorized: { text: "!", color: "#dc2626", title: "Chrome MCP \u2014 rejected (bad/stale token; re-pair)" },
1624
- idle: { text: "\u25CB", color: "#6b7280", title: "Chrome MCP \u2014 not connected (open options to pair)" }
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": "Chrome MCP Bridge",
4
- "version": "0.8.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 MCP — open options to pair"
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 MCP — Pairing</title>
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>, open the
22
- printed <code>handshake.json</code>, and paste its <code>port</code> and
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.8.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",