[FEATURE] Add shared loopback HTTP transport - #7
Conversation
- Compare stdio and HTTP lifecycle and resource trade-offs - Document authenticated systemd and launchd setup
There was a problem hiding this comment.
🟡 Changes recommended
It introduces a potentially flaky test due to a hard-coded HTTP port and contains a README statement that no longer matches the updated readiness log output.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an opt-in authenticated loopback Streamable HTTP transport (alongside the existing stdio default) for wrapper-owned providers, and updates the wrapper to MCP v2-era APIs to support both legacy and 2026-07-28 “modern” exchanges per request/connection.
Changes:
- Add
--transport httpmode with bearer-token auth, loopback Host/Origin validation, and ahttp-auth-headershelper command for clients that support dynamic headers (e.g., Claude Code). - Pool Codex App Server runtimes by canonical project root for HTTP requests, including idle reaping and safe shutdown draining.
- Expand the fast test suite to cover HTTP auth/security, runtime pooling/eviction, and modern-era negotiation/foreground continuation behavior.
File summaries
| File | Description |
|---|---|
| test.sh | Adds modern stdio negotiation tests, HTTP daemon contract tests, Codex HTTP pooling/eviction tests, and closed-stderr shutdown regression coverage. |
| server.js | Implements authenticated loopback HTTP transport, token management, Codex runtime pooling for HTTP, and MCP v2 server/stdio handling with modern-era support. |
| README.md | Documents transport modes and configuration for running a shared HTTP daemon and dynamic auth header helper usage. |
| package.json | Replaces the v1 SDK dependency with MCP v2 @modelcontextprotocol/server/node and adds @modelcontextprotocol/client for tests. |
| package-lock.json | Updates lockfile to reflect the MCP v2 dependency graph changes. |
| CHANGELOG.md | Adds unreleased entries describing HTTP transport support, modern negotiation, and the stderr-EPIPE shutdown fix. |
Review details
- Files reviewed: 5/6 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| local tmpdir shared_dir stderr_output status mode | ||
|
|
||
| echo "--- $label ---" | ||
| tmpdir=$(mktemp -d) | ||
| shared_dir="$tmpdir/shared" | ||
| mkdir "$shared_dir" | ||
| chmod 0755 "$shared_dir" | ||
| set +e | ||
| stderr_output=$($TIMEOUT_CMD 2 $SERVER --provider gemini \ | ||
| --transport http --http-port 18766 \ | ||
| --http-token-file "$shared_dir/bearer-token" 2>&1 >/dev/null) |
| The bridge writes `[mcp-agents] ready (provider: <name>)` to stderr when | ||
| listening; stdout remains MCP-only. Use the Quickstart configuration above if | ||
| you prefer this mode. |
Review: 5 blockers — please holdThe security boundary here is genuinely well built, and I want to lead with that because I tested it rather than read it. Against a running daemon:
The problems aren't in the security boundary. They're all one root cause:
Line numbers are from this branch's tree. 🔴 Blocker — one App Server child exit pins the pooled runtime forever
So that turn is in
Triggers aren't exotic: any child crash or OOM, the deliberate Under stdio this self-healed when the per-client bridge exited. The daemon removes that escape hatch, so the feature meant to reduce process count strands one Suggested fix: give 🔴 Blocker — the per-request abort signal interrupts the very turn
|
Follow-up: independent Codex review converged on the same defects, plus two new onesI ran a second, fully independent review through OpenAI Codex — fresh session, read-only, given the tree and the intent but not my findings, so it read everything cold. It landed on the same five defects, at the same functions. Given they were found twice by different means, I'd treat them as settled rather than debatable.
Codex's overall call, in its own words:
Note it rates the Two findings I missed1. HTTP disconnect never cancels a blocking Claude/Gemini call —
Failure: a client makes a blocking I'd rate this higher than Codex's P2, precisely because "many processes alive" is the complaint this PR is meant to answer. 2. The runtime pool is unbounded, and
Reframing the cross-client finding — this one is Thomas's call, not a bug reportI owe you a correction on how I framed the job-visibility blocker. Codex pointed me at That changes the finding but doesn't remove it. As written, the PR knowingly contradicts the AGENTS.md invariant "Background job records remain connection-local" without amending AGENTS.md, the CHANGELOG, or the README. So the question isn't "is this a bug" — it's "is this the intended semantics for the daemon, and if so, should the invariant be rewritten?" That's a maintainer decision. @thomaswitt should rule on it; if the answer is yes, the invariant and the docs need to move with it, and What Codex independently confirmed as soundWorth recording, since it clears several things I had flagged only for scrutiny:
Codex could not run |
Two further findings from a second independent Codex runI ran Codex a second time in a separate session (different thread, again read-only, again given no findings from me). It reproduced everything above independently, and surfaced two defects that neither the first Codex run nor my own review caught. Flagging them because one of them undermines a core assumption of the pooling design. 🔴 The project-root header does not constrain where the turn actually runs
These are two entirely independent client-supplied inputs:
The only validation applied to Failure: a request routed under project A executes in project B, while its sessions, leases, bridge sidecars and interactions are all stored under A's state hash. A later request correctly routed under B cannot find that state. And combined with the cross-client visibility above, clients sharing A's runtime can observe and control work that is really running in B. Codex rated this Medium. I'd put it with the blockers: it means the pool key is a routing hint rather than an isolation boundary, which is the opposite of what "pool by canonical project root" implies — and it's the assumption the whole HTTP design rests on. Suggested fix: require 🔵
|
Blocked by:
Merge order: #5 → #6 → #7. PRs #5 and #6 must be merged and closed
before this PR.
Summary
codex,claude, andgeminiproviders.browserand frozencodex-legacytransport boundaries.request-contained
2026-07-28exchanges remain independent.promise-deduplicated startup and safe idle reaping.
http-auth-headershelper for Claude Code's dynamicAuthorizationheader.
Claude configuration, and Linux/macOS user services.
The branch is directly stacked on PR #6, which is stacked on PR #5. Because
the head branches live in a fork and I do not have upstream push permission,
this PR targets
main; its diff will include both dependencies until theymerge.
Transport and lifecycle
codexclaudegeminibrowsercodex-legacyCodex runtimes remain alive while an MCP request, foreground turn,
interaction, startup, or nonterminal background job is active. Once a runtime
becomes evictable, it receives the full idle grace period before shutdown.
Concurrent first requests for the same canonical root share one startup
promise; symlink aliases resolve to the same pool key.
Local security boundary
127.0.0.1./mcprequest.0600under a current-user-owned private directory.and cross-origin requests.
X-Mcp-Agents-Project-Rootfor Codex HTTP requests and canonicalizeit before runtime lookup.
Verification
validation, modern and legacy HTTP, canonical-root pooling, idle reaping,
active-work retention, and shutdown.
headersHelper, reported the HTTPserver connected, negotiated the modern era, and called
ping, returningpong.HTTP_POOL_OK; a second clientreused App Server generation 1.
1444.6 MiB RSS / 7471.7 ms; HTTP used 1 process / 140.6 MiB RSS / 3248.3 ms.
as valid XML.
node --check server.js,bash -n test.sh,git diff --check,npm ls --depth=0, andnpm pack --dry-run --json: passed.SKIP_INTEGRATION=1 ./test.sh: 390 passed, 1deadline-sensitive legacy timing failure. Every HTTP test and the final
child-leak check passed; the failed legacy case passed when rerun in
isolation.