@gemus/mcp-proxy
v0.1.18
Published
Local stdio<->HTTP MCP proxy for Codex desktop (transparent passthrough + execute tap + imagegen backfill, Issue #1751) plus the one-command Gemus installers for Codex and DeepSeek Harness (Issue #2924).
Maintainers
Readme
@gemus/mcp-proxy (Issues #1751, #1756, #1917, #2356)
Local stdio↔HTTP MCP proxy that gives Codex desktop (direct-connect / Mode B) users canvas
image write-back. The Codex plugin declares it as a default-disabled, command-based MCP server
so Codex can spawn it locally after explicit enablement; the proxy owns upstream auth to the remote
Gemus /api/mcp (Codex does no OAuth for stdio servers). Full design:
docs/plans/1751-codex-desktop-companion.md
and docs/architecture/agent architecture/codex-desktop-companion.md.
Status: implemented
src/proxy.mjs is the executable router: setup loads the isolated full installer, setup-env
loads only secure credential acquisition plus the existing platform environment adapter, and no
arguments load src/relay.mjs. The relay is the production passthrough, execute-tap, and deterministic
image-backfill companion. A trusted Stop hook triggers generated_images/rollout discovery;
ExecuteClaimTracker then delivers an unambiguous image to its planned node or conservatively
creates an orphan image-upload. MCP initialization advertises the shared Codex capability
contract used by the server blueprint policy.
Upstream connection reliability (#2328)
The proxy uses one owned Undici Agent for all upstream MCP traffic. By default its TCP
address-family racing policy sets autoSelectFamily: true with a
PROXY_CONNECT_ATTEMPT_TIMEOUT_MS=1000 per-address-family attempt window. An operator may set a
positive numeric override; invalid, empty, and non-positive values fall back to 1000, while
values below 10 ms clamp to 10 ms. The transport's request method, body, and auth headers are
preserved; the proxy adds only that owned dispatcher.
This is a connection-attempt policy, not an MCP retry policy. For a post-start request, a rejected
POST before response headers produces one request only: the affected JSON-RPC id receives -32000
with Gemus upstream connection failed before a response was received. The raw network reason is
never returned to Codex. During initialize, the same classification writes one sanitized fatal
diagnostic and exits non-zero. Once an SSE response has started, a body that ends before its result
remains the distinct #2068 failure:
Gemus upstream stream disconnected before the tool result was delivered.
On graceful or fatal shutdown, the proxy bounds session DELETE, then transport close, before it
closes the owned Agent. Each stage has a 2-second bound; an Agent that does not close in its bound
has its retained per-origin dispatchers force-destroyed before the Agent is destroyed. Repeated
shutdown signals share the same cleanup. See the transport reliability
runbook for the network boundaries and the
Companion architecture
for the request path.
Release gate (#2330)
@gemus/[email protected] was published and verified on 2026-07-28. Release Issue
#2330 rolls the plugin to exact 0.1.9, forwards
PROXY_CONNECT_ATTEMPT_TIMEOUT_MS, and bumps the plugin contract to 0.1.12. After that change
merges, the remaining gate is to verify the public mirror, refresh the installed Codex plugin
cache, and run repeated fresh-process ESA and, when available, isolated direct-domain smoke tests.
That public 0.1.9 package is immutable and transport-only: it does not contain the one-command
setup added by #2324. The setup release therefore advances to @gemus/[email protected], with
plugin contract 0.1.14; the historical #2328/#2330 release remains 0.1.9/0.1.12.
#2356 advances the proxy to 0.1.11 and the plugin to 0.1.16. It accepts sparse unrelated
Codex 0.142 catalog records while keeping Gemus identity checks strict, and adds the key-free,
single-line setup-env command used by Advanced setup.
#2357/#2363 advance the proxy to 0.1.12 and the plugin to 0.1.17. They decouple setup from
platform-specific filesystem facts: the symlink boundary check now judges only the components
setup itself creates or writes (previously any ancestor symlink, including macOS /var ->
private/var, refused the whole run), and the executable-entry guard resolves process.argv[1]
through realpathSync before comparing it with import.meta.url (previously a symlinked
invocation path — node_modules/.bin shims included — made the CLI exit 0 without doing
anything).
#2365 advances the proxy to 0.1.13 and the plugin to 0.1.18. The Windows install script now
derives its exit code only from persistence: failure is an explicit exit 1 in catch, success an
explicit exit 0 after finally. Previously the best-effort Remove-Item Env:GEMUS_URL cleanup was
the script's last statement, and clearing a variable that was never set (every user who does not set
the optional GEMUS_URL) leaves $? false even under -ErrorAction SilentlyContinue, so PowerShell
5.1 returned 1 and setup reported a failure that had not happened.
What was validated end-to-end (2026-07-06, codex-cli 0.142.2 → local dev /api/mcp)
- Transparent passthrough — initialize + tools/list relay all 12 gemus tools (incl.
execute/batch_execute). SDK transport auto-capturesmcp-session-idfrom the initialize response; raw send/onmessage wiring works with normal sequencing. - (e) proxy-per-session —
codex mcp add gemus -- node <proxy>→ codex spawns one proxy per session (clientInfo: codex-mcp-client 0.142.2); reportsAuth: Unsupported(proxy owns auth). - (d) execute-tap — real codex
tools/call(workflow_list,canvas_read{workflowId}) tapped;workflowIdfromarguments, and turn/thread identity from_meta.x-codex-turn-metadata(thread_id/turn_id) — so per-turn scoping needs no Stop hook.executeis the same path. - (c) turn-end + retrieval —
Stophook fires post-flush carryingtranscript_path→ exact rollout; imagegen writesgenerated_images/<session>/<sanitized-call-id>.png(historicallyig_*, currentlycall_*) + rolloutsaved_path+base64. - Session cap — the key-only session cap defaults to 10 concurrent sessions per user
(
MCP_KEY_ONLY_SESSION_CAP); the proxy MUSTDELETEits upstream session on close (implemented incleanup()) to avoid leaking slots.
Companion setup and migration (Codex desktop)
The plugin owns the non-secret process contract: exact @gemus/[email protected],
startup_timeout_sec = 60, tool_timeout_sec = 300, and a default-disabled rollout. The user
environment owns GEMUS_KEY, optional GEMUS_URL, and optional
PROXY_CONNECT_ATTEMPT_TIMEOUT_MS; the public plugin and generated commands never contain their
values.
For the production Gemus service, use this Windows/macOS setup command; it is safe to rerun:
npx -y @gemus/[email protected] setupsetup also accepts an optional --url <http(s) URL> for a self-hosted or development endpoint —
the same argument shape as setup-env and setup-dsh — and runs on Linux as well as Windows and
macOS.
It securely prompts for the key, reconciles the marketplace/plugin, migrates legacy Gemus config,
enables the Companion, and preserves a backup when it changes config.toml. Fully quit and restart
Codex when it finishes, then open a new task. Setup enables the plugin-owned integration during reconciliation.
Before asking for the Gemus key, setup verifies the Codex CLI and login. If Codex CLI is missing on
Windows, it discloses the mutable official OpenAI install script and asks with a default of no; only
an explicit y/yes runs it. Key typing and paste show one * per character while keeping the
plaintext out of output and diagnostics.
The full setup resolves one absolute executable from PATH only, binds every
reconciliation command to that exact target, validates its codex-cli version output, and prints
the executable, version, and Codex config home before mutation.
The public setup command, shell history, and setup diagnostics remain key-free. On macOS, the
short-lived launchctl setenv child necessarily receives the key in its argv.
Environment ownership and lifetime:
- Windows / PowerShell: persist the key in the Windows user environment scope. Fully quit and restart Codex so it receives the updated environment.
- macOS: If changing login context, sign out and back in first; rerun the
launchctl setenvsetup in the new login session; the value lasts for the current login session only. Then fully quit and restart Codex and open a new task. - Linux:
setupwrites a# >>> gemus >>>managed block into your zsh or bash startup file, so the value survives a reboot; open a new terminal and runcodex. Shells with different syntax (fish and friends) are refused withGEMUS_SHELL_UNSUPPORTED— use the manual steps instead.setup-envstill exports the key and launches Codex from the same terminal.
Companion and direct modes are mutually exclusive. Linux, self-hosted GEMUS_URL, and
troubleshooting use the same key-free single-line environment entrypoint:
npx -y @gemus/[email protected] setup-env
# Self-hosted/development:
npx -y @gemus/[email protected] setup-env --url "https://your-gemus.example/api/mcp"It securely prompts for GEMUS_KEY; optional GEMUS_URL for self-hosted/development is supplied
with --url; credential-bearing, query-string, and fragment URLs are rejected. Windows reuses the
user-environment adapter, macOS reuses the current-login-session
adapter, and Linux launches Codex from the same terminal with the acquired environment. Then:
Run the environment-only setup above.
Remove any legacy global server (
codex mcp remove gemus; not-found is harmless).Install with
codex plugin marketplace add Gemus-AI/gemus-codex-plugin, thencodex plugin add gemus@gemus. Existing users refresh the marketplace snapshot and replace the installed cache:codex plugin marketplace upgrade gemus codex plugin remove gemus@gemus codex plugin add gemus@gemusExplicitly enable the plugin-scoped server:
[plugins."gemus@gemus".mcp_servers.gemus] enabled = trueFully quit and restart Codex, open a new task, then trust the Gemus Stop hook with
/hooks.
Intentional direct HTTP/OAuth users keep their global direct server and leave the plugin Companion disabled:
[plugins."gemus@gemus".mcp_servers.gemus]
enabled = falseDeepSeek Harness one-command install (#2924)
npx -y @gemus/[email protected] setup-dsh [--url "https://your-gemus.example/api/mcp"]setup-dsh acquires the key (masked prompt, or GEMUS_KEY from the environment), persists it,
installs the .dsh-plugin bundle from its public mirror, and then starts dsh in a child that
already carries the key. That last step is the point of the subcommand: environment variables only
reach new processes, and the Windows WM_SETTINGCHANGE broadcast does not refresh terminals that are
already open — so a user who sets the key and starts dsh in the same terminal gets a silent
"the model has no gemus tools", indistinguishable from a bad key.
Persistence differs from setup-env on purpose. Codex Desktop is a GUI app, so setup-env uses
macOS launchctl setenv, which expires with the login session. dsh is a CLI, so setup-dsh writes
a managed # >>> gemus >>> block to the shell startup file (~/.zshrc, or ~/.bash_profile /
~/.bashrc for bash) and matches the Windows behaviour of surviving a reboot. The block is replaced,
never stacked, on rerun. Shells whose syntax is not export (fish) are refused with a pointer to the
manual steps rather than handed a line that would error.
The bundle it installs is config only and does not depend on this package at runtime — a dsh CLI change can only break the installer, never an existing user's session.
Local dev
pnpm --filter @gemus/codex-backfill-core build # proxy imports the built core at runtime
GEMUS_KEY=mak_... GEMUS_URL=http://localhost:3000/api/mcp PROXY_LOG=/tmp/proxy.log node src/proxy.mjsEnv: GEMUS_KEY (required), GEMUS_URL (default https://gemus.ai/api/mcp), PROXY_LOG (optional
debug log), PROXY_BACKFILL_IDLE_MS (default 180000 — turn-idle fallback before salvage-orphan).
