Skip to content

fix(mcp,doctor): serve MCP under a pty; explicit Codex args = ["mcp"] - #1616

Merged
yvgude merged 3 commits into
mainfrom
fix/1597-codex-mcp-args
Aug 30, 2026
Merged

fix(mcp,doctor): serve MCP under a pty; explicit Codex args = ["mcp"]#1616
yvgude merged 3 commits into
mainfrom
fix/1597-codex-mcp-args

Conversation

@yvgude

@yvgude yvgude commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Supersedes #1597 by @reysilvaa, whose commit is preserved here with their authorship — I could not push to their fork (403), so the branch lives in this repo instead. Also fixes the root cause their report uncovered.

What @reysilvaa got right

args = ["mcp"] really is the correct default, and their writer changes land unchanged: write_codex_config, upsert_codex_toml and ensure_codex_mcp_server now emit it, matching what the Copilot writer already did.

What I changed in their patch

1. The red test. codex_toml_upserts_existing_section still asserted args = [] and was taking down Test (ubuntu-latest), Test (windows-latest) and Coverage. Fixed, plus a companion test for the behaviour the writer deliberately preserves: args = ["mcp", "--verbose"] survives verbatim instead of being flattened.

2. args = [] is not drift. Their check_codex_toml required the mcp argument, which would have turned every existing Codex installation red. Both args = [] and an absent args start the stdio server (cli/dispatch/server.rs matches None | Some("mcp")), so the doctor would have reported a working config as broken and --fix would have rewritten it into a semantically identical file — the #1596 failure mode, hitting far more users than the change helps. The check now accepts absent / empty / mcp-carrying args and flags only an args array that replaces the entry point (["dashboard"]).

The root cause — and my earlier review was wrong about it

I told @reysilvaa and the reporter of #1595 that bare invocation still serves MCP, and showed a probe to prove it. That probe was wrong: I piped stdin. The reporter's environment gives the process a PTY, and cli/dispatch/mod.rs treats a TTY on stdin as "a human is here", prints the quickstart and exits.

Reproduced against the shipped 3.10.1 binary:

stdin = pipe          →  "lean-ctx v3.10.1 MCP server starting"   ✓
stdin = pty (bare)    →  quickstart text, process exits           ✗
stdin = pty + "mcp"   →  server starts                            ✓

So the report was accurate and args = ["mcp"] was a real workaround, not a cosmetic one. Devin, containerized agent runners, and anything driving the child through script/expect allocate a PTY; every one of them silently failed to connect, with nothing in the logs to explain it.

The fix: a TTY tells us a human may be watching — it never tells us a client is absent. The quickstart now goes to stderr (which MCP clients log rather than parse) and the server starts either way. The human reads what is happening and presses Ctrl-C; the client completes its handshake on stdout. The quickstart text says so instead of claiming the process "is waiting silently".

rust/tests/suite/mcp_pty_startup_1595.rs allocates a real pty with openpty (echo off), spawns the binary with no subcommand, and requires an initialize response. It fails against the previous behaviour.

Verification

  • cargo test --lib → 10546 passed, 0 failed
  • cargo clippy --lib --all-features -- -D warnings → clean
  • cargo fmt --check → clean
  • new pty integration test passes; removing the fix makes it fail

Fixes #1595
Closes #1597

Co-authored-by: reysilvaa reynaldsilva123@gmail.com

🤖 Generated with Claude Code

reysilvaa and others added 3 commits August 30, 2026 09:01
Maintainer follow-up on #1597, applied to the contributor's branch so the work
stays theirs. Two changes, both from the review.

1. `codex_toml_upserts_existing_section` still asserted `args = []` and took
   down Test (ubuntu-latest), Test (windows-latest) and Coverage. It now
   asserts the writer's new output, and a companion test pins the case the
   writer deliberately leaves alone: `args = ["mcp", "--verbose"]` survives
   verbatim rather than being flattened to `["mcp"]`.

2. `check_codex_toml` required the `mcp` argument, which made every existing
   installation drift. `args = []` and an absent `args` both start the stdio
   server — `cli/dispatch/server.rs` matches `None | Some("mcp")` — so the
   doctor would have gone red for users whose config works, and `--fix` would
   have rewritten a functional file into a semantically identical one. That is
   the #1596 failure mode. The check now accepts absent, empty, and
   `mcp`-carrying `args`, and flags only an `args` array that replaces the
   entry point with something else (`["dashboard"]`).

The writer side keeps the PR's behaviour unchanged: new installs are written
explicitly as `args = ["mcp"]`, matching the Copilot writer. New installs are
explicit, existing installs stay green, nobody's file is rewritten for nothing.

The `args = ["mcp"]` default is worth having on its own merits, and it is also
a real workaround for #1595: a client that spawns the server under a pty gets
the quickstart instead of a server on a bare invocation. The root cause of that
is fixed separately.

Verification: cargo test --lib (10546 passed, 0 failed), cargo clippy --lib
--all-features -D warnings clean, cargo fmt clean.

Co-Authored-By: reysilvaa <reysilvaa@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#1595 reported that upgrading 3.9.18 → 3.9.20 broke the MCP connection unless
`args = ["mcp"]` was added to the Codex config. I reviewed that report, probed
a shipped binary, and told the reporter it did not reproduce. My probe piped
stdin. Their environment does not — Devin, containerized agent runners, and
anything driving the child through `script`/`expect` hand the process a pty.

    stdin = pipe        -> "lean-ctx 3.10.1 MCP server starting"   works
    stdin = pty, bare   -> quickstart text, process exits          broken
    stdin = pty + mcp   -> server starts                           works

`run()` treated a terminal on stdin as proof that a human was there, printed
the quickstart and returned. For every client that allocates a pty the server
therefore never came up, and nothing in the client's log explained why: the
child exited 0 after writing help text to stdout.

A TTY says a human *may* be watching. It never says a client is absent, and
there is no fd-level signal that distinguishes the two. So the quickstart stops
being a decision about whether to serve: it goes to stderr — which MCP clients
log rather than parse — and the server starts either way. The human sees the
explanation and presses Ctrl-C; the client completes its handshake on stdout.
The text now says the process is serving and how to stop it, instead of
claiming it "is waiting silently" before exiting.

`mcp_pty_startup_1595` allocates a real pty with `openpty` (echo off), spawns
the binary with no subcommand, and requires an `initialize` response. Restoring
the `return` makes it fail, so it pins the behaviour rather than the wording.

Verification: cargo test --lib (10538 passed, 0 failed), the new pty test green
and red without the fix, cargo clippy --lib --all-features -D warnings clean,
cargo fmt clean.

Fixes #1595

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@reysilvaa

Copy link
Copy Markdown
Contributor

Thanks for review my pull request, and i kindly help to keep contributing in this project

@yvgude
yvgude merged commit df31e06 into main Aug 30, 2026
29 checks passed
@yvgude
yvgude deleted the fix/1597-codex-mcp-args branch August 30, 2026 07:54
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: 3.9.20 upgrade breaks mcp

2 participants