codex-control-chrome-mcp 1.0.0 → 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 +42 -0
- package/LICENSE +21 -0
- package/README.md +127 -15
- package/SECURITY.md +37 -0
- package/docs/install.md +64 -0
- package/docs/release.md +54 -0
- package/package.json +9 -3
- package/src/host-identity.js +126 -0
- package/src/installer.js +124 -43
- package/src/mcp-server.js +31 -2
- package/src/package-meta.js +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
This project follows semantic versioning.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
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
|
+
|
|
21
|
+
## [1.0.1] - 2026-06-26
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Open source governance files.
|
|
26
|
+
- CI and npm trusted publishing workflows.
|
|
27
|
+
- Node.js test coverage for framing, RPC, and installer behavior.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Runtime package name and version are now read from `package.json` instead of being hard-coded in implementation files.
|
|
32
|
+
|
|
33
|
+
## [1.0.0] - 2026-06-26
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Initial npm package.
|
|
38
|
+
- MCP stdio server.
|
|
39
|
+
- Chrome Native Messaging host bridge.
|
|
40
|
+
- Native host install and uninstall commands with manifest backup.
|
|
41
|
+
- Basic Chrome tab, navigation, screenshot, CDP, and event MCP tools.
|
|
42
|
+
- Skill documentation for Agent tool usage.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iola1999
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,55 +1,167 @@
|
|
|
1
1
|
# Codex Control Chrome MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/codex-control-chrome-mcp)
|
|
4
|
+
[](https://github.com/iola1999/codex-control-chrome-mcp/actions/workflows/ci.yml)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
Expose the Codex Chrome Extension flow to other Agent tools through MCP.
|
|
8
|
+
|
|
9
|
+
This project lets MCP clients control the user's normal Chrome profile through the installed [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg). It is useful when an Agent needs existing tabs, cookies, logged-in sessions, installed extensions, screenshots, console/network events, or raw Chrome DevTools Protocol commands.
|
|
10
|
+
|
|
11
|
+
This is an independent community project. It is not affiliated with OpenAI, Codex, Google, or Chrome.
|
|
4
12
|
|
|
5
13
|
- NPM: [codex-control-chrome-mcp](https://www.npmjs.com/package/codex-control-chrome-mcp)
|
|
6
14
|
- GitHub: [iola1999/codex-control-chrome-mcp](https://github.com/iola1999/codex-control-chrome-mcp)
|
|
7
15
|
- Chrome Extension: [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg)
|
|
8
16
|
- Skill: [`skills/codex-control-chrome-mcp/SKILL.md`](./skills/codex-control-chrome-mcp/SKILL.md)
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
## Status
|
|
19
|
+
|
|
20
|
+
The current implementation provides:
|
|
11
21
|
|
|
12
22
|
- official `@modelcontextprotocol/sdk` stdio server
|
|
13
23
|
- Chrome Native Messaging host mode
|
|
14
|
-
- MCP stdio server mode
|
|
15
24
|
- install/uninstall commands with manifest backup
|
|
16
25
|
- JSON-RPC bridge socket under `/tmp/codex-browser-use`
|
|
17
|
-
-
|
|
26
|
+
- Chrome tab, navigation, screenshot, CDP, and event MCP tools
|
|
27
|
+
- bundled skill documentation for Agent tool ordering
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
## Platform Support
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
| Platform | Status | Notes |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| macOS | Tested | Primary supported platform. |
|
|
34
|
+
| Linux | Experimental | Manifest path targets Google Chrome under `~/.config/google-chrome`. |
|
|
35
|
+
| Windows | Unsupported | Native Messaging registration uses registry keys on Windows and is not implemented yet. |
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
Node.js 20 or newer is required. npm Trusted Publishing for releases uses Node.js 24 in GitHub Actions.
|
|
26
38
|
|
|
27
|
-
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
Install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome profile you want to automate.
|
|
42
|
+
|
|
43
|
+
Install the Chrome native host:
|
|
28
44
|
|
|
29
45
|
```bash
|
|
30
|
-
npx -y codex-control-chrome-mcp@latest
|
|
46
|
+
npx -y codex-control-chrome-mcp@latest install-native-host
|
|
31
47
|
```
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
Configure your Agent to use the MCP stdio server:
|
|
34
50
|
|
|
35
51
|
```json
|
|
36
52
|
{
|
|
37
53
|
"type": "stdio",
|
|
38
54
|
"command": "npx",
|
|
39
|
-
"args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"]
|
|
55
|
+
"args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"],
|
|
56
|
+
"startup_timeout_sec": 30
|
|
40
57
|
}
|
|
41
58
|
```
|
|
42
59
|
|
|
60
|
+
Check status:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx -y codex-control-chrome-mcp@latest status
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Uninstall and restore the previous Codex native host manifest:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx -y codex-control-chrome-mcp@latest uninstall-native-host
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Agent Skill
|
|
73
|
+
|
|
43
74
|
If your Agent supports skills, install or reference the bundled skill folder:
|
|
44
75
|
|
|
45
76
|
```text
|
|
46
77
|
skills/codex-control-chrome-mcp
|
|
47
78
|
```
|
|
48
79
|
|
|
49
|
-
The skill documents the recommended MCP tool order for
|
|
80
|
+
The skill documents the recommended MCP tool order for:
|
|
81
|
+
|
|
82
|
+
- status checks
|
|
83
|
+
- tab listing and claiming
|
|
84
|
+
- CDP attach and command execution
|
|
85
|
+
- screenshots
|
|
86
|
+
- network and console event reads
|
|
87
|
+
- tab finalization
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
## Security Notes
|
|
90
|
+
|
|
91
|
+
This project controls the user's normal Chrome profile. MCP clients connected to it can inspect page contents and send raw CDP commands to claimed tabs.
|
|
92
|
+
|
|
93
|
+
Only install and run it on machines and Chrome profiles you own or are explicitly authorized to automate. Do not send browser cookies, password stores, profile databases, tokens, or private session files in issues or logs.
|
|
94
|
+
|
|
95
|
+
See [SECURITY.md](./SECURITY.md) for the security model and reporting process.
|
|
96
|
+
|
|
97
|
+
## How It Works
|
|
98
|
+
|
|
99
|
+
The Codex Chrome Extension does not need an external Chrome remote debugging port. It uses Chrome Native Messaging and the extension's `chrome.debugger` permission:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
Chrome extension
|
|
103
|
+
-> chrome.runtime.connectNative("com.openai.codexextension")
|
|
104
|
+
-> codex-control-chrome-mcp native host
|
|
105
|
+
-> local MCP bridge socket
|
|
106
|
+
-> chrome.debugger.attach / chrome.debugger.sendCommand
|
|
107
|
+
-> CDP
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Because the control entrypoint is inside the normal Chrome profile, existing cookies, logged-in sessions, tabs, and extensions can be reused.
|
|
111
|
+
|
|
112
|
+
Read more:
|
|
52
113
|
|
|
53
114
|
- [Architecture](./docs/architecture.md)
|
|
54
115
|
- [Install And Uninstall](./docs/install.md)
|
|
55
116
|
- [Official Socket Probe Notes](./docs/socket-probe.md)
|
|
117
|
+
- [Release Process](./docs/release.md)
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
Install dependencies:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm ci
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Run checks:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm run ci
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Run the MCP server locally:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
node ./bin/codex-control-chrome-mcp.js --stdio
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Run native host mode locally for development:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
node ./bin/codex-control-chrome-mcp.js --native-host
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Troubleshooting
|
|
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
|
+
|
|
155
|
+
If `status` shows no bridge sockets, reload Chrome or the Codex Chrome Extension after installing the native host.
|
|
156
|
+
|
|
157
|
+
If Codex App integration stops working, uninstall this native host to restore the backed-up manifest:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npx -y codex-control-chrome-mcp@latest uninstall-native-host
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
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
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Security fixes are provided for the latest published npm version.
|
|
6
|
+
|
|
7
|
+
## Reporting a Vulnerability
|
|
8
|
+
|
|
9
|
+
Please report security issues privately by emailing `iola1999@foxmail.com`.
|
|
10
|
+
|
|
11
|
+
Include:
|
|
12
|
+
|
|
13
|
+
- affected version
|
|
14
|
+
- operating system
|
|
15
|
+
- Chrome version and profile type
|
|
16
|
+
- MCP client name and version
|
|
17
|
+
- reproduction steps
|
|
18
|
+
- relevant `codex-control-chrome-mcp status` output with sensitive paths or data redacted
|
|
19
|
+
|
|
20
|
+
Do not include browser cookies, password-store data, session databases, or other private Chrome profile files in reports.
|
|
21
|
+
|
|
22
|
+
## Security Model
|
|
23
|
+
|
|
24
|
+
This project controls the user's normal Chrome profile through the Codex Chrome Extension and Chrome DevTools Protocol. That can expose sensitive browser state, including logged-in sessions and page contents.
|
|
25
|
+
|
|
26
|
+
The trusted boundary is the local user account. Only install and run this tool on machines and Chrome profiles you own or are explicitly authorized to automate.
|
|
27
|
+
|
|
28
|
+
This project is not affiliated with OpenAI, Codex, Google, or Chrome. It reuses the installed Codex Chrome Extension native messaging host name so the extension can connect to this local bridge.
|
|
29
|
+
|
|
30
|
+
## Known Security Tradeoffs
|
|
31
|
+
|
|
32
|
+
- The native host manifest for `com.openai.codexextension` is replaced during install and restored during uninstall.
|
|
33
|
+
- The local bridge socket is intended for same-user local access only.
|
|
34
|
+
- MCP clients connected to this server can send raw CDP commands to claimed tabs.
|
|
35
|
+
- The project does not attempt to enforce per-site or per-command policy.
|
|
36
|
+
|
|
37
|
+
For operational details, see `docs/architecture.md` and `docs/install.md`.
|
package/docs/install.md
CHANGED
|
@@ -17,6 +17,16 @@ The current implementation provides:
|
|
|
17
17
|
|
|
18
18
|
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
19
|
|
|
20
|
+
This is an independent community project. It is not affiliated with OpenAI, Codex, Google, or Chrome.
|
|
21
|
+
|
|
22
|
+
## Platform Support
|
|
23
|
+
|
|
24
|
+
| Platform | Status | Notes |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| 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. |
|
|
29
|
+
|
|
20
30
|
## Install
|
|
21
31
|
|
|
22
32
|
First install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome profile you want to automate.
|
|
@@ -43,6 +53,22 @@ The installer:
|
|
|
43
53
|
2. Backs it up in the same directory with a timestamped suffix.
|
|
44
54
|
3. Writes a new manifest pointing to this project's CLI.
|
|
45
55
|
4. Records restore metadata in `~/.codex-control-chrome-mcp/install-state.json`.
|
|
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`.
|
|
46
72
|
|
|
47
73
|
Example manifest path on macOS:
|
|
48
74
|
|
|
@@ -50,6 +76,12 @@ Example manifest path on macOS:
|
|
|
50
76
|
~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.openai.codexextension.json
|
|
51
77
|
```
|
|
52
78
|
|
|
79
|
+
Example manifest path on Linux:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
~/.config/google-chrome/NativeMessagingHosts/com.openai.codexextension.json
|
|
83
|
+
```
|
|
84
|
+
|
|
53
85
|
## MCP Client Config
|
|
54
86
|
|
|
55
87
|
Use stdio mode from other Agent tools:
|
|
@@ -112,8 +144,40 @@ This prints:
|
|
|
112
144
|
|
|
113
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.
|
|
114
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
|
+
|
|
115
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.
|
|
116
158
|
|
|
117
159
|
Stopping the MCP stdio process does not uninstall the native host. Uninstall explicitly to restore the original manifest.
|
|
118
160
|
|
|
119
161
|
The MCP stdio server uses the official `@modelcontextprotocol/sdk` package and must not write logs to stdout. Use stderr for diagnostics.
|
|
162
|
+
|
|
163
|
+
## Troubleshooting
|
|
164
|
+
|
|
165
|
+
If `status` shows no sockets:
|
|
166
|
+
|
|
167
|
+
1. Confirm Chrome is running with the profile where the extension is installed.
|
|
168
|
+
2. Reload or disable/enable the Codex Chrome Extension.
|
|
169
|
+
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.
|
|
172
|
+
|
|
173
|
+
If Codex App integration is affected, uninstall this project:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npx -y codex-control-chrome-mcp@latest uninstall-native-host
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
If the recorded backup is stale or manually changed, inspect the manifest and use `--force` only when you intentionally want to restore/remove the current project manifest.
|
|
180
|
+
|
|
181
|
+
## Publishing
|
|
182
|
+
|
|
183
|
+
This package is published from GitHub Actions through npm Trusted Publishing. See [Release Process](./release.md).
|
package/docs/release.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Release Process
|
|
2
|
+
|
|
3
|
+
Releases are published from GitHub Actions through npm Trusted Publishing.
|
|
4
|
+
|
|
5
|
+
## Trusted Publishing Setup
|
|
6
|
+
|
|
7
|
+
Configure the npm package settings:
|
|
8
|
+
|
|
9
|
+
- Package: `codex-control-chrome-mcp`
|
|
10
|
+
- Publisher: GitHub Actions
|
|
11
|
+
- Organization or user: `iola1999`
|
|
12
|
+
- Repository: `codex-control-chrome-mcp`
|
|
13
|
+
- Workflow filename: `publish.yml`
|
|
14
|
+
- Environment name: leave blank unless you add a GitHub deployment environment
|
|
15
|
+
- Allowed action: `npm publish`
|
|
16
|
+
|
|
17
|
+
Trusted Publishing requires a GitHub-hosted runner, OIDC permission, npm CLI 11.5.1 or newer, and Node.js 22.14.0 or newer. The publish workflow uses Node.js 24 and installs the latest npm before publishing.
|
|
18
|
+
|
|
19
|
+
## Publish A New Version
|
|
20
|
+
|
|
21
|
+
1. Update `package.json` version.
|
|
22
|
+
2. Update `CHANGELOG.md`.
|
|
23
|
+
3. Run checks locally:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run ci
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
4. Commit and push to `main`.
|
|
30
|
+
5. Create and push a matching tag:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git tag v1.0.1
|
|
34
|
+
git push origin v1.0.1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The tag must match the package version exactly. For example, `package.json` version `1.0.1` must use tag `v1.0.1`.
|
|
38
|
+
|
|
39
|
+
## What The Workflow Does
|
|
40
|
+
|
|
41
|
+
The `publish.yml` workflow:
|
|
42
|
+
|
|
43
|
+
1. Installs dependencies with `npm ci`.
|
|
44
|
+
2. Verifies the tag matches `package.json`.
|
|
45
|
+
3. Runs `npm run ci`.
|
|
46
|
+
4. Publishes with `npm publish`.
|
|
47
|
+
|
|
48
|
+
No long-lived `NPM_TOKEN` is required.
|
|
49
|
+
|
|
50
|
+
## GitHub Release
|
|
51
|
+
|
|
52
|
+
After the npm publish workflow succeeds, create a GitHub Release from the same tag and copy the relevant section from `CHANGELOG.md` into the release notes.
|
|
53
|
+
|
|
54
|
+
Do not create a new GitHub Release for a tag if the npm publish failed.
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-control-chrome-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server and native host bridge for controlling Chrome through the Codex Chrome extension.",
|
|
5
|
+
"license": "MIT",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
7
8
|
"codex-control-chrome-mcp": "bin/codex-control-chrome-mcp.js"
|
|
@@ -32,13 +33,18 @@
|
|
|
32
33
|
"src",
|
|
33
34
|
"docs",
|
|
34
35
|
"skills",
|
|
35
|
-
"
|
|
36
|
+
"CHANGELOG.md",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"README.md",
|
|
39
|
+
"SECURITY.md"
|
|
36
40
|
],
|
|
37
41
|
"scripts": {
|
|
38
42
|
"start": "node ./bin/codex-control-chrome-mcp.js --stdio",
|
|
39
43
|
"native-host": "node ./bin/codex-control-chrome-mcp.js --native-host",
|
|
40
44
|
"status": "node ./bin/codex-control-chrome-mcp.js status",
|
|
41
|
-
"check": "node --check ./bin/codex-control-chrome-mcp.js && node --check ./src/*.js"
|
|
45
|
+
"check": "node --check ./bin/codex-control-chrome-mcp.js && node --check ./src/*.js && node --check ./test/*.test.js",
|
|
46
|
+
"test": "node --test",
|
|
47
|
+
"ci": "npm run check && npm test && npm pack --dry-run"
|
|
42
48
|
},
|
|
43
49
|
"engines": {
|
|
44
50
|
"node": ">=20"
|
|
@@ -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,19 +8,26 @@ import {
|
|
|
8
8
|
INSTALL_STATE_PATH,
|
|
9
9
|
nativeHostManifestPath,
|
|
10
10
|
} from "./constants.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
import {
|
|
12
|
+
MANIFEST_DESCRIPTION,
|
|
13
|
+
classifyHost,
|
|
14
|
+
inspectHostBinary,
|
|
15
|
+
isOfficialCodexManifest,
|
|
16
|
+
isOurLauncherPath,
|
|
17
|
+
isProjectManifest,
|
|
18
|
+
} from "./host-identity.js";
|
|
19
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
20
|
+
|
|
21
|
+
export async function installNativeHost({ binPath, proxy = true, paths = defaultInstallPaths() } = {}) {
|
|
22
|
+
const { manifestPath, appStateDir, configPath, installStatePath } = paths;
|
|
18
23
|
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
19
|
-
await fs.mkdir(
|
|
24
|
+
await fs.mkdir(appStateDir, { recursive: true });
|
|
20
25
|
|
|
21
|
-
const hostPath = binPath
|
|
26
|
+
const hostPath = binPath
|
|
27
|
+
? await createLocalLauncher(path.resolve(binPath), appStateDir)
|
|
28
|
+
: await createNpxLauncher(appStateDir);
|
|
22
29
|
const existing = await readJsonIfExists(manifestPath);
|
|
23
|
-
const previousState = await readJsonIfExists(
|
|
30
|
+
const previousState = await readJsonIfExists(installStatePath);
|
|
24
31
|
const previousBackup = previousState?.backupPath
|
|
25
32
|
? await readJsonIfExists(previousState.backupPath)
|
|
26
33
|
: null;
|
|
@@ -33,23 +40,9 @@ export async function installNativeHost({ binPath, proxy = true } = {}) {
|
|
|
33
40
|
backupPath = previousState.backupPath;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} else if (
|
|
40
|
-
proxy &&
|
|
41
|
-
typeof previousState?.officialHostPath === "string" &&
|
|
42
|
-
path.resolve(previousState.officialHostPath) !== path.resolve(hostPath)
|
|
43
|
-
) {
|
|
44
|
-
officialHostPath = previousState.officialHostPath;
|
|
45
|
-
} else if (
|
|
46
|
-
proxy &&
|
|
47
|
-
existing?.path &&
|
|
48
|
-
!existingIsOurs &&
|
|
49
|
-
path.resolve(existing.path) !== path.resolve(hostPath)
|
|
50
|
-
) {
|
|
51
|
-
officialHostPath = existing.path;
|
|
52
|
-
}
|
|
43
|
+
const officialHostPath = proxy
|
|
44
|
+
? await resolveOfficialHostPath({ existing, existingIsOurs, previousState, previousBackup, hostPath })
|
|
45
|
+
: null;
|
|
53
46
|
|
|
54
47
|
const manifest = {
|
|
55
48
|
allowed_origins: [`chrome-extension://${CODEX_EXTENSION_ID}/`],
|
|
@@ -64,19 +57,96 @@ export async function installNativeHost({ binPath, proxy = true } = {}) {
|
|
|
64
57
|
installedAt: new Date().toISOString(),
|
|
65
58
|
manifestPath,
|
|
66
59
|
hostPath,
|
|
60
|
+
binPath: binPath ? path.resolve(binPath) : null,
|
|
67
61
|
launchMode: binPath ? "direct-bin" : "npx-launcher",
|
|
68
62
|
backupPath,
|
|
69
63
|
officialHostPath,
|
|
70
64
|
proxy,
|
|
71
65
|
};
|
|
72
|
-
await fs.writeFile(
|
|
73
|
-
await fs.writeFile(
|
|
66
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
67
|
+
await fs.writeFile(installStatePath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
74
68
|
return config;
|
|
75
69
|
}
|
|
76
70
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
|
|
147
|
+
export async function uninstallNativeHost({ force = false, paths = defaultInstallPaths() } = {}) {
|
|
148
|
+
const { manifestPath, installStatePath } = paths;
|
|
149
|
+
const state = await readJsonIfExists(installStatePath);
|
|
80
150
|
const manifest = await readJsonIfExists(manifestPath);
|
|
81
151
|
|
|
82
152
|
if (!manifest && !state?.backupPath) {
|
|
@@ -99,10 +169,25 @@ export async function uninstallNativeHost({ force = false } = {}) {
|
|
|
99
169
|
}
|
|
100
170
|
|
|
101
171
|
export async function nativeHostInstallStatus() {
|
|
102
|
-
const manifestPath =
|
|
172
|
+
const { manifestPath, installStatePath } = defaultInstallPaths();
|
|
103
173
|
const manifest = await readJsonIfExists(manifestPath);
|
|
104
|
-
const state = await readJsonIfExists(
|
|
105
|
-
|
|
174
|
+
const state = await readJsonIfExists(installStatePath);
|
|
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 };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function defaultInstallPaths() {
|
|
185
|
+
return {
|
|
186
|
+
manifestPath: nativeHostManifestPath(),
|
|
187
|
+
appStateDir: APP_STATE_DIR,
|
|
188
|
+
configPath: APP_CONFIG_PATH,
|
|
189
|
+
installStatePath: INSTALL_STATE_PATH,
|
|
190
|
+
};
|
|
106
191
|
}
|
|
107
192
|
|
|
108
193
|
async function backupManifest(manifestPath) {
|
|
@@ -115,11 +200,11 @@ async function backupManifest(manifestPath) {
|
|
|
115
200
|
return backupPath;
|
|
116
201
|
}
|
|
117
202
|
|
|
118
|
-
async function createNpxLauncher() {
|
|
203
|
+
async function createNpxLauncher(appStateDir) {
|
|
119
204
|
if (process.platform === "win32") {
|
|
120
205
|
throw new Error("NPX native host launcher installation is not implemented for Windows yet.");
|
|
121
206
|
}
|
|
122
|
-
const launcherPath = path.join(
|
|
207
|
+
const launcherPath = path.join(appStateDir, "native-host-launcher");
|
|
123
208
|
const script = `#!/usr/bin/env sh
|
|
124
209
|
exec npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} --native-host "$@"
|
|
125
210
|
`;
|
|
@@ -128,18 +213,14 @@ exec npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} --native-host "$@"
|
|
|
128
213
|
return launcherPath;
|
|
129
214
|
}
|
|
130
215
|
|
|
131
|
-
function
|
|
132
|
-
return manifest?.description === MANIFEST_DESCRIPTION;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function createLocalLauncher(binPath) {
|
|
216
|
+
async function createLocalLauncher(binPath, appStateDir) {
|
|
136
217
|
if (process.platform === "win32") {
|
|
137
218
|
return binPath;
|
|
138
219
|
}
|
|
139
220
|
if (!binPath.endsWith(".js")) {
|
|
140
221
|
return binPath;
|
|
141
222
|
}
|
|
142
|
-
const launcherPath = path.join(
|
|
223
|
+
const launcherPath = path.join(appStateDir, "native-host-local-launcher");
|
|
143
224
|
const script = `#!/usr/bin/env sh
|
|
144
225
|
exec ${shellQuote(process.execPath)} ${shellQuote(binPath)} "$@"
|
|
145
226
|
`;
|
package/src/mcp-server.js
CHANGED
|
@@ -4,12 +4,15 @@ 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";
|
|
8
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
7
9
|
|
|
8
10
|
export async function runMcpServer() {
|
|
11
|
+
await autoRegisterNativeHost();
|
|
9
12
|
const runtime = new ChromeUseRuntime();
|
|
10
13
|
const server = new McpServer({
|
|
11
|
-
name:
|
|
12
|
-
version:
|
|
14
|
+
name: PACKAGE_NAME,
|
|
15
|
+
version: PACKAGE_VERSION,
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
registerTools(server, runtime);
|
|
@@ -18,6 +21,32 @@ export async function runMcpServer() {
|
|
|
18
21
|
await server.connect(transport);
|
|
19
22
|
}
|
|
20
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
|
+
|
|
21
50
|
class ChromeUseRuntime {
|
|
22
51
|
constructor() {
|
|
23
52
|
this.sessionId = `codex-control-chrome-mcp-${crypto.randomUUID()}`;
|