codex-control-chrome-mcp 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +8 -0
- package/docs/install.md +28 -2
- package/package.json +1 -1
- package/src/host-identity.js +126 -0
- package/src/installer.js +95 -24
- package/src/mcp-server.js +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ This project follows semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.1.0] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Self-healing native host registration: the MCP stdio server re-asserts the manifest on startup when a Codex update reverts it (opt out with `CODEX_CONTROL_CHROME_NO_AUTO_REGISTER=1`).
|
|
14
|
+
- Robust identification of OpenAI's genuine host via stable signals (manifest name + extension origin, bundle path, binary format, and the macOS OpenAI code signature) instead of the binary filename.
|
|
15
|
+
- `status` now reports `registered` and a `classification` of the live manifest/host.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Proxy target resolution preferred a stale recorded path over the live manifest, so a renamed/removed official host (e.g. `extension-host` → `Codex for Chrome`) could be recorded as the proxy target. It now prefers the live official manifest and skips paths that no longer exist.
|
|
20
|
+
|
|
9
21
|
## [1.0.1] - 2026-06-26
|
|
10
22
|
|
|
11
23
|
### Added
|
package/README.md
CHANGED
|
@@ -144,6 +144,14 @@ node ./bin/codex-control-chrome-mcp.js --native-host
|
|
|
144
144
|
|
|
145
145
|
## Troubleshooting
|
|
146
146
|
|
|
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
|
+
### A Codex update stopped the bridge
|
|
150
|
+
|
|
151
|
+
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
|
+
|
|
153
|
+
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
|
+
|
|
147
155
|
If `status` shows no bridge sockets, reload Chrome or the Codex Chrome Extension after installing the native host.
|
|
148
156
|
|
|
149
157
|
If Codex App integration stops working, uninstall this native host to restore the backed-up manifest:
|
package/docs/install.md
CHANGED
|
@@ -53,7 +53,22 @@ The installer:
|
|
|
53
53
|
2. Backs it up in the same directory with a timestamped suffix.
|
|
54
54
|
3. Writes a new manifest pointing to this project's CLI.
|
|
55
55
|
4. Records restore metadata in `~/.codex-control-chrome-mcp/install-state.json`.
|
|
56
|
-
5. Records the
|
|
56
|
+
5. Records the genuine OpenAI host path for proxy mode when available.
|
|
57
|
+
|
|
58
|
+
### Identifying the official host
|
|
59
|
+
|
|
60
|
+
The installer identifies OpenAI's genuine host with stable signals instead of the
|
|
61
|
+
binary filename (OpenAI has renamed it across releases, e.g. `extension-host` →
|
|
62
|
+
`Codex for Chrome`):
|
|
63
|
+
|
|
64
|
+
- the manifest `name` (`com.openai.codexextension`) and the official extension origin,
|
|
65
|
+
- the bundle directory structure (`.../openai-bundled/.../extension-host/...`),
|
|
66
|
+
- the binary format (a compiled Mach-O/ELF executable, not a shell script),
|
|
67
|
+
- on macOS, the OpenAI code signature (Developer ID Team `2DC432GLL2`).
|
|
68
|
+
|
|
69
|
+
When choosing the proxy target it prefers the **live** manifest (most up to date) and
|
|
70
|
+
skips paths that no longer exist, so a renamed or removed host is never recorded as the
|
|
71
|
+
official target. `status` reports these signals under `classification`.
|
|
57
72
|
|
|
58
73
|
Example manifest path on macOS:
|
|
59
74
|
|
|
@@ -129,6 +144,16 @@ This prints:
|
|
|
129
144
|
|
|
130
145
|
Chrome launches the native host only when the extension connects. If no bridge socket is visible, reload Chrome or the Codex extension after installing the manifest.
|
|
131
146
|
|
|
147
|
+
### Auto re-register after Codex updates
|
|
148
|
+
|
|
149
|
+
A Codex App/extension update re-registers its own `com.openai.codexextension` manifest and reverts this project's proxy registration. To avoid a manual repair each time, the MCP stdio server re-asserts the manifest on startup when it detects it was reverted (only if it was installed before — it never bootstraps a fresh install silently). Logs go to stderr only.
|
|
150
|
+
|
|
151
|
+
Re-asserting the manifest takes effect on the extension's **next** Native Messaging connection, so reload the Codex Chrome Extension or restart Chrome once after a Codex update. Disable this behavior with:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
CODEX_CONTROL_CHROME_NO_AUTO_REGISTER=1
|
|
155
|
+
```
|
|
156
|
+
|
|
132
157
|
If the extension is missing, install the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) from Chrome Web Store and enable it in the target Chrome profile.
|
|
133
158
|
|
|
134
159
|
Stopping the MCP stdio process does not uninstall the native host. Uninstall explicitly to restore the original manifest.
|
|
@@ -142,7 +167,8 @@ If `status` shows no sockets:
|
|
|
142
167
|
1. Confirm Chrome is running with the profile where the extension is installed.
|
|
143
168
|
2. Reload or disable/enable the Codex Chrome Extension.
|
|
144
169
|
3. Run `npx -y codex-control-chrome-mcp@latest status`.
|
|
145
|
-
4. Check
|
|
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.
|
|
146
172
|
|
|
147
173
|
If Codex App integration is affected, uninstall this project:
|
|
148
174
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { APP_STATE_DIR, CODEX_EXTENSION_ID, CODEX_NATIVE_HOST_NAME } from "./constants.js";
|
|
5
|
+
|
|
6
|
+
// Description we stamp on manifests we own. Used as one (not the only) signal.
|
|
7
|
+
export const MANIFEST_DESCRIPTION = "Codex Control Chrome MCP native messaging host";
|
|
8
|
+
|
|
9
|
+
/** A manifest this project wrote (description marker, or path inside our state dir). */
|
|
10
|
+
export function isProjectManifest(manifest) {
|
|
11
|
+
if (!manifest || typeof manifest !== "object") return false;
|
|
12
|
+
if (manifest.description === MANIFEST_DESCRIPTION) return true;
|
|
13
|
+
return isOurLauncherPath(manifest.path);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** True when a host path points at a launcher we generated under APP_STATE_DIR. */
|
|
17
|
+
export function isOurLauncherPath(hostPath) {
|
|
18
|
+
if (typeof hostPath !== "string" || hostPath.length === 0) return false;
|
|
19
|
+
const resolved = path.resolve(hostPath);
|
|
20
|
+
const stateRoot = path.resolve(APP_STATE_DIR) + path.sep;
|
|
21
|
+
return resolved === path.resolve(APP_STATE_DIR) || resolved.startsWith(stateRoot);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Positive identification of OpenAI's genuine Codex native-host *manifest*,
|
|
26
|
+
* using stable identifiers (host name + the official extension id) rather than
|
|
27
|
+
* the binary filename (which OpenAI has renamed across releases).
|
|
28
|
+
*/
|
|
29
|
+
export function isOfficialCodexManifest(manifest) {
|
|
30
|
+
if (!manifest || typeof manifest !== "object") return false;
|
|
31
|
+
if (isProjectManifest(manifest)) return false;
|
|
32
|
+
if (manifest.name !== CODEX_NATIVE_HOST_NAME) return false;
|
|
33
|
+
const origins = Array.isArray(manifest.allowed_origins) ? manifest.allowed_origins : [];
|
|
34
|
+
return origins.some((origin) => originMatchesCodexExtension(origin));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function originMatchesCodexExtension(origin) {
|
|
38
|
+
return typeof origin === "string" && origin.includes(`chrome-extension://${CODEX_EXTENSION_ID}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Stable directory-structure signature of the bundled Codex host (filename-agnostic). */
|
|
42
|
+
export function hostPathLooksOfficial(hostPath) {
|
|
43
|
+
if (typeof hostPath !== "string" || hostPath.length === 0) return false;
|
|
44
|
+
const normalized = path.resolve(hostPath).split(path.sep).join("/").toLowerCase();
|
|
45
|
+
return normalized.includes("/openai-bundled/") && normalized.includes("/extension-host/");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const MAGIC = {
|
|
49
|
+
"7f454c46": "elf", // ELF
|
|
50
|
+
feedface: "macho", // Mach-O 32 LE
|
|
51
|
+
feedfacf: "macho", // Mach-O 64 LE
|
|
52
|
+
cefaedfe: "macho", // Mach-O 32 BE
|
|
53
|
+
cffaedfe: "macho", // Mach-O 64 BE
|
|
54
|
+
cafebabe: "macho", // universal (fat) — also Java .class, acceptable for a host binary
|
|
55
|
+
bebafeca: "macho",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Inspect the host file: does it exist, is it a compiled binary or a shell script? */
|
|
59
|
+
export async function inspectHostBinary(hostPath) {
|
|
60
|
+
if (typeof hostPath !== "string" || hostPath.length === 0) {
|
|
61
|
+
return { exists: false, kind: "missing", executable: false };
|
|
62
|
+
}
|
|
63
|
+
let handle;
|
|
64
|
+
try {
|
|
65
|
+
const stat = await fs.stat(hostPath);
|
|
66
|
+
if (!stat.isFile()) return { exists: true, kind: "unknown", executable: false };
|
|
67
|
+
handle = await fs.open(hostPath, "r");
|
|
68
|
+
const buf = Buffer.alloc(4);
|
|
69
|
+
const { bytesRead } = await handle.read(buf, 0, 4, 0);
|
|
70
|
+
const head = buf.subarray(0, bytesRead);
|
|
71
|
+
let kind = "unknown";
|
|
72
|
+
if (head.length >= 2 && head[0] === 0x23 && head[1] === 0x21) kind = "script"; // #!
|
|
73
|
+
else if (head.length === 4 && MAGIC[head.toString("hex")]) kind = MAGIC[head.toString("hex")];
|
|
74
|
+
else if (head.length >= 2 && head[0] === 0x4d && head[1] === 0x5a) kind = "pe"; // MZ
|
|
75
|
+
return { exists: true, kind, executable: (stat.mode & 0o111) !== 0 };
|
|
76
|
+
} catch {
|
|
77
|
+
return { exists: false, kind: "missing", executable: false };
|
|
78
|
+
} finally {
|
|
79
|
+
await handle?.close().catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Best-effort macOS code-signature check; the strongest OpenAI signal when available. */
|
|
84
|
+
export function verifyOpenAiCodesign(hostPath) {
|
|
85
|
+
if (process.platform !== "darwin" || typeof hostPath !== "string" || !hostPath) {
|
|
86
|
+
return Promise.resolve({ available: false });
|
|
87
|
+
}
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
execFile("codesign", ["-dvvv", hostPath], { timeout: 4000 }, (error, stdout, stderr) => {
|
|
90
|
+
const out = `${stdout || ""}${stderr || ""}`; // codesign -d writes to stderr
|
|
91
|
+
if (!out) return resolve({ available: false, error: error?.message });
|
|
92
|
+
const teamId = (out.match(/TeamIdentifier=(\S+)/) || [])[1] || null;
|
|
93
|
+
const authority = (out.match(/Authority=([^\r\n]+)/) || [])[1] || null;
|
|
94
|
+
const openai = /openai/i.test(out);
|
|
95
|
+
resolve({ available: true, signed: Boolean(authority), openai, teamId, authority });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Full classification of a (manifest, hostPath) pair against stable signals.
|
|
102
|
+
* `trustedOfficial` means: looks like OpenAI's real host and is safe to back up /
|
|
103
|
+
* proxy to. We require the manifest-level identity plus at least one host-level
|
|
104
|
+
* signal (bundle path, compiled binary, or an OpenAI code signature).
|
|
105
|
+
*/
|
|
106
|
+
export async function classifyHost({ manifest, hostPath } = {}) {
|
|
107
|
+
const resolvedPath = hostPath ?? manifest?.path ?? null;
|
|
108
|
+
const ours = isProjectManifest(manifest) || isOurLauncherPath(resolvedPath);
|
|
109
|
+
const officialManifest = isOfficialCodexManifest(manifest);
|
|
110
|
+
const pathOfficial = hostPathLooksOfficial(resolvedPath);
|
|
111
|
+
const binary = await inspectHostBinary(resolvedPath);
|
|
112
|
+
const codesign = await verifyOpenAiCodesign(resolvedPath);
|
|
113
|
+
|
|
114
|
+
const hostSignal = pathOfficial || codesign.openai === true || binary.kind === "macho" || binary.kind === "elf";
|
|
115
|
+
const trustedOfficial = !ours && officialManifest && hostSignal;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
ours,
|
|
119
|
+
officialManifest,
|
|
120
|
+
pathOfficial,
|
|
121
|
+
binary,
|
|
122
|
+
codesign,
|
|
123
|
+
trustedOfficial,
|
|
124
|
+
kind: ours ? "ours" : trustedOfficial ? "official-openai" : "unknown",
|
|
125
|
+
};
|
|
126
|
+
}
|
package/src/installer.js
CHANGED
|
@@ -8,10 +8,16 @@ import {
|
|
|
8
8
|
INSTALL_STATE_PATH,
|
|
9
9
|
nativeHostManifestPath,
|
|
10
10
|
} from "./constants.js";
|
|
11
|
+
import {
|
|
12
|
+
MANIFEST_DESCRIPTION,
|
|
13
|
+
classifyHost,
|
|
14
|
+
inspectHostBinary,
|
|
15
|
+
isOfficialCodexManifest,
|
|
16
|
+
isOurLauncherPath,
|
|
17
|
+
isProjectManifest,
|
|
18
|
+
} from "./host-identity.js";
|
|
11
19
|
import { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
12
20
|
|
|
13
|
-
const MANIFEST_DESCRIPTION = "Codex Control Chrome MCP native messaging host";
|
|
14
|
-
|
|
15
21
|
export async function installNativeHost({ binPath, proxy = true, paths = defaultInstallPaths() } = {}) {
|
|
16
22
|
const { manifestPath, appStateDir, configPath, installStatePath } = paths;
|
|
17
23
|
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
@@ -34,23 +40,9 @@ export async function installNativeHost({ binPath, proxy = true, paths = default
|
|
|
34
40
|
backupPath = previousState.backupPath;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} else if (
|
|
41
|
-
proxy &&
|
|
42
|
-
typeof previousState?.officialHostPath === "string" &&
|
|
43
|
-
path.resolve(previousState.officialHostPath) !== path.resolve(hostPath)
|
|
44
|
-
) {
|
|
45
|
-
officialHostPath = previousState.officialHostPath;
|
|
46
|
-
} else if (
|
|
47
|
-
proxy &&
|
|
48
|
-
existing?.path &&
|
|
49
|
-
!existingIsOurs &&
|
|
50
|
-
path.resolve(existing.path) !== path.resolve(hostPath)
|
|
51
|
-
) {
|
|
52
|
-
officialHostPath = existing.path;
|
|
53
|
-
}
|
|
43
|
+
const officialHostPath = proxy
|
|
44
|
+
? await resolveOfficialHostPath({ existing, existingIsOurs, previousState, previousBackup, hostPath })
|
|
45
|
+
: null;
|
|
54
46
|
|
|
55
47
|
const manifest = {
|
|
56
48
|
allowed_origins: [`chrome-extension://${CODEX_EXTENSION_ID}/`],
|
|
@@ -65,6 +57,7 @@ export async function installNativeHost({ binPath, proxy = true, paths = default
|
|
|
65
57
|
installedAt: new Date().toISOString(),
|
|
66
58
|
manifestPath,
|
|
67
59
|
hostPath,
|
|
60
|
+
binPath: binPath ? path.resolve(binPath) : null,
|
|
68
61
|
launchMode: binPath ? "direct-bin" : "npx-launcher",
|
|
69
62
|
backupPath,
|
|
70
63
|
officialHostPath,
|
|
@@ -75,6 +68,82 @@ export async function installNativeHost({ binPath, proxy = true, paths = default
|
|
|
75
68
|
return config;
|
|
76
69
|
}
|
|
77
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the genuine OpenAI host to proxy to. Authority order:
|
|
73
|
+
* 1. the live manifest, when it is verifiably OpenAI's (most up to date),
|
|
74
|
+
* 2. the previously recorded official host,
|
|
75
|
+
* 3. the backed-up manifest's path.
|
|
76
|
+
* Self-references and our own launcher are dropped, and a candidate whose file
|
|
77
|
+
* still exists is preferred so a renamed/removed host (e.g. after a Codex
|
|
78
|
+
* update) never gets recorded as the official target.
|
|
79
|
+
*/
|
|
80
|
+
async function resolveOfficialHostPath({ existing, existingIsOurs, previousState, previousBackup, hostPath }) {
|
|
81
|
+
const candidates = [];
|
|
82
|
+
if (existing?.path && !existingIsOurs && isOfficialCodexManifest(existing)) {
|
|
83
|
+
candidates.push(existing.path);
|
|
84
|
+
}
|
|
85
|
+
if (typeof previousState?.officialHostPath === "string") {
|
|
86
|
+
candidates.push(previousState.officialHostPath);
|
|
87
|
+
}
|
|
88
|
+
if (previousBackup?.path && !isProjectManifest(previousBackup)) {
|
|
89
|
+
candidates.push(previousBackup.path);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
const unique = [];
|
|
94
|
+
for (const candidate of candidates) {
|
|
95
|
+
const resolved = path.resolve(candidate);
|
|
96
|
+
if (resolved === path.resolve(hostPath) || isOurLauncherPath(candidate) || seen.has(resolved)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
seen.add(resolved);
|
|
100
|
+
unique.push(candidate);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const candidate of unique) {
|
|
104
|
+
if ((await inspectHostBinary(candidate)).exists) return candidate;
|
|
105
|
+
}
|
|
106
|
+
// None of the candidates exist on disk; fall back to the most authoritative
|
|
107
|
+
// one (the live official manifest) for diagnostics rather than inventing null.
|
|
108
|
+
return unique[0] ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Idempotently re-assert a previously-established install. Used on stdio
|
|
113
|
+
* startup so a Codex update that reclaims the manifest self-heals without a
|
|
114
|
+
* manual `install-native-host`. Never bootstraps a fresh install silently.
|
|
115
|
+
*/
|
|
116
|
+
export async function ensureNativeHostRegistered({ paths = defaultInstallPaths() } = {}) {
|
|
117
|
+
const { manifestPath, installStatePath } = paths;
|
|
118
|
+
const state = await readJsonIfExists(installStatePath);
|
|
119
|
+
if (!state || typeof state.hostPath !== "string") {
|
|
120
|
+
return { action: "skip", reason: "not-installed" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const manifest = await readJsonIfExists(manifestPath);
|
|
124
|
+
const pointsToUs =
|
|
125
|
+
isProjectManifest(manifest) &&
|
|
126
|
+
typeof manifest?.path === "string" &&
|
|
127
|
+
path.resolve(manifest.path) === path.resolve(state.hostPath);
|
|
128
|
+
const launcherExists = (await inspectHostBinary(state.hostPath)).exists;
|
|
129
|
+
if (pointsToUs && launcherExists) {
|
|
130
|
+
return { action: "ok", reason: "already-registered" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = await installNativeHost({
|
|
134
|
+
binPath: state.launchMode === "direct-bin" ? state.binPath ?? undefined : undefined,
|
|
135
|
+
proxy: state.proxy !== false,
|
|
136
|
+
paths,
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
action: "re-registered",
|
|
140
|
+
reason: launcherExists ? "manifest-reverted" : "launcher-missing",
|
|
141
|
+
manifestPath,
|
|
142
|
+
hostPath: result.hostPath,
|
|
143
|
+
officialHostPath: result.officialHostPath,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
78
147
|
export async function uninstallNativeHost({ force = false, paths = defaultInstallPaths() } = {}) {
|
|
79
148
|
const { manifestPath, installStatePath } = paths;
|
|
80
149
|
const state = await readJsonIfExists(installStatePath);
|
|
@@ -103,7 +172,13 @@ export async function nativeHostInstallStatus() {
|
|
|
103
172
|
const { manifestPath, installStatePath } = defaultInstallPaths();
|
|
104
173
|
const manifest = await readJsonIfExists(manifestPath);
|
|
105
174
|
const state = await readJsonIfExists(installStatePath);
|
|
106
|
-
|
|
175
|
+
const classification = await classifyHost({ manifest, hostPath: manifest?.path });
|
|
176
|
+
const registered =
|
|
177
|
+
isProjectManifest(manifest) &&
|
|
178
|
+
typeof state?.hostPath === "string" &&
|
|
179
|
+
typeof manifest?.path === "string" &&
|
|
180
|
+
path.resolve(manifest.path) === path.resolve(state.hostPath);
|
|
181
|
+
return { manifestPath, manifest, state, registered, classification };
|
|
107
182
|
}
|
|
108
183
|
|
|
109
184
|
function defaultInstallPaths() {
|
|
@@ -138,10 +213,6 @@ exec npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} --native-host "$@"
|
|
|
138
213
|
return launcherPath;
|
|
139
214
|
}
|
|
140
215
|
|
|
141
|
-
function isProjectManifest(manifest) {
|
|
142
|
-
return manifest?.description === MANIFEST_DESCRIPTION;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
216
|
async function createLocalLauncher(binPath, appStateDir) {
|
|
146
217
|
if (process.platform === "win32") {
|
|
147
218
|
return binPath;
|
package/src/mcp-server.js
CHANGED
|
@@ -4,9 +4,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { connectBridge, listBridgeSockets } from "./bridge-client.js";
|
|
6
6
|
import { nativeHostManifestPath } from "./constants.js";
|
|
7
|
+
import { ensureNativeHostRegistered } from "./installer.js";
|
|
7
8
|
import { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
8
9
|
|
|
9
10
|
export async function runMcpServer() {
|
|
11
|
+
await autoRegisterNativeHost();
|
|
10
12
|
const runtime = new ChromeUseRuntime();
|
|
11
13
|
const server = new McpServer({
|
|
12
14
|
name: PACKAGE_NAME,
|
|
@@ -19,6 +21,32 @@ export async function runMcpServer() {
|
|
|
19
21
|
await server.connect(transport);
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* On stdio startup, re-assert our native-host manifest if a Codex update (or
|
|
26
|
+
* anything else) reverted it. Best-effort and silent: it only logs to stderr
|
|
27
|
+
* (stdout is the MCP JSON-RPC channel) and never blocks server startup.
|
|
28
|
+
* Opt out with CODEX_CONTROL_CHROME_NO_AUTO_REGISTER=1.
|
|
29
|
+
*
|
|
30
|
+
* Note: re-asserting the manifest only takes effect on the Chrome extension's
|
|
31
|
+
* next Native Messaging connection (reload the extension or restart Chrome).
|
|
32
|
+
*/
|
|
33
|
+
async function autoRegisterNativeHost() {
|
|
34
|
+
if (process.env.CODEX_CONTROL_CHROME_NO_AUTO_REGISTER === "1") return;
|
|
35
|
+
try {
|
|
36
|
+
const result = await ensureNativeHostRegistered();
|
|
37
|
+
if (result.action === "re-registered") {
|
|
38
|
+
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",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
`[codex-control-chrome-mcp] auto-register skipped: ${error?.message ?? error}\n`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
class ChromeUseRuntime {
|
|
23
51
|
constructor() {
|
|
24
52
|
this.sessionId = `codex-control-chrome-mcp-${crypto.randomUUID()}`;
|