You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Emit the MCP server startup notifications that codex core already produces as first-class events in codex exec --json output, so that tools wrapping codex exec can observe per-server MCP lifecycle (starting, ready, failed, cancelled) without spawning and probing MCP servers themselves.
Why
External tools and agent frameworks that wrap codex exec --json currently have no way to tell:
which configured MCP servers actually started,
which failed (and why),
how long users wait on MCP boot at the start of a turn,
They also cannot populate health dashboards or CI diagnostics without re-implementing the full MCP client path themselves (stdio process group management, docker cleanup, Windows taskkill /T, OAuth token store reuse, transport branching, env allowlists). That is tens of lines of hard-to-maintain lifecycle handling per host, all to rediscover data Codex already knows.
Concrete pain point: issue #17024 (local stdio MCP startup fails even though manual initialize succeeds). A user hitting that today gets no structured signal via exec --json. Codex knows the startup failed and the reason, but the JSONL stream stays silent. A bridge or dashboard cannot even display the failure message without screen-scraping human-readable output.
Closely related prior art:
PR Handle required MCP startup failures across components #10902 (merged). Establishes the required flag for MCP servers and makes codex exec fail fast on required-server startup failure. That change proves maintainers treat MCP startup state as a first-class exec-layer concern. This proposal is the natural complement: let callers see why it failed, in machine-readable form.
RFC 0001: MCP Management Overhaul - Comprehensive Improvements for Q4 2025 #3778 (closed). RFC 0001 proposed a broad codex mcp test <name> [--json] subcommand as part of a larger overhaul. This proposal intentionally does NOT propose a new subcommand. It reuses the existing exec --json surface and the existing notifications that already flow through the app-server protocol layer.
Current state (verified in rust-v0.120.0)
The full pipeline from the MCP connection manager to the app-server protocol layer already exists and is already consumed by the TUI. codex exec --json is the one consumer that drops the notifications.
codex-rs/codex-mcp/src/mcp_connection_manager.rs (lines 747-814)
emits per-server progress events
|
v
EventMsg::McpStartupUpdate(McpStartupUpdateEvent { server, status })
status: McpStartupStatus { Starting | Ready | Failed { error } | Cancelled }
(codex-rs/protocol/src/protocol.rs lines 1439, 3214, 3224)
|
v
codex-rs/app-server/src/bespoke_event_handling.rs (lines 234-262)
translates core event to protocol notification (API v2)
|
v
ServerNotification::McpServerStatusUpdated
wire name: "mcpServer/startupStatus/updated"
payload: { name: String, status: McpServerStartupState, error: Option<String> }
(codex-rs/app-server-protocol/src/protocol/v2.rs lines 5515-5532,
registered in codex-rs/app-server-protocol/src/protocol/common.rs line 991)
|
+-> TUI: consumed by codex-rs/tui/src/chatwidget.rs (line 3063)
|
+-> exec JSONL: codex-rs/exec/src/event_processor_with_jsonl_output.rs line 581
`_ => CodexStatus::Running` catch-all, silently dropped
collect_thread_events in event_processor_with_jsonl_output.rs has no arm for ServerNotification::McpServerStatusUpdated. The notification falls through the _ => catch-all at the end of the match.
Proposal
Add four ThreadEvent variants (in codex-rs/exec/src/exec_events.rs) and handle the existing notification in collect_thread_events:
Mapping from the existing McpServerStartupState enum:
Starting to mcp.server.init_started
Ready to mcp.server.ready
Failed to mcp.server.failed { name, error } (preserves notification.error passthrough)
Cancelled to mcp.server.cancelled { name, error } (preserves notification.error passthrough)
Cancelled is kept distinct from Failed to match how codex-rs/tui/src/chatwidget.rs:2932,3070 already treats the two separately. An interrupted startup is not the same as an initialization failure and consumers should be able to distinguish them.
Scope
Five files, +251 LOC, zero deletions, within a single crate (codex-rs/exec):
codex-rs/exec/src/exec_events.rs: new ThreadEvent variants and payload structs.
codex-rs/exec/src/event_processor_with_jsonl_output.rs: new arm in collect_thread_events before the _ => catch-all, plus imports.
codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs: unit test covering all four state transitions.
codex-rs/exec/src/lib.rs: public re-exports of the four new payload structs (matches existing alphabetical re-export pattern for every other exec_events payload type).
codex-rs/exec/tests/event_processor_with_json_output.rs: integration test asserting both the enum mapping and the serialized JSON wire shape (type string values plus skip_serializing_if = "Option::is_none" behavior for the optional error field).
No changes to codex-core, codex-mcp, codex-protocol, codex-app-server, or codex-app-server-protocol. No new crate dependencies. No changes to any manually-maintained schema file.
Backwards compatibility
Purely additive. Existing codex exec --json consumers that match only on thread.started, turn.*, or item.* event types will see the new events as unknown type values and can safely ignore them. No existing event shape changes.
Extend codex mcp list --json to include runtime state: possible but does not help callers that need live lifecycle updates during a turn (e.g. "a required server just failed, abort the turn cleanly").
Probe MCP servers directly from the external tool: what we have been doing. Requires replicating process group management, transport branching, OAuth token store reuse, and Windows and docker lifecycle handling. Fragile and duplicates Codex code.
Put timing (boot_ms) and tool listings (tools[]) in this first change: intentionally deferred. This proposal only surfaces data Codex already emits. A follow-up issue can propose adding boot_ms to McpServerStatusUpdatedNotification (cross-crate change, wire-type evolution); tool listings are already available via the existing mcpServerStatus/list RPC so consumers can call that after seeing ready.
Open questions
Is codex exec --json considered a stable public API, or best-effort debug output? This proposal would benefit from an explicit stability statement in docs/exec.md for the new events.
Should the events carry a timestamp field? Existing thread.*, turn.*, and item.* events do not, so omitting for consistency, but open to guidance.
Should mcp.server.cancelled distinguish "user interrupted startup" from "startup deadline exceeded"? The current McpStartupStatus::Cancelled variant in core has no further discriminator; current draft preserves notification.error string passthrough.
Implementation
I have a draft patch applied against rust-v0.120.0 on a local feature branch feat/mcp-init-events-exec-json. Happy to open a PR if this proposal is directionally acceptable. Everything is in the codex-exec crate; no changes to codex-core, codex-mcp, or protocol crates.
Verification results:
cargo build -p codex-exec: clean
cargo test -p codex-exec: 40 lib + 62 integration, all passing (including new unit test and new integration test)
This is driven by maintaining codex-mcp-bridge (an open-source MCP server that wraps codex exec) and specifically by the introspection phase of its design. Without the change, each external consumer has to re-implement MCP probing against their own config-parsing rules, including process group cleanup, transport branching, OAuth token store reuse, and OS-specific lifecycle quirks. With the change, they parse the JSONL they already consume.
No equivalent structured per-server MCP lifecycle stream exists in other MCP hosts I could verify (Claude Code's /mcp and claude mcp list are plain-text output, @modelcontextprotocol/sdk exposes init only via await client.connect() and UnauthorizedError, spec-level lifecycle is handshake-based). Codex is well-positioned to set a pattern here.
What feature would you like to see?
Emit the MCP server startup notifications that
codexcore already produces as first-class events incodex exec --jsonoutput, so that tools wrappingcodex execcan observe per-server MCP lifecycle (starting, ready, failed, cancelled) without spawning and probing MCP servers themselves.Why
External tools and agent frameworks that wrap
codex exec --jsoncurrently have no way to tell:required(PR Handle required MCP startup failures across components #10902) is the reasonexecis about to fail fast.They also cannot populate health dashboards or CI diagnostics without re-implementing the full MCP client path themselves (stdio process group management, docker cleanup, Windows
taskkill /T, OAuth token store reuse, transport branching, env allowlists). That is tens of lines of hard-to-maintain lifecycle handling per host, all to rediscover data Codex already knows.Concrete pain point: issue #17024 (local stdio MCP startup fails even though manual
initializesucceeds). A user hitting that today gets no structured signal viaexec --json. Codex knows the startup failed and the reason, but the JSONL stream stays silent. A bridge or dashboard cannot even display the failure message without screen-scraping human-readable output.Closely related prior art:
requiredflag for MCP servers and makescodex execfail fast on required-server startup failure. That change proves maintainers treat MCP startup state as a first-class exec-layer concern. This proposal is the natural complement: let callers see why it failed, in machine-readable form.codex mcp test <name> [--json]subcommand as part of a larger overhaul. This proposal intentionally does NOT propose a new subcommand. It reuses the existingexec --jsonsurface and the existing notifications that already flow through the app-server protocol layer.Current state (verified in
rust-v0.120.0)The full pipeline from the MCP connection manager to the app-server protocol layer already exists and is already consumed by the TUI.
codex exec --jsonis the one consumer that drops the notifications.collect_thread_eventsinevent_processor_with_jsonl_output.rshas no arm forServerNotification::McpServerStatusUpdated. The notification falls through the_ =>catch-all at the end of the match.Proposal
Add four
ThreadEventvariants (incodex-rs/exec/src/exec_events.rs) and handle the existing notification incollect_thread_events:Payloads:
Mapping from the existing
McpServerStartupStateenum:Startingtomcp.server.init_startedReadytomcp.server.readyFailedtomcp.server.failed { name, error }(preservesnotification.errorpassthrough)Cancelledtomcp.server.cancelled { name, error }(preservesnotification.errorpassthrough)Cancelledis kept distinct fromFailedto match howcodex-rs/tui/src/chatwidget.rs:2932,3070already treats the two separately. An interrupted startup is not the same as an initialization failure and consumers should be able to distinguish them.Scope
Five files, +251 LOC, zero deletions, within a single crate (
codex-rs/exec):codex-rs/exec/src/exec_events.rs: newThreadEventvariants and payload structs.codex-rs/exec/src/event_processor_with_jsonl_output.rs: new arm incollect_thread_eventsbefore the_ =>catch-all, plus imports.codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs: unit test covering all four state transitions.codex-rs/exec/src/lib.rs: public re-exports of the four new payload structs (matches existing alphabetical re-export pattern for every otherexec_eventspayload type).codex-rs/exec/tests/event_processor_with_json_output.rs: integration test asserting both the enum mapping and the serialized JSON wire shape (typestring values plusskip_serializing_if = "Option::is_none"behavior for the optionalerrorfield).No changes to
codex-core,codex-mcp,codex-protocol,codex-app-server, orcodex-app-server-protocol. No new crate dependencies. No changes to any manually-maintained schema file.Backwards compatibility
Purely additive. Existing
codex exec --jsonconsumers that match only onthread.started,turn.*, oritem.*event types will see the new events as unknowntypevalues and can safely ignore them. No existing event shape changes.Alternatives considered
codex mcp test [--json]subcommand (proposed in RFC 0001: MCP Management Overhaul - Comprehensive Improvements for Q4 2025 #3778 RFC 0001): adds a second surface that every external tool needs to integrate with separately. Reusingexec --jsonpiggybacks on the stream they already consume.codex mcp list --jsonto include runtime state: possible but does not help callers that need live lifecycle updates during a turn (e.g. "a required server just failed, abort the turn cleanly").boot_ms) and tool listings (tools[]) in this first change: intentionally deferred. This proposal only surfaces data Codex already emits. A follow-up issue can propose addingboot_mstoMcpServerStatusUpdatedNotification(cross-crate change, wire-type evolution); tool listings are already available via the existingmcpServerStatus/listRPC so consumers can call that after seeingready.Open questions
codex exec --jsonconsidered a stable public API, or best-effort debug output? This proposal would benefit from an explicit stability statement indocs/exec.mdfor the new events.timestampfield? Existingthread.*,turn.*, anditem.*events do not, so omitting for consistency, but open to guidance.mcp.server.cancelleddistinguish "user interrupted startup" from "startup deadline exceeded"? The currentMcpStartupStatus::Cancelledvariant in core has no further discriminator; current draft preservesnotification.errorstring passthrough.Implementation
I have a draft patch applied against
rust-v0.120.0on a local feature branchfeat/mcp-init-events-exec-json. Happy to open a PR if this proposal is directionally acceptable. Everything is in thecodex-execcrate; no changes tocodex-core,codex-mcp, or protocol crates.Verification results:
cargo build -p codex-exec: cleancargo test -p codex-exec: 40 lib + 62 integration, all passing (including new unit test and new integration test)cargo clippy -p codex-exec --all-targets -- -D warnings: cleancargo fmt -p codex-exec -- --check: cleanContext
This is driven by maintaining
codex-mcp-bridge(an open-source MCP server that wrapscodex exec) and specifically by the introspection phase of its design. Without the change, each external consumer has to re-implement MCP probing against their own config-parsing rules, including process group cleanup, transport branching, OAuth token store reuse, and OS-specific lifecycle quirks. With the change, they parse the JSONL they already consume.No equivalent structured per-server MCP lifecycle stream exists in other MCP hosts I could verify (Claude Code's
/mcpandclaude mcp listare plain-text output,@modelcontextprotocol/sdkexposes init only viaawait client.connect()andUnauthorizedError, spec-level lifecycle is handshake-based). Codex is well-positioned to set a pattern here.