Add setup-codex-chrome-native curated skill

This commit is contained in:
easong-openai
2026-02-09 18:18:05 -08:00
parent 4ab6e0fd99
commit 122ef30edf
6 changed files with 1164 additions and 0 deletions
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf of
any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don\'t include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,63 @@
---
name: setup-codex-chrome-native
description: Set up and repair Codex CLI connectivity to the codex-chrome extension by installing native messaging manifests and configuring the codex-extension MCP server entry in standalone mode only (retail Codex + installed extension). Use when users ask to connect Codex CLI to the extension, reinstall native host scripts, switch browser target for Chrome or Chromium, or troubleshoot disconnected native host or MCP wiring.
---
# Setup Codex Chrome Native
## Overview
Install, reconnect, verify, and uninstall the codex-chrome native host integration used by `codex-cli`.
The bundled script is standalone-only: no source repository checkout is required.
## Workflow
1. Collect required inputs:
- Browser target (`chrome`, `chromium`, or `all`)
- Optional extension id override (`--extension-id`)
- Optional explicit schemas path (`--tool-schemas`) when auto-detection fails
2. Run `scripts/manage_codex_chrome_native.sh` with one of:
- `setup` to install native host + configure MCP
- `status` to check manifests, extension/schema detection, and MCP visibility
- `uninstall` to remove manifests (and optionally MCP entry)
3. For `setup`, instruct the user to confirm Chrome extension loading:
- Browser target (`chrome`, `chromium`, or `all`)
- Open `chrome://extensions`
- Ensure `codex-extension` is installed/enabled
- Check extension Options and confirm native host status is connected
4. If setup fails, read `references/native-setup.md` and follow the matching troubleshooting path.
5. If Codex reports MCP startup/initialize handshake failures, run:
- `./scripts/manage_codex_chrome_native.sh setup --skip-install`
- then reload/reconnect the extension in `chrome://extensions`.
## Commands
Run from this skill directory or call with an absolute path:
```bash
# Setup using defaults
./scripts/manage_codex_chrome_native.sh setup
# Setup against Chromium and explicit extension id
./scripts/manage_codex_chrome_native.sh setup --browser chromium --extension-id <extension-id>
# Standalone setup with explicit schemas path
./scripts/manage_codex_chrome_native.sh setup --extension-id <extension-id> --tool-schemas <path/to/tool-schemas.json>
# Check setup state
./scripts/manage_codex_chrome_native.sh status
# Uninstall and remove codex-extension MCP entry
./scripts/manage_codex_chrome_native.sh uninstall --remove-mcp
```
## Resources
- `scripts/manage_codex_chrome_native.sh`: deterministic wrapper around extension setup/uninstall/status.
- `references/native-setup.md`: manual commands, manifest locations, and troubleshooting.
## Notes
- Prefer `setup` then `status` before deeper debugging.
- Keep changes scoped to native host setup and MCP registration only.
- Report exact failing command and stderr when escalation is needed.
@@ -0,0 +1,4 @@
interface:
display_name: "Codex Chrome Native Setup"
short_description: "Set up Codex Chrome native host + MCP"
default_prompt: "Use $setup-codex-chrome-native to install and verify the Codex Chrome native host and Codex MCP connection."
@@ -0,0 +1,91 @@
# Native Setup Reference
## Inputs
- `browser`: `chrome`, `chromium`, or `all`
- `extension_id`: optional override when extension auto-detection fails
- `tool_schemas`: optional explicit `tool-schemas.json` path
## Preferred command paths
Use the bundled script first:
```bash
./scripts/manage_codex_chrome_native.sh setup
./scripts/manage_codex_chrome_native.sh status
./scripts/manage_codex_chrome_native.sh uninstall --remove-mcp
```
## Verification checklist
1. Native host manifest exists:
- `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.openai.codex_experimental_extension.json`
- `~/Library/Application Support/Chromium/NativeMessagingHosts/com.openai.codex_experimental_extension.json` (if using Chromium)
2. `codex mcp list` includes entry `codex-extension` with URL `http://127.0.0.1:8787/mcp`.
3. Extension is installed in Chrome and options show native status connected.
4. `status` output reports standalone wrapper at `~/Library/Application Support/CodexExtension/codex-native-host-shim-standalone.sh`.
## Troubleshooting
### Missing extension id
Symptoms:
- setup script fails with message about missing extension id.
Fix:
- pass `--extension-id <id>` to the bundled script.
### Node or npm not available
Symptoms:
- `Missing required command: node` or `npm`.
Fix:
- install Node.js and rerun setup.
### MCP entry missing
Symptoms:
- native host manifest exists but `codex mcp list` does not show `codex-extension`.
Fix:
- rerun setup.
- if still missing, run:
- `codex mcp add codex-extension --url http://127.0.0.1:8787/mcp`
- `codex mcp list`
### Extension still disconnected
Symptoms:
- extension options show disconnected after setup.
Fix:
1. Open `chrome://extensions`, enable Developer Mode, reload extension.
2. Ensure `codex-extension` is installed/enabled.
3. Run `./scripts/manage_codex_chrome_native.sh status` and verify manifest presence.
### Tool schemas path not found (standalone mode)
Symptoms:
- setup fails with `Could not locate tool-schemas.json`.
Fix:
1. Confirm extension is installed in the selected browser profile.
2. Provide explicit schemas path:
- `./scripts/manage_codex_chrome_native.sh setup --extension-id <id> --tool-schemas <path/to/tool-schemas.json>`
### MCP startup handshake fails after idle
Symptoms:
- Codex shows errors similar to:
- `MCP startup failed: handshaking with MCP server failed`
- `error decoding response body`
Cause:
- A long-lived `mcp-shim` process can keep stale session state and reject a fresh initialize request.
Fix:
1. Run:
- `./scripts/manage_codex_chrome_native.sh setup --skip-install`
2. Reload the extension from `chrome://extensions` (or click reconnect in extension options).
3. Re-run the command in Codex.
@@ -0,0 +1,431 @@
#!/usr/bin/env bash
set -euo pipefail
HOST_NAME="com.openai.codex_experimental_extension"
WRAPPER_DIR="$HOME/Library/Application Support/CodexExtension"
STANDALONE_DIR="$WRAPPER_DIR/standalone-mcp-shim"
STANDALONE_WRAPPER="$WRAPPER_DIR/codex-native-host-shim-standalone.sh"
usage() {
cat <<'USAGE'
Usage:
manage_codex_chrome_native.sh <setup|status|uninstall> [options]
Options:
--browser <target> chrome | chromium | all (default: chrome)
--extension-id <id> Extension id override
--tool-schemas <path> Explicit tool-schemas.json path
--skip-install Skip npm install during setup
--remove-mcp Remove codex-extension MCP entry on uninstall
-h, --help Show this message
USAGE
}
fail() {
echo "[setup-codex-chrome-native] $*" >&2
exit 1
}
info() {
echo "[setup-codex-chrome-native] $*"
}
ensure_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || fail "Missing required command: $cmd"
}
manifest_paths() {
local browser="$1"
if [[ "$browser" == "chrome" || "$browser" == "all" ]]; then
printf '%s\n' "$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/$HOST_NAME.json"
fi
if [[ "$browser" == "chromium" || "$browser" == "all" ]]; then
printf '%s\n' "$HOME/Library/Application Support/Chromium/NativeMessagingHosts/$HOST_NAME.json"
fi
}
extension_roots() {
local browser="$1"
if [[ "$browser" == "chrome" || "$browser" == "all" ]]; then
printf '%s\n' "$HOME/Library/Application Support/Google/Chrome/Default/Extensions"
fi
if [[ "$browser" == "chromium" || "$browser" == "all" ]]; then
printf '%s\n' "$HOME/Library/Application Support/Chromium/Default/Extensions"
fi
}
show_port_status() {
if ! command -v lsof >/dev/null 2>&1; then
return 0
fi
local listeners
listeners="$(lsof -nP -iTCP:8787 -sTCP:LISTEN 2>/dev/null || true)"
if [[ -n "$listeners" ]]; then
info "Port 8787 listener(s):"
echo "$listeners"
else
info "No listener currently bound to TCP 8787."
fi
}
cleanup_stale_shim() {
local patterns
patterns=(
'mcp-shim/dist/index.js'
'standalone-mcp-shim/index.mjs'
'codex-native-host-shim-standalone.sh'
)
local pattern
for pattern in "${patterns[@]}"; do
if pgrep -f "$pattern" >/dev/null 2>&1; then
info "Stopping existing process(es) matching: $pattern"
pkill -f "$pattern" 2>/dev/null || true
fi
done
sleep 1
}
latest_version_dir() {
local base_dir="$1"
[[ -d "$base_dir" ]] || return 1
local latest=""
latest="$(find "$base_dir" -mindepth 1 -maxdepth 1 -type d -print 2>/dev/null | sort -V | tail -n 1 || true)"
[[ -n "$latest" ]] || return 1
printf '%s\n' "$latest"
}
detect_extension_id() {
local browser="$1"
local override_id="$2"
if [[ -n "$override_id" ]]; then
printf '%s\n' "$override_id"
return 0
fi
local root
while IFS= read -r root; do
[[ -d "$root" ]] || continue
local id_dir
while IFS= read -r id_dir; do
[[ -d "$id_dir" ]] || continue
local manifest_dir
manifest_dir="$(latest_version_dir "$id_dir" || true)"
[[ -n "$manifest_dir" ]] || continue
local manifest_path="$manifest_dir/manifest.json"
[[ -f "$manifest_path" ]] || continue
if grep -q '"name"[[:space:]]*:[[:space:]]*"codex-extension"' "$manifest_path"; then
basename "$id_dir"
return 0
fi
done < <(find "$root" -mindepth 1 -maxdepth 1 -type d -print 2>/dev/null | sort)
done < <(extension_roots "$browser")
return 1
}
resolve_tool_schemas_path() {
local browser="$1"
local extension_id="$2"
local override_path="$3"
if [[ -n "$override_path" ]]; then
[[ -f "$override_path" ]] || fail "--tool-schemas path does not exist: $override_path"
printf '%s\n' "$override_path"
return 0
fi
local root
while IFS= read -r root; do
local id_root="$root/$extension_id"
[[ -d "$id_root" ]] || continue
local ver_dir
ver_dir="$(latest_version_dir "$id_root" || true)"
[[ -n "$ver_dir" ]] || continue
local schemas="$ver_dir/tool-schemas.json"
if [[ -f "$schemas" ]]; then
printf '%s\n' "$schemas"
return 0
fi
done < <(extension_roots "$browser")
return 1
}
configure_codex_mcp() {
if ! command -v codex >/dev/null 2>&1; then
info "codex CLI not found on PATH; skipping MCP entry configuration."
return 0
fi
codex mcp remove codex-extension >/dev/null 2>&1 || true
codex mcp add codex-extension --url http://127.0.0.1:8787/mcp
}
install_native_manifest() {
local browser="$1"
local extension_id="$2"
local host_path="$3"
while IFS= read -r manifest; do
mkdir -p "$(dirname "$manifest")"
cat > "$manifest" <<JSON
{
"name": "$HOST_NAME",
"description": "Codex Experimental Extension Native Host",
"path": "$host_path",
"type": "stdio",
"allowed_origins": [
"chrome-extension://$extension_id/"
]
}
JSON
info "Installed native host manifest: $manifest"
done < <(manifest_paths "$browser")
}
remove_native_manifests() {
local browser="$1"
while IFS= read -r manifest; do
if [[ -f "$manifest" ]]; then
rm -f "$manifest"
info "Removed manifest: $manifest"
else
info "Manifest not found (skipped): $manifest"
fi
done < <(manifest_paths "$browser")
}
setup_standalone_mode() {
local browser="$1"
local extension_id="$2"
local tool_schemas="$3"
local skip_install="$4"
local script_dir="$5"
ensure_cmd node
ensure_cmd npm
cleanup_stale_shim
mkdir -p "$STANDALONE_DIR"
cp "$script_dir/standalone-mcp-shim.mjs" "$STANDALONE_DIR/index.mjs"
cat > "$STANDALONE_DIR/package.json" <<'JSON'
{
"name": "codex-extension-standalone-shim",
"private": true,
"type": "module",
"version": "0.1.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
}
}
JSON
if [[ "$skip_install" -eq 0 ]]; then
info "Installing standalone shim dependencies..."
npm --prefix "$STANDALONE_DIR" install
else
info "Skipping npm install (--skip-install)."
fi
[[ -d "$STANDALONE_DIR/node_modules/@modelcontextprotocol/sdk" ]] || \
fail "Standalone dependencies missing. Re-run setup without --skip-install."
local node_bin
node_bin="$(command -v node || true)"
[[ -n "$node_bin" ]] || fail "Unable to locate node on PATH"
mkdir -p "$WRAPPER_DIR"
cat > "$STANDALONE_WRAPPER" <<WRAP
#!/bin/bash
export CODEX_TOOL_SCHEMAS_PATH="$tool_schemas"
exec "$node_bin" "$STANDALONE_DIR/index.mjs" "\$@"
WRAP
chmod +x "$STANDALONE_WRAPPER"
info "Installed standalone host wrapper: $STANDALONE_WRAPPER"
install_native_manifest "$browser" "$extension_id" "$STANDALONE_WRAPPER"
configure_codex_mcp
}
status_report() {
local browser="$1"
local extension_id="$2"
local tool_schemas="$3"
local found=0
while IFS= read -r manifest; do
if [[ -f "$manifest" ]]; then
info "Manifest found: $manifest"
found=1
else
info "Manifest not found: $manifest"
fi
done < <(manifest_paths "$browser")
if [[ "$found" -eq 0 ]]; then
info "No manifests found for target=$browser."
fi
if [[ -n "$extension_id" ]]; then
info "Detected extension id: $extension_id"
else
info "Could not auto-detect extension id from installed browser extensions."
fi
if [[ -n "$tool_schemas" ]]; then
info "Tool schemas path: $tool_schemas"
else
info "Tool schemas file not detected."
fi
if [[ -f "$STANDALONE_WRAPPER" ]]; then
info "Standalone wrapper present: $STANDALONE_WRAPPER"
fi
if command -v codex >/dev/null 2>&1; then
info "Codex MCP entries:"
codex mcp list || true
else
info "codex CLI not found on PATH; skipping MCP listing."
fi
show_port_status
}
uninstall_all() {
local browser="$1"
local remove_mcp="$2"
remove_native_manifests "$browser"
local wrappers=(
"$WRAPPER_DIR/codex-native-host.sh"
"$WRAPPER_DIR/codex-native-host-shim.sh"
"$WRAPPER_DIR/codex-native-host-fake.sh"
"$STANDALONE_WRAPPER"
)
local wrapper
for wrapper in "${wrappers[@]}"; do
if [[ -f "$wrapper" ]]; then
rm -f "$wrapper"
info "Removed wrapper: $wrapper"
fi
done
if [[ -d "$STANDALONE_DIR" ]]; then
rm -rf "$STANDALONE_DIR"
info "Removed standalone shim directory: $STANDALONE_DIR"
fi
if [[ "$remove_mcp" -eq 1 ]]; then
if command -v codex >/dev/null 2>&1; then
if codex mcp remove codex-extension >/dev/null 2>&1; then
info "Removed Codex MCP entry: codex-extension"
else
info "Codex MCP entry not removed (may not exist): codex-extension"
fi
else
info "codex CLI not found; skipping MCP removal"
fi
fi
}
MODE="${1:-}"
if [[ -z "$MODE" || "$MODE" == "--help" || "$MODE" == "-h" ]]; then
usage
exit 0
fi
shift || true
BROWSER="chrome"
EXTENSION_ID=""
TOOL_SCHEMAS=""
SKIP_INSTALL=0
REMOVE_MCP=0
while [[ $# -gt 0 ]]; do
case "$1" in
--browser)
[[ $# -ge 2 ]] || fail "--browser requires a value"
BROWSER="$2"
shift 2
;;
--extension-id)
[[ $# -ge 2 ]] || fail "--extension-id requires a value"
EXTENSION_ID="$2"
shift 2
;;
--tool-schemas)
[[ $# -ge 2 ]] || fail "--tool-schemas requires a value"
TOOL_SCHEMAS="$2"
shift 2
;;
--skip-install)
SKIP_INSTALL=1
shift
;;
--remove-mcp)
REMOVE_MCP=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
fail "Unknown argument: $1"
;;
esac
done
case "$MODE" in
setup|status|uninstall) ;;
*) fail "Unknown mode: $MODE" ;;
esac
case "$BROWSER" in
chrome|chromium|all) ;;
*) fail "Invalid browser target: $BROWSER (expected chrome|chromium|all)" ;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
info "Mode: $MODE"
info "Implementation: standalone"
info "Browser target: $BROWSER"
if [[ "$MODE" == "setup" ]]; then
resolved_extension_id="$(detect_extension_id "$BROWSER" "$EXTENSION_ID" || true)"
[[ -n "$resolved_extension_id" ]] || fail "Could not detect Codex extension id. Pass --extension-id <id>."
resolved_tool_schemas="$(resolve_tool_schemas_path "$BROWSER" "$resolved_extension_id" "$TOOL_SCHEMAS" || true)"
[[ -n "$resolved_tool_schemas" ]] || fail "Could not locate tool-schemas.json. Pass --tool-schemas <path>."
setup_standalone_mode "$BROWSER" "$resolved_extension_id" "$resolved_tool_schemas" "$SKIP_INSTALL" "$SCRIPT_DIR"
info "Setup complete."
info "Next: open chrome://extensions, reload the codex-extension, then verify in extension options."
status_report "$BROWSER" "${resolved_extension_id:-$EXTENSION_ID}" "${resolved_tool_schemas:-$TOOL_SCHEMAS}"
exit 0
fi
if [[ "$MODE" == "status" ]]; then
resolved_extension_id="$(detect_extension_id "$BROWSER" "$EXTENSION_ID" || true)"
resolved_tool_schemas=""
if [[ -n "$resolved_extension_id" ]]; then
resolved_tool_schemas="$(resolve_tool_schemas_path "$BROWSER" "$resolved_extension_id" "$TOOL_SCHEMAS" || true)"
fi
status_report "$BROWSER" "$resolved_extension_id" "$resolved_tool_schemas"
exit 0
fi
if [[ "$MODE" == "uninstall" ]]; then
uninstall_all "$BROWSER" "$REMOVE_MCP"
info "Uninstall complete."
fi
@@ -0,0 +1,374 @@
#!/usr/bin/env node
import fs from 'node:fs'
import { randomUUID } from 'node:crypto'
import { createServer } from 'node:http'
import { stdin, stdout, stderr, env } from 'node:process'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
const MAX_MESSAGE_BYTES = Number(env.CODEX_SHIM_MAX_MESSAGE_BYTES ?? 8 * 1024 * 1024)
const TOOL_TIMEOUT_MS = Number(env.CODEX_SHIM_TOOL_TIMEOUT_MS ?? 30_000)
const MCP_PORT = Number(env.CODEX_MCP_PORT ?? 8787)
const MCP_HOST = env.CODEX_MCP_HOST ?? '127.0.0.1'
const MCP_AUTH_TOKEN = env.CODEX_MCP_AUTH_TOKEN
const CODEX_CLIENT_ID = env.CODEX_CLIENT_ID
const TOOL_SCHEMAS_PATH = env.CODEX_TOOL_SCHEMAS_PATH
const MCP_ALLOWED_ORIGINS = env.CODEX_MCP_ALLOWED_ORIGINS
? env.CODEX_MCP_ALLOWED_ORIGINS.split(',').map((value) => value.trim()).filter(Boolean)
: []
/** @typedef {null | boolean | number | string | Json[] | { [k: string]: Json }} Json */
/**
* @typedef {{
* request: { tool: string, args: Json, clientId: string, timeoutMs: number },
* resolve: (value: { content: Json, isError: boolean }) => void,
* reject: (reason: Error) => void,
* timeoutId: NodeJS.Timeout
* }} PendingToolRequest
*/
/** @type {Buffer} */
let nativeBuffer = Buffer.alloc(0)
/** @type {PendingToolRequest | null} */
let inFlight = null
/** @type {PendingToolRequest[]} */
const queue = []
function log(message, extra) {
const payload = extra ? `${message} ${JSON.stringify(extra)}` : message
stderr.write(`[standalone-mcp-shim] ${payload}\n`)
}
function writeNativeMessage(msg) {
const json = Buffer.from(JSON.stringify(msg), 'utf8')
const header = Buffer.alloc(4)
header.writeUInt32LE(json.length, 0)
stdout.write(Buffer.concat([header, json]))
}
function dispatch(entry) {
writeNativeMessage({
type: 'tool_request',
method: 'execute_tool',
params: {
tool: entry.request.tool,
args: entry.request.args,
client_id: entry.request.clientId,
},
})
}
function dispatchNext() {
const next = queue.shift() ?? null
if (!next) return
inFlight = next
dispatch(next)
}
function failInFlight(error) {
if (!inFlight) return
const entry = inFlight
clearTimeout(entry.timeoutId)
inFlight = null
entry.reject(error)
dispatchNext()
}
/**
* @param {string} tool
* @param {Json} args
* @param {string} clientId
* @param {number=} timeoutMs
* @returns {Promise<{content: Json, isError: boolean}>}
*/
function sendToolRequest(tool, args, clientId, timeoutMs = TOOL_TIMEOUT_MS) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
failInFlight(new Error(`Tool request timed out after ${timeoutMs}ms`))
}, timeoutMs)
const entry = {
request: { tool, args, clientId, timeoutMs },
resolve,
reject,
timeoutId,
}
if (inFlight) {
queue.push(entry)
return
}
inFlight = entry
dispatch(entry)
})
}
function handleToolResponse(msg) {
if (!inFlight) {
log('tool_response without pending request', msg)
return
}
const entry = inFlight
clearTimeout(entry.timeoutId)
inFlight = null
const isError = Boolean(msg.error)
const content = msg.error?.content ?? msg.result?.content ?? null
entry.resolve({ content, isError })
dispatchNext()
}
function handleNativeMessage(msg) {
switch (msg?.type) {
case 'ping':
writeNativeMessage({ type: 'pong' })
return
case 'get_status':
writeNativeMessage({ type: 'status_response', status: { ok: true, version: 'standalone' } })
return
case 'tool_response':
handleToolResponse(msg)
return
case 'notification':
return
default:
log('unexpected native message', msg)
return
}
}
/**
* @param {Buffer} chunk
*/
function onNativeData(chunk) {
nativeBuffer = Buffer.concat([nativeBuffer, chunk])
while (nativeBuffer.length >= 4) {
const length = nativeBuffer.readUInt32LE(0)
if (length > MAX_MESSAGE_BYTES) {
log('native message length exceeds max', { length, max: MAX_MESSAGE_BYTES })
nativeBuffer = Buffer.alloc(0)
return
}
if (nativeBuffer.length < 4 + length) return
const payload = nativeBuffer.subarray(4, 4 + length)
nativeBuffer = nativeBuffer.subarray(4 + length)
try {
const msg = JSON.parse(payload.toString('utf8'))
handleNativeMessage(msg)
} catch (error) {
log('invalid native JSON payload', { error: String(error) })
}
}
}
/**
* @returns {Array<{name: string, description: string, inputSchema: Record<string, unknown>}>}
*/
function loadToolSchemas() {
if (!TOOL_SCHEMAS_PATH) {
throw new Error('Missing CODEX_TOOL_SCHEMAS_PATH env var')
}
const raw = fs.readFileSync(TOOL_SCHEMAS_PATH, 'utf8')
const parsed = JSON.parse(raw)
if (!Array.isArray(parsed)) {
throw new Error(`Invalid tool schema file at ${TOOL_SCHEMAS_PATH}: expected array`)
}
return parsed
}
function extractImageContent(payload) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null
const base64Image = typeof payload.base64Image === 'string' ? payload.base64Image : undefined
if (!base64Image) return null
if (base64Image.startsWith('data:')) {
const match = base64Image.match(/^data:(.*?);base64,(.*)$/)
if (match) {
return { mimeType: match[1] || 'image/png', data: match[2] || '' }
}
}
return { mimeType: 'image/png', data: base64Image }
}
function formatToolResult(response) {
const text = typeof response.content === 'string' ? response.content : JSON.stringify(response.content ?? null)
const structuredContent =
typeof response.content === 'object' && response.content !== null && !Array.isArray(response.content)
? response.content
: undefined
const content = [{ type: 'text', text }]
const image = extractImageContent(structuredContent)
if (image) {
content.push({ type: 'image', data: image.data, mimeType: image.mimeType })
}
return { content, structuredContent, isError: response.isError }
}
function isLoopback(address) {
if (!address) return false
return address === '::1' || address === '127.0.0.1' || address.startsWith('::ffff:127.')
}
function isAllowedOrigin(origin, allowlist) {
try {
const url = new URL(origin)
if (allowlist.length === 0) return isLoopback(url.hostname)
return allowlist.includes(origin)
} catch {
return false
}
}
function matchesBearerToken(value, expected) {
const [scheme, token] = value.split(' ')
if (!scheme || !token) return false
if (scheme.toLowerCase() !== 'bearer') return false
return token === expected
}
function getSessionIdHeader(value) {
if (typeof value === 'string' && value.trim()) return value.trim()
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item === 'string' && item.trim()) return item.trim()
}
}
return undefined
}
async function main() {
stdin.on('data', onNativeData)
stdin.on('error', (error) => log('stdin error', { error: String(error) }))
stdout.on('error', (error) => log('stdout error', { error: String(error) }))
const transports = new Map()
function createMcpServer() {
const server = new Server(
{ name: 'codex-extension-standalone-shim', version: '0.1.0' },
{ capabilities: { tools: { listChanged: true } } },
)
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = loadToolSchemas().map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
}))
return { tools }
})
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const toolName = request.params.name
const args = request.params.arguments ?? {}
const clientId = extra.sessionId ?? CODEX_CLIENT_ID ?? 'codex-mcp'
try {
const response = await sendToolRequest(toolName, /** @type {Json} */ (args), clientId)
return formatToolResult(response)
} catch (error) {
return formatToolResult({ content: { error: String(error) }, isError: true })
}
})
return server
}
/** @returns {Promise<StreamableHTTPServerTransport>} */
async function createSessionTransport() {
const mcpServer = createMcpServer()
let transport
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
transports.set(sessionId, transport)
},
})
transport.onclose = () => {
const sid = transport.sessionId
if (sid) transports.delete(sid)
}
await mcpServer.connect(transport)
return transport
}
const httpServer = createServer(async (req, res) => {
const requestUrl = new URL(req.url ?? '/', `http://${MCP_HOST}`)
if (requestUrl.pathname !== '/mcp') {
res.statusCode = 404
res.end('Not Found')
return
}
if (!isLoopback(req.socket.remoteAddress)) {
res.statusCode = 403
res.end('Forbidden')
return
}
const origin = req.headers.origin
if (origin && !isAllowedOrigin(origin, MCP_ALLOWED_ORIGINS)) {
res.statusCode = 403
res.end('Forbidden')
return
}
if (MCP_AUTH_TOKEN) {
const auth = req.headers.authorization ?? ''
if (!matchesBearerToken(auth, MCP_AUTH_TOKEN)) {
res.statusCode = 401
res.end('Unauthorized')
return
}
}
const method = req.method ?? 'GET'
const sessionId = getSessionIdHeader(req.headers['mcp-session-id'])
if (method === 'POST') {
if (sessionId) {
const existing = transports.get(sessionId)
if (!existing) {
res.statusCode = 404
res.end('Not Found')
return
}
await existing.handleRequest(req, res)
return
}
const transport = await createSessionTransport()
await transport.handleRequest(req, res)
if (!transport.sessionId) {
await transport.close().catch(() => {})
}
return
}
if (method === 'GET' || method === 'DELETE') {
if (!sessionId) {
res.statusCode = 400
res.end('Bad Request')
return
}
const existing = transports.get(sessionId)
if (!existing) {
res.statusCode = 404
res.end('Not Found')
return
}
await existing.handleRequest(req, res)
return
}
res.statusCode = 405
res.end('Method Not Allowed')
})
await new Promise((resolve) => {
httpServer.listen(MCP_PORT, MCP_HOST, resolve)
})
log('shim started', { host: MCP_HOST, port: MCP_PORT, schemas: TOOL_SCHEMAS_PATH })
}
main().catch((error) => {
log('fatal error', { error: String(error) })
process.exit(1)
})