codex-control-chrome-mcp 1.1.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ This project follows semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.2.1] - 2026-07-03
10
+
11
+ ### Fixed
12
+
13
+ - Native host failed to start on 1.2.0. The install launcher runs the host with `--browser <name>` before the extension origin that Chrome appends, so the origin was no longer `args[0]`. The CLI dispatcher only recognized native-host mode when the origin was `args[0]`, so the launcher's argv fell through to `Unknown command: --browser` and the host exited on every connect. The visible symptoms were the Codex extension connection flapping between connected/disconnected and `status` never listing a bridge socket (`sockets: []`). The dispatcher now detects the origin by shape anywhere in the args, and a regression test covers the launcher argv.
14
+
15
+ ## [1.2.0] - 2026-06-29
16
+
17
+ ### Added
18
+
19
+ - Microsoft Edge support as a first-class browser alongside Chrome. `install-native-host`, `uninstall-native-host`, and `status` accept `--browser <chrome|edge|all>`.
20
+ - Auto-detection: without `--browser`, `install-native-host` installs for every supported browser whose profile directory exists, `uninstall-native-host` targets every browser it previously installed for, and `status` reports both browsers.
21
+ - The MCP stdio server's self-heal now re-asserts the manifest for every browser it manages, not just Chrome.
22
+
23
+ ### Changed
24
+
25
+ - Native host install state is now stored per browser (`install-state.<browser>.json` / `config.<browser>.json`). Chrome keeps the original unsuffixed filenames, so existing installs remain valid; Edge no longer clobbers Chrome's restore and self-heal metadata.
26
+ - `status` output now nests install state under each browser (`install.chrome`, `install.edge`). The `chrome_status` MCP tool additionally reports `manifestPaths` for every supported browser.
27
+
9
28
  ## [1.1.0] - 2026-06-27
10
29
 
11
30
  ### Added
package/README.md CHANGED
@@ -15,17 +15,6 @@ This is an independent community project. It is not affiliated with OpenAI, Code
15
15
  - Chrome Extension: [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg)
16
16
  - Skill: [`skills/codex-control-chrome-mcp/SKILL.md`](./skills/codex-control-chrome-mcp/SKILL.md)
17
17
 
18
- ## Status
19
-
20
- The current implementation provides:
21
-
22
- - official `@modelcontextprotocol/sdk` stdio server
23
- - Chrome Native Messaging host mode
24
- - install/uninstall commands with manifest backup
25
- - JSON-RPC bridge socket under `/tmp/codex-browser-use`
26
- - Chrome tab, navigation, screenshot, CDP, and event MCP tools
27
- - bundled skill documentation for Agent tool ordering
28
-
29
18
  ## Platform Support
30
19
 
31
20
  | Platform | Status | Notes |
@@ -36,16 +25,33 @@ The current implementation provides:
36
25
 
37
26
  Node.js 20 or newer is required. npm Trusted Publishing for releases uses Node.js 24 in GitHub Actions.
38
27
 
28
+ ## Browser Support
29
+
30
+ Both Chromium browsers use the same Codex extension and the same `chrome-extension://` origin, so a single manifest works for either.
31
+
32
+ | Browser | Status | Notes |
33
+ | --- | --- | --- |
34
+ | Google Chrome | Tested | Default target. |
35
+ | Microsoft Edge | Supported | Same extension and native-messaging mechanism; installs to the Edge `NativeMessagingHosts` directory. |
36
+
37
+ Commands accept `--browser <chrome\|edge\|all>`. Without the flag, `install-native-host` installs for every supported browser whose profile directory exists, `uninstall-native-host` targets every browser it previously installed for, and `status` reports both.
38
+
39
39
  ## Quick Start
40
40
 
41
- Install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome profile you want to automate.
41
+ Install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome or Edge profile you want to automate.
42
42
 
43
- Install the Chrome native host:
43
+ Install the native host (auto-detects the browsers present):
44
44
 
45
45
  ```bash
46
46
  npx -y codex-control-chrome-mcp@latest install-native-host
47
47
  ```
48
48
 
49
+ To target one browser explicitly:
50
+
51
+ ```bash
52
+ npx -y codex-control-chrome-mcp@latest install-native-host --browser edge
53
+ ```
54
+
49
55
  Configure your Agent to use the MCP stdio server:
50
56
 
51
57
  ```json
@@ -144,16 +150,12 @@ node ./bin/codex-control-chrome-mcp.js --native-host
144
150
 
145
151
  ## Troubleshooting
146
152
 
147
- `status` reports a `registered` flag and a `classification` of the live manifest. When `registered` is `false` and `classification.kind` is `official-openai`, a Codex update has reclaimed the manifest (see below). `classification` identifies OpenAI's genuine host by stable signals — the `com.openai.codexextension` name, the official extension origin, the bundle path, the binary format, and (on macOS) the OpenAI code signature — not by filename.
148
-
149
153
  ### A Codex update stopped the bridge
150
154
 
151
155
  Codex App/extension updates re-register their own `com.openai.codexextension` native-messaging manifest, which overwrites this project's proxy registration (the bundled host binary has even been renamed across releases). The symptom is `status` showing `sockets: []`, `registered: false`, and a manifest `path` back inside the Codex bundle.
152
156
 
153
157
  The MCP stdio server now **self-heals**: on startup it re-asserts the manifest when it detects it was reverted, so you usually do not need to re-run `install-native-host`. Because Chrome only reads the manifest when the extension reconnects, **reload the Codex Chrome Extension (or restart Chrome) once** after a Codex update to bring the bridge back. Set `CODEX_CONTROL_CHROME_NO_AUTO_REGISTER=1` to disable the auto re-register.
154
158
 
155
- If `status` shows no bridge sockets, reload Chrome or the Codex Chrome Extension after installing the native host.
156
-
157
159
  If Codex App integration stops working, uninstall this native host to restore the backed-up manifest:
158
160
 
159
161
  ```bash
@@ -162,6 +164,8 @@ npx -y codex-control-chrome-mcp@latest uninstall-native-host
162
164
 
163
165
  If tab or CDP tools fail, use the bundled skill workflow: list tabs again, claim only current tab IDs, attach before raw CDP calls, and read CDP events after enabling the relevant domain.
164
166
 
167
+ See [Install And Uninstall](./docs/install.md#troubleshooting) for the full per-browser checklist (the `registered` flag and host `classification`).
168
+
165
169
  ## License
166
170
 
167
171
  MIT
@@ -203,20 +203,16 @@ Agent-side MCP config should use:
203
203
  }
204
204
  ```
205
205
 
206
- Chrome Native Messaging manifests cannot store command arguments. Therefore `install-native-host` creates a stable launcher script:
207
-
208
- ```sh
209
- npx -y codex-control-chrome-mcp@latest install-native-host
210
- ```
206
+ Chrome Native Messaging manifests cannot store command arguments. Therefore `install-native-host` writes a stable launcher script at:
211
207
 
212
208
  ```text
213
209
  ~/.codex-control-chrome-mcp/native-host-launcher
214
210
  ```
215
211
 
216
- That launcher runs:
212
+ That launcher runs (the `--browser` flag tells the host which browser's proxy config to read):
217
213
 
218
214
  ```sh
219
- npx -y codex-control-chrome-mcp@1.0.0 --native-host "$@"
215
+ npx -y codex-control-chrome-mcp@latest --native-host --browser chrome "$@"
220
216
  ```
221
217
 
222
218
  The native host manifest points to the launcher, not directly to `npx`.
package/docs/install.md CHANGED
@@ -8,13 +8,6 @@ Package:
8
8
 
9
9
  ## Development Status
10
10
 
11
- The current implementation provides:
12
-
13
- - MCP stdio mode: `npx -y codex-control-chrome-mcp@latest --stdio`
14
- - manifest install: `npx -y codex-control-chrome-mcp@latest install-native-host`
15
- - manifest uninstall: `npx -y codex-control-chrome-mcp@latest uninstall-native-host`
16
- - bridge/socket status: `npx -y codex-control-chrome-mcp@latest status`
17
-
18
11
  Do not run install on a machine where Codex Chrome integration is important unless you are ready to test proxy mode or restore from backup.
19
12
 
20
13
  This is an independent community project. It is not affiliated with OpenAI, Codex, Google, or Chrome.
@@ -24,35 +17,47 @@ This is an independent community project. It is not affiliated with OpenAI, Code
24
17
  | Platform | Status | Notes |
25
18
  | --- | --- | --- |
26
19
  | macOS | Tested | Primary supported platform. |
27
- | Linux | Experimental | Uses the Google Chrome user-level Native Messaging host directory. |
28
- | Windows | Unsupported | Chrome Native Messaging registration uses registry keys on Windows and is not implemented yet. |
20
+ | Linux | Experimental | Uses the browser's user-level Native Messaging host directory. |
21
+ | Windows | Unsupported | Native Messaging registration uses registry keys on Windows and is not implemented yet. |
22
+
23
+ ## Browser Support
24
+
25
+ | Browser | Status | Notes |
26
+ | --- | --- | --- |
27
+ | Google Chrome | Tested | Default target. |
28
+ | Microsoft Edge | Supported | Same Codex extension and `chrome-extension://` origin; installs to Edge's `NativeMessagingHosts` directory. |
29
+
30
+ Commands accept `--browser <chrome|edge|all>`. Without it, `install-native-host` auto-detects the browsers present on the machine, `uninstall-native-host` targets every browser it previously installed for, and `status` reports both browsers.
29
31
 
30
32
  ## Install
31
33
 
32
- First install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome profile you want to automate.
34
+ First install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome or Edge profile you want to automate.
33
35
 
34
36
  ```bash
35
37
  npx -y codex-control-chrome-mcp@latest install-native-host
36
38
  ```
37
39
 
38
- This writes a stable launcher:
40
+ Without `--browser`, this installs for every supported browser whose profile directory exists. Pass `--browser <chrome|edge|all>` to target one explicitly.
41
+
42
+ This writes a stable per-browser launcher (Edge uses the `-edge` suffix):
39
43
 
40
44
  ```text
41
45
  ~/.codex-control-chrome-mcp/native-host-launcher
46
+ ~/.codex-control-chrome-mcp/native-host-launcher-edge
42
47
  ```
43
48
 
44
- The launcher invokes:
49
+ The launcher invokes (the `--browser` flag tells the host which browser's config to read for proxy mode):
45
50
 
46
51
  ```bash
47
- npx -y codex-control-chrome-mcp@1.0.0 --native-host "$@"
52
+ npx -y codex-control-chrome-mcp@latest --native-host --browser chrome "$@"
48
53
  ```
49
54
 
50
- The installer:
55
+ For each selected browser the installer:
51
56
 
52
- 1. Reads the existing `com.openai.codexextension.json`.
57
+ 1. Reads the existing `com.openai.codexextension.json` in that browser's directory.
53
58
  2. Backs it up in the same directory with a timestamped suffix.
54
59
  3. Writes a new manifest pointing to this project's CLI.
55
- 4. Records restore metadata in `~/.codex-control-chrome-mcp/install-state.json`.
60
+ 4. Records restore metadata in `~/.codex-control-chrome-mcp/install-state.json` (Chrome) or `install-state.<browser>.json` (other browsers), so browsers never clobber each other's state.
56
61
  5. Records the genuine OpenAI host path for proxy mode when available.
57
62
 
58
63
  ### Identifying the official host
@@ -70,16 +75,18 @@ When choosing the proxy target it prefers the **live** manifest (most up to date
70
75
  skips paths that no longer exist, so a renamed or removed host is never recorded as the
71
76
  official target. `status` reports these signals under `classification`.
72
77
 
73
- Example manifest path on macOS:
78
+ Example manifest paths on macOS:
74
79
 
75
80
  ```text
76
81
  ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.openai.codexextension.json
82
+ ~/Library/Application Support/Microsoft Edge/NativeMessagingHosts/com.openai.codexextension.json
77
83
  ```
78
84
 
79
- Example manifest path on Linux:
85
+ Example manifest paths on Linux:
80
86
 
81
87
  ```text
82
88
  ~/.config/google-chrome/NativeMessagingHosts/com.openai.codexextension.json
89
+ ~/.config/microsoft-edge/NativeMessagingHosts/com.openai.codexextension.json
83
90
  ```
84
91
 
85
92
  ## MCP Client Config
@@ -120,7 +127,7 @@ For local development:
120
127
  npx -y codex-control-chrome-mcp@latest uninstall-native-host
121
128
  ```
122
129
 
123
- This restores the backed-up official manifest if one was recorded.
130
+ This restores the backed-up official manifest if one was recorded. Without `--browser` it uninstalls from every browser it previously installed for; pass `--browser <chrome|edge|all>` to scope it.
124
131
 
125
132
  Use `--force` only if the current manifest was manually edited and you still want to restore/remove it:
126
133
 
@@ -134,11 +141,13 @@ npx -y codex-control-chrome-mcp@latest uninstall-native-host --force
134
141
  npx -y codex-control-chrome-mcp@latest status
135
142
  ```
136
143
 
137
- This prints:
144
+ This prints, per browser under `install.chrome` / `install.edge`:
138
145
 
139
146
  - active native host manifest
140
147
  - saved install state
141
- - discovered bridge sockets under `/tmp/codex-browser-use`
148
+ - the `registered` flag and live-manifest `classification`
149
+
150
+ and the discovered bridge sockets under `/tmp/codex-browser-use`. Pass `--browser` to scope it to one browser.
142
151
 
143
152
  ## Operational Notes
144
153
 
@@ -167,8 +176,8 @@ If `status` shows no sockets:
167
176
  1. Confirm Chrome is running with the profile where the extension is installed.
168
177
  2. Reload or disable/enable the Codex Chrome Extension.
169
178
  3. Run `npx -y codex-control-chrome-mcp@latest status`.
170
- 4. Check `registered`. If it is `false` and `classification.kind` is `official-openai`, a Codex update reclaimed the manifest. The stdio server re-asserts it on next startup; reload the extension or restart Chrome to reconnect (or run `install-native-host` to repair immediately).
171
- 5. When `registered` is `true`, the active manifest points to `~/.codex-control-chrome-mcp/native-host-launcher` (or your local launcher) and the extension just needs to connect.
179
+ 4. Check `install.<browser>.registered`. If it is `false` and `classification.kind` is `official-openai`, a Codex update reclaimed that browser's manifest. The stdio server re-asserts it on next startup; reload the extension or restart the browser to reconnect (or run `install-native-host` to repair immediately).
180
+ 5. When `registered` is `true`, the active manifest points to `~/.codex-control-chrome-mcp/native-host-launcher` (or the `-edge` variant, or your local launcher) and the extension just needs to connect.
172
181
 
173
182
  If Codex App integration is affected, uninstall this project:
174
183
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-control-chrome-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "MCP server and native host bridge for controlling Chrome through the Codex Chrome extension.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.js CHANGED
@@ -1,41 +1,60 @@
1
1
  import { runMcpServer } from "./mcp-server.js";
2
2
  import { runNativeHost } from "./native-host.js";
3
- import { installNativeHost, nativeHostInstallStatus, uninstallNativeHost } from "./installer.js";
3
+ import {
4
+ detectInstalledBrowsers,
5
+ detectManagedBrowsers,
6
+ installNativeHost,
7
+ nativeHostInstallStatus,
8
+ uninstallNativeHost,
9
+ } from "./installer.js";
10
+ import { DEFAULT_BROWSER, SUPPORTED_BROWSERS } from "./constants.js";
4
11
  import { listBridgeSockets } from "./bridge-client.js";
5
12
  import { probeLengthPrefixedSocket } from "./probe.js";
6
13
 
7
14
  export async function runCli(argv) {
8
15
  const args = argv.slice(2);
9
- const command = args[0];
16
+ const invocation = classifyInvocation(args);
10
17
 
11
- if (args.includes("--stdio") || command === "stdio") {
18
+ if (invocation.mode === "mcp") {
12
19
  await runMcpServer();
13
20
  return;
14
21
  }
15
22
 
16
- if (args.includes("--native-host") || looksLikeChromeOrigin(command)) {
17
- await runNativeHost({ origin: looksLikeChromeOrigin(command) ? command : argv[3] });
23
+ if (invocation.mode === "native-host") {
24
+ await runNativeHost({ origin: invocation.origin ?? argv[3], browser: invocation.browser });
18
25
  return;
19
26
  }
20
27
 
28
+ const command = invocation.command;
21
29
  switch (command) {
22
30
  case "install-native-host": {
23
31
  const binPath = readOption(args, "--bin");
24
32
  const noProxy = args.includes("--no-proxy");
25
- const result = await installNativeHost({ binPath, proxy: !noProxy });
33
+ const browsers = await resolveBrowsers(readOption(args, "--browser"), "install");
34
+ const result = {};
35
+ for (const browser of browsers) {
36
+ result[browser] = await installNativeHost({ binPath, proxy: !noProxy, browser });
37
+ }
26
38
  printJson(result);
27
39
  return;
28
40
  }
29
41
  case "uninstall-native-host": {
30
- const result = await uninstallNativeHost({ force: args.includes("--force") });
42
+ const force = args.includes("--force");
43
+ const browsers = await resolveBrowsers(readOption(args, "--browser"), "uninstall");
44
+ const result = {};
45
+ for (const browser of browsers) {
46
+ result[browser] = await uninstallNativeHost({ force, browser });
47
+ }
31
48
  printJson(result);
32
49
  return;
33
50
  }
34
51
  case "status": {
35
- printJson({
36
- install: await nativeHostInstallStatus(),
37
- sockets: await listBridgeSockets(),
38
- });
52
+ const browsers = await resolveBrowsers(readOption(args, "--browser"), "status");
53
+ const install = {};
54
+ for (const browser of browsers) {
55
+ install[browser] = await nativeHostInstallStatus({ browser });
56
+ }
57
+ printJson({ install, sockets: await listBridgeSockets() });
39
58
  return;
40
59
  }
41
60
  case "probe-socket": {
@@ -57,6 +76,31 @@ export async function runCli(argv) {
57
76
  }
58
77
  }
59
78
 
79
+ /**
80
+ * Decide how an invocation should be handled, purely from the CLI args.
81
+ *
82
+ * Chrome/Edge launch the native host by appending the extension origin as a
83
+ * positional arg, and the install launcher prepends `--browser <name>`. That
84
+ * prefix shifts the origin out of `args[0]`, so both the mode check and the
85
+ * origin lookup must scan by shape rather than position. (A 1.2.0 regression
86
+ * only checked `args[0]` here, so the launcher's `--browser`-prefixed argv fell
87
+ * through to `Unknown command: --browser` and the native host exited on every
88
+ * connect.)
89
+ */
90
+ export function classifyInvocation(args) {
91
+ if (args.includes("--stdio") || args[0] === "stdio") {
92
+ return { mode: "mcp" };
93
+ }
94
+ if (args.includes("--native-host") || args.some(looksLikeChromeOrigin)) {
95
+ return {
96
+ mode: "native-host",
97
+ origin: args.find(looksLikeChromeOrigin),
98
+ browser: normalizeBrowser(readOption(args, "--browser")),
99
+ };
100
+ }
101
+ return { mode: "command", command: args[0] };
102
+ }
103
+
60
104
  function looksLikeChromeOrigin(value) {
61
105
  return typeof value === "string" && value.startsWith("chrome-extension://");
62
106
  }
@@ -66,6 +110,39 @@ function readOption(args, name) {
66
110
  return index >= 0 ? args[index + 1] : undefined;
67
111
  }
68
112
 
113
+ function normalizeBrowser(value) {
114
+ if (value == null) return DEFAULT_BROWSER;
115
+ const browser = String(value).toLowerCase();
116
+ if (!SUPPORTED_BROWSERS.includes(browser)) {
117
+ throw new Error(
118
+ `Unsupported browser: ${value}. Supported values: ${SUPPORTED_BROWSERS.join(", ")}, all.`,
119
+ );
120
+ }
121
+ return browser;
122
+ }
123
+
124
+ /**
125
+ * Map an optional `--browser` selector to the list of browsers a command acts
126
+ * on. With no selector each command auto-detects: install targets browsers
127
+ * present on the machine, uninstall targets browsers we previously installed,
128
+ * and status reports every supported browser.
129
+ */
130
+ async function resolveBrowsers(selector, mode) {
131
+ if (selector && selector !== "all") {
132
+ return [normalizeBrowser(selector)];
133
+ }
134
+ if (selector === "all" || mode === "status") {
135
+ return [...SUPPORTED_BROWSERS];
136
+ }
137
+ if (mode === "install") {
138
+ const present = await detectInstalledBrowsers();
139
+ return present.length ? present : [DEFAULT_BROWSER];
140
+ }
141
+ // uninstall
142
+ const managed = await detectManagedBrowsers();
143
+ return managed.length ? managed : [DEFAULT_BROWSER];
144
+ }
145
+
69
146
  function printJson(value) {
70
147
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
71
148
  }
@@ -76,14 +153,16 @@ function printHelp() {
76
153
  Usage:
77
154
  codex-control-chrome-mcp --stdio
78
155
  codex-control-chrome-mcp --native-host
79
- codex-control-chrome-mcp install-native-host [--bin /path/to/codex-control-chrome-mcp] [--no-proxy]
80
- codex-control-chrome-mcp uninstall-native-host [--force]
81
- codex-control-chrome-mcp status
156
+ codex-control-chrome-mcp install-native-host [--bin /path/to/codex-control-chrome-mcp] [--no-proxy] [--browser <chrome|edge|all>]
157
+ codex-control-chrome-mcp uninstall-native-host [--force] [--browser <chrome|edge|all>]
158
+ codex-control-chrome-mcp status [--browser <chrome|edge|all>]
82
159
  codex-control-chrome-mcp probe-socket <socket-path>
83
160
 
84
161
  Notes:
85
162
  --stdio is the MCP server mode for Agent tools.
86
- --native-host is normally launched by Chrome through Native Messaging.
163
+ --native-host is normally launched by Chrome/Edge through Native Messaging.
87
164
  install-native-host without --bin writes a stable launcher that runs npx.
165
+ --browser selects a browser. Without it, install/uninstall auto-detect the
166
+ browsers present (or previously installed) and status reports both.
88
167
  `);
89
168
  }
package/src/constants.js CHANGED
@@ -10,38 +10,61 @@ export const APP_STATE_DIR = path.join(os.homedir(), ".codex-control-chrome-mcp"
10
10
  export const APP_CONFIG_PATH = path.join(APP_STATE_DIR, "config.json");
11
11
  export const INSTALL_STATE_PATH = path.join(APP_STATE_DIR, "install-state.json");
12
12
 
13
- export function nativeHostManifestPath() {
14
- const home = os.homedir();
15
- switch (process.platform) {
16
- case "darwin":
17
- return path.join(
18
- home,
19
- "Library",
20
- "Application Support",
21
- "Google",
22
- "Chrome",
23
- "NativeMessagingHosts",
24
- `${CODEX_NATIVE_HOST_NAME}.json`,
25
- );
26
- case "linux":
27
- return path.join(
28
- home,
29
- ".config",
30
- "google-chrome",
31
- "NativeMessagingHosts",
32
- `${CODEX_NATIVE_HOST_NAME}.json`,
33
- );
34
- case "win32":
35
- return path.join(
36
- home,
37
- "AppData",
38
- "Local",
39
- "Google",
40
- "Chrome",
41
- "NativeMessagingHosts",
42
- `${CODEX_NATIVE_HOST_NAME}.json`,
43
- );
44
- default:
45
- throw new Error(`Unsupported platform: ${process.platform}`);
13
+ // Chromium browsers that ship the Codex extension and use the same Native
14
+ // Messaging mechanism. Edge installs Chrome Web Store extensions under the
15
+ // same `chrome-extension://<id>` origin, so one manifest works for both.
16
+ export const DEFAULT_BROWSER = "chrome";
17
+ export const SUPPORTED_BROWSERS = ["chrome", "edge"];
18
+
19
+ // Per-platform location of each browser's user profile directory. The Native
20
+ // Messaging host manifest lives in a `NativeMessagingHosts` subdirectory, and
21
+ // the profile directory's existence is our "is this browser installed" signal.
22
+ // (On Windows native hosts are registered through the registry, not a file in
23
+ // this directory, which is why Windows is unsupported for now.)
24
+ const BROWSER_PROFILE_SEGMENTS = {
25
+ chrome: {
26
+ darwin: ["Library", "Application Support", "Google", "Chrome"],
27
+ linux: [".config", "google-chrome"],
28
+ win32: ["AppData", "Local", "Google", "Chrome"],
29
+ },
30
+ edge: {
31
+ darwin: ["Library", "Application Support", "Microsoft Edge"],
32
+ linux: [".config", "microsoft-edge"],
33
+ win32: ["AppData", "Local", "Microsoft", "Edge"],
34
+ },
35
+ };
36
+
37
+ export function browserProfileDir(browser = DEFAULT_BROWSER) {
38
+ const perPlatform = BROWSER_PROFILE_SEGMENTS[browser];
39
+ if (!perPlatform) {
40
+ throw new Error(`Unsupported browser: ${browser}`);
41
+ }
42
+ const segments = perPlatform[process.platform];
43
+ if (!segments) {
44
+ throw new Error(`Unsupported platform: ${process.platform}`);
46
45
  }
46
+ return path.join(os.homedir(), ...segments);
47
+ }
48
+
49
+ export function nativeHostManifestPath(browser = DEFAULT_BROWSER) {
50
+ return path.join(
51
+ browserProfileDir(browser),
52
+ "NativeMessagingHosts",
53
+ `${CODEX_NATIVE_HOST_NAME}.json`,
54
+ );
55
+ }
56
+
57
+ // Chrome keeps the original unsuffixed state filenames so installs from before
58
+ // multi-browser support remain valid; other browsers get a per-browser suffix
59
+ // so a second install never clobbers the first one's restore/self-heal metadata.
60
+ export function configPathFor(browser = DEFAULT_BROWSER) {
61
+ return browser === DEFAULT_BROWSER
62
+ ? APP_CONFIG_PATH
63
+ : path.join(APP_STATE_DIR, `config.${browser}.json`);
64
+ }
65
+
66
+ export function installStatePathFor(browser = DEFAULT_BROWSER) {
67
+ return browser === DEFAULT_BROWSER
68
+ ? INSTALL_STATE_PATH
69
+ : path.join(APP_STATE_DIR, `install-state.${browser}.json`);
47
70
  }
package/src/installer.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import {
4
- APP_CONFIG_PATH,
5
4
  APP_STATE_DIR,
6
5
  CODEX_EXTENSION_ID,
7
6
  CODEX_NATIVE_HOST_NAME,
8
- INSTALL_STATE_PATH,
7
+ DEFAULT_BROWSER,
8
+ SUPPORTED_BROWSERS,
9
+ browserProfileDir,
10
+ configPathFor,
11
+ installStatePathFor,
9
12
  nativeHostManifestPath,
10
13
  } from "./constants.js";
11
14
  import {
@@ -18,14 +21,20 @@ import {
18
21
  } from "./host-identity.js";
19
22
  import { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
20
23
 
21
- export async function installNativeHost({ binPath, proxy = true, paths = defaultInstallPaths() } = {}) {
24
+ export async function installNativeHost({
25
+ binPath,
26
+ proxy = true,
27
+ browser = DEFAULT_BROWSER,
28
+ paths = defaultInstallPaths(browser),
29
+ } = {}) {
22
30
  const { manifestPath, appStateDir, configPath, installStatePath } = paths;
31
+ const browserId = paths.browser ?? browser;
23
32
  await fs.mkdir(path.dirname(manifestPath), { recursive: true });
24
33
  await fs.mkdir(appStateDir, { recursive: true });
25
34
 
26
35
  const hostPath = binPath
27
- ? await createLocalLauncher(path.resolve(binPath), appStateDir)
28
- : await createNpxLauncher(appStateDir);
36
+ ? await createLocalLauncher(path.resolve(binPath), appStateDir, browserId)
37
+ : await createNpxLauncher(appStateDir, browserId);
29
38
  const existing = await readJsonIfExists(manifestPath);
30
39
  const previousState = await readJsonIfExists(installStatePath);
31
40
  const previousBackup = previousState?.backupPath
@@ -55,6 +64,7 @@ export async function installNativeHost({ binPath, proxy = true, paths = default
55
64
 
56
65
  const config = {
57
66
  installedAt: new Date().toISOString(),
67
+ browser: browserId,
58
68
  manifestPath,
59
69
  hostPath,
60
70
  binPath: binPath ? path.resolve(binPath) : null,
@@ -113,7 +123,10 @@ async function resolveOfficialHostPath({ existing, existingIsOurs, previousState
113
123
  * startup so a Codex update that reclaims the manifest self-heals without a
114
124
  * manual `install-native-host`. Never bootstraps a fresh install silently.
115
125
  */
116
- export async function ensureNativeHostRegistered({ paths = defaultInstallPaths() } = {}) {
126
+ export async function ensureNativeHostRegistered({
127
+ browser = DEFAULT_BROWSER,
128
+ paths = defaultInstallPaths(browser),
129
+ } = {}) {
117
130
  const { manifestPath, installStatePath } = paths;
118
131
  const state = await readJsonIfExists(installStatePath);
119
132
  if (!state || typeof state.hostPath !== "string") {
@@ -144,7 +157,11 @@ export async function ensureNativeHostRegistered({ paths = defaultInstallPaths()
144
157
  };
145
158
  }
146
159
 
147
- export async function uninstallNativeHost({ force = false, paths = defaultInstallPaths() } = {}) {
160
+ export async function uninstallNativeHost({
161
+ force = false,
162
+ browser = DEFAULT_BROWSER,
163
+ paths = defaultInstallPaths(browser),
164
+ } = {}) {
148
165
  const { manifestPath, installStatePath } = paths;
149
166
  const state = await readJsonIfExists(installStatePath);
150
167
  const manifest = await readJsonIfExists(manifestPath);
@@ -168,8 +185,11 @@ export async function uninstallNativeHost({ force = false, paths = defaultInstal
168
185
  return { ok: true, action: "removed", manifestPath };
169
186
  }
170
187
 
171
- export async function nativeHostInstallStatus() {
172
- const { manifestPath, installStatePath } = defaultInstallPaths();
188
+ export async function nativeHostInstallStatus({
189
+ browser = DEFAULT_BROWSER,
190
+ paths = defaultInstallPaths(browser),
191
+ } = {}) {
192
+ const { manifestPath, installStatePath } = paths;
173
193
  const manifest = await readJsonIfExists(manifestPath);
174
194
  const state = await readJsonIfExists(installStatePath);
175
195
  const classification = await classifyHost({ manifest, hostPath: manifest?.path });
@@ -178,15 +198,47 @@ export async function nativeHostInstallStatus() {
178
198
  typeof state?.hostPath === "string" &&
179
199
  typeof manifest?.path === "string" &&
180
200
  path.resolve(manifest.path) === path.resolve(state.hostPath);
181
- return { manifestPath, manifest, state, registered, classification };
201
+ return { browser: paths.browser ?? browser, manifestPath, manifest, state, registered, classification };
202
+ }
203
+
204
+ /** Browsers whose user profile directory exists on this machine (install targets). */
205
+ export async function detectInstalledBrowsers() {
206
+ const present = [];
207
+ for (const browser of SUPPORTED_BROWSERS) {
208
+ let dir;
209
+ try {
210
+ dir = browserProfileDir(browser);
211
+ } catch {
212
+ continue; // unsupported platform for this browser
213
+ }
214
+ try {
215
+ await fs.access(dir);
216
+ present.push(browser);
217
+ } catch {
218
+ // profile directory absent -> browser not installed (or never launched)
219
+ }
220
+ }
221
+ return present;
182
222
  }
183
223
 
184
- function defaultInstallPaths() {
224
+ /** Browsers we have previously installed for (an install-state file exists). */
225
+ export async function detectManagedBrowsers() {
226
+ const managed = [];
227
+ for (const browser of SUPPORTED_BROWSERS) {
228
+ if (await readJsonIfExists(installStatePathFor(browser))) {
229
+ managed.push(browser);
230
+ }
231
+ }
232
+ return managed;
233
+ }
234
+
235
+ function defaultInstallPaths(browser = DEFAULT_BROWSER) {
185
236
  return {
186
- manifestPath: nativeHostManifestPath(),
237
+ browser,
238
+ manifestPath: nativeHostManifestPath(browser),
187
239
  appStateDir: APP_STATE_DIR,
188
- configPath: APP_CONFIG_PATH,
189
- installStatePath: INSTALL_STATE_PATH,
240
+ configPath: configPathFor(browser),
241
+ installStatePath: installStatePathFor(browser),
190
242
  };
191
243
  }
192
244
 
@@ -200,29 +252,36 @@ async function backupManifest(manifestPath) {
200
252
  return backupPath;
201
253
  }
202
254
 
203
- async function createNpxLauncher(appStateDir) {
255
+ // Each browser gets its own launcher so the native host knows which browser's
256
+ // config to read for proxy mode. Chrome keeps the original unsuffixed filename
257
+ // so manifests written before multi-browser support still resolve.
258
+ function launcherFileName(base, browser) {
259
+ return browser === DEFAULT_BROWSER ? base : `${base}-${browser}`;
260
+ }
261
+
262
+ async function createNpxLauncher(appStateDir, browser = DEFAULT_BROWSER) {
204
263
  if (process.platform === "win32") {
205
264
  throw new Error("NPX native host launcher installation is not implemented for Windows yet.");
206
265
  }
207
- const launcherPath = path.join(appStateDir, "native-host-launcher");
266
+ const launcherPath = path.join(appStateDir, launcherFileName("native-host-launcher", browser));
208
267
  const script = `#!/usr/bin/env sh
209
- exec npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} --native-host "$@"
268
+ exec npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} --native-host --browser ${browser} "$@"
210
269
  `;
211
270
  await fs.writeFile(launcherPath, script, { encoding: "utf8", mode: 0o755 });
212
271
  await fs.chmod(launcherPath, 0o755);
213
272
  return launcherPath;
214
273
  }
215
274
 
216
- async function createLocalLauncher(binPath, appStateDir) {
275
+ async function createLocalLauncher(binPath, appStateDir, browser = DEFAULT_BROWSER) {
217
276
  if (process.platform === "win32") {
218
277
  return binPath;
219
278
  }
220
279
  if (!binPath.endsWith(".js")) {
221
280
  return binPath;
222
281
  }
223
- const launcherPath = path.join(appStateDir, "native-host-local-launcher");
282
+ const launcherPath = path.join(appStateDir, launcherFileName("native-host-local-launcher", browser));
224
283
  const script = `#!/usr/bin/env sh
225
- exec ${shellQuote(process.execPath)} ${shellQuote(binPath)} "$@"
284
+ exec ${shellQuote(process.execPath)} ${shellQuote(binPath)} --browser ${browser} "$@"
226
285
  `;
227
286
  await fs.writeFile(launcherPath, script, { encoding: "utf8", mode: 0o755 });
228
287
  await fs.chmod(launcherPath, 0o755);
package/src/mcp-server.js CHANGED
@@ -3,8 +3,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { connectBridge, listBridgeSockets } from "./bridge-client.js";
6
- import { nativeHostManifestPath } from "./constants.js";
7
- import { ensureNativeHostRegistered } from "./installer.js";
6
+ import { SUPPORTED_BROWSERS, nativeHostManifestPath } from "./constants.js";
7
+ import { detectManagedBrowsers, ensureNativeHostRegistered } from "./installer.js";
8
8
  import { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
9
9
 
10
10
  export async function runMcpServer() {
@@ -32,21 +32,34 @@ export async function runMcpServer() {
32
32
  */
33
33
  async function autoRegisterNativeHost() {
34
34
  if (process.env.CODEX_CONTROL_CHROME_NO_AUTO_REGISTER === "1") return;
35
+ let browsers = [];
35
36
  try {
36
- const result = await ensureNativeHostRegistered();
37
- if (result.action === "re-registered") {
37
+ browsers = await detectManagedBrowsers();
38
+ } catch {
39
+ browsers = [];
40
+ }
41
+ for (const browser of browsers) {
42
+ try {
43
+ const result = await ensureNativeHostRegistered({ browser });
44
+ if (result.action === "re-registered") {
45
+ process.stderr.write(
46
+ `[codex-control-chrome-mcp] native host manifest for ${browser} was ${result.reason}; ` +
47
+ `re-registered it. Reload the Codex extension (or restart ${browserLabel(browser)}) ` +
48
+ "to reconnect the bridge.\n",
49
+ );
50
+ }
51
+ } catch (error) {
38
52
  process.stderr.write(
39
- `[codex-control-chrome-mcp] native host manifest was ${result.reason}; re-registered it. ` +
40
- "Reload the Codex Chrome extension (or restart Chrome) to reconnect the bridge.\n",
53
+ `[codex-control-chrome-mcp] auto-register (${browser}) skipped: ${error?.message ?? error}\n`,
41
54
  );
42
55
  }
43
- } catch (error) {
44
- process.stderr.write(
45
- `[codex-control-chrome-mcp] auto-register skipped: ${error?.message ?? error}\n`,
46
- );
47
56
  }
48
57
  }
49
58
 
59
+ function browserLabel(browser) {
60
+ return browser === "edge" ? "Edge" : "Chrome";
61
+ }
62
+
50
63
  class ChromeUseRuntime {
51
64
  constructor() {
52
65
  this.sessionId = `codex-control-chrome-mcp-${crypto.randomUUID()}`;
@@ -83,11 +96,14 @@ function registerTools(server, runtime) {
83
96
  server.registerTool(
84
97
  "chrome_status",
85
98
  {
86
- description: "Show native host manifest path and discovered bridge sockets.",
99
+ description: "Show native host manifest paths and discovered bridge sockets.",
87
100
  inputSchema: {},
88
101
  },
89
102
  async () => toolResult({
90
103
  manifestPath: nativeHostManifestPath(),
104
+ manifestPaths: Object.fromEntries(
105
+ SUPPORTED_BROWSERS.map((browser) => [browser, nativeHostManifestPath(browser)]),
106
+ ),
91
107
  sockets: await listBridgeSockets(),
92
108
  }),
93
109
  );
@@ -3,12 +3,17 @@ import fs from "node:fs/promises";
3
3
  import net from "node:net";
4
4
  import path from "node:path";
5
5
  import { spawn } from "node:child_process";
6
- import { APP_CONFIG_PATH, BRIDGE_SOCKET_DIR, BRIDGE_SOCKET_PREFIX } from "./constants.js";
6
+ import {
7
+ BRIDGE_SOCKET_DIR,
8
+ BRIDGE_SOCKET_PREFIX,
9
+ DEFAULT_BROWSER,
10
+ configPathFor,
11
+ } from "./constants.js";
7
12
  import { LengthPrefixedJsonPeer } from "./framing.js";
8
13
 
9
14
  const LOCAL_ID_PREFIX = "codex-control-chrome-mcp:";
10
15
 
11
- export async function runNativeHost({ origin = process.argv[2] } = {}) {
16
+ export async function runNativeHost({ origin = process.argv[2], browser = DEFAULT_BROWSER } = {}) {
12
17
  await fs.mkdir(BRIDGE_SOCKET_DIR, { recursive: true });
13
18
  const socketPath = path.join(
14
19
  BRIDGE_SOCKET_DIR,
@@ -21,7 +26,7 @@ export async function runNativeHost({ origin = process.argv[2] } = {}) {
21
26
  name: "chrome-extension",
22
27
  });
23
28
 
24
- const proxyPeer = await maybeStartOfficialProxy(origin);
29
+ const proxyPeer = await maybeStartOfficialProxy(origin, browser);
25
30
  const bridge = new NativeHostBridge({ extensionPeer, proxyPeer });
26
31
  await bridge.listen(socketPath);
27
32
 
@@ -201,8 +206,8 @@ class NativeHostBridge {
201
206
  }
202
207
  }
203
208
 
204
- async function maybeStartOfficialProxy(origin) {
205
- const config = await readJsonIfExists(APP_CONFIG_PATH);
209
+ async function maybeStartOfficialProxy(origin, browser = DEFAULT_BROWSER) {
210
+ const config = await readJsonIfExists(configPathFor(browser));
206
211
  const officialHostPath = config?.officialHostPath;
207
212
  if (typeof officialHostPath !== "string" || officialHostPath.length === 0) {
208
213
  return null;