Summary
codex app-server fires one mcpServer/startupStatus/updated JSON-RPC notification per server per status transition (starting -> ready / failed / cancelled). For a config with N MCP servers, that is ~2N notifications during a single thread bootstrap, and harnesses driving codex app-server over stdio end up writing ad-hoc filters to silence the firehose once they only care about the final readiness state.
Codex actually already aggregates this internally as EventMsg::McpStartupComplete(McpStartupCompleteEvent { ready, failed, cancelled }) — codex-mcp/src/connection_manager.rs:294-313 fires it after all server tasks join — but app-server/src/bespoke_event_handling.rs never lifts it onto the JSON-RPC surface, so external clients cannot subscribe to it. Today they have to track per-server transitions, count the configured server names, and synthesize the aggregate themselves.
A real downstream filter that this would make redundant:
// downstream wrapper TUI: only render an MCP tool entry when the per-server
// notification reports a real failure; everything else is suppressed because
// the firehose is too noisy.
if (method === 'mcpServer/startupStatus/updated') {
const showMcpStartupFailure = Boolean(params.error) || statusIsFailed(params.status);
if (!showMcpStartupFailure) return;
}
The codex TUI's live MCP startup panel (tui/src/chatwidget/mcp_startup.rs) does want the per-server stream, so the fix can't be "stop emitting individual updates". It needs to be per-deployment opt-out.
Proposed solution
-
New aggregate JSON-RPC notification mcpServer/startupStatus/completed with payload McpServerStartupCompletedNotification { ready: Vec<String>, failed: Vec<{ name, error }>, cancelled: Vec<String> }, always emitted when the existing internal EventMsg::McpStartupComplete fires. Clients can render the equivalent of MCP: 5 ready, 1 failed (foo), 0 cancelled from a single message.
-
Per-deployment opt-out for the per-server stream via [notifications] in config.toml:
[notifications]
mcp_startup_individual = false # only emit the aggregate summary
Default is true (preserves the historical "one notification per server per transition" behavior the codex TUI relies on). Setting false suppresses mcpServer/startupStatus/updated but never the aggregate, so failure detail is still observable.
-
Encapsulation via a small ServerNotificationsConfig helper that future [notifications] knobs can extend, rather than threading &Config through every event handler.
Reference implementation
A working implementation with tests lives on the team-wcv fork:
Touches:
codex-rs/app-server-protocol/src/protocol/v2/mcp.rs — McpServerStartupFailureSummary + McpServerStartupCompletedNotification payload types.
codex-rs/app-server-protocol/src/protocol/common.rs — register McpServerStartupCompleted => \"mcpServer/startupStatus/completed\".
codex-rs/config/src/config_toml.rs — NotificationsToml { mcp_startup_individual } + pub notifications: Option<NotificationsToml> on ConfigToml.
codex-rs/core/src/config/mod.rs — resolves into a flat Config::notifications_mcp_startup_individual: bool (default true).
codex-rs/core/config.schema.json — regenerated via just write-config-schema.
codex-rs/app-server/src/server_notifications_config.rs — new module with ServerNotificationsConfig::{from_config, emit_individual_mcp_startup} plus a with_individual_mcp_startup_disabled() test helper.
codex-rs/app-server/src/request_processors/{thread_lifecycle,thread_processor,turn_processor}.rs — plumb notifications_config through ListenerTaskContext.
codex-rs/app-server/src/bespoke_event_handling.rs — gate McpServerStatusUpdated behind emit_individual_mcp_startup(), handle EventMsg::McpStartupComplete to emit the aggregate. Three new tests cover (1) per-server emission on default config, (2) per-server suppression when the flag is off, and (3) aggregate emission regardless of the per-server gate.
- App-server protocol schema artifacts regenerated via
just write-app-server-schema (new v2/McpServerStartupCompletedNotification.{json,ts} + v2/McpServerStartupFailureSummary.ts).
cargo test -p codex-app-server --lib → 221 passed, 0 failed (218 pre-existing + 3 new). cargo test --lib -p codex-app-server -p codex-config -p codex-core → 1746 passed, 0 failed. cargo clippy --no-deps clean on all touched crates. just fmt applied.
Per `docs/contributing.md`
External contributions are by invitation only, so this is filed as an enhancement request with reference implementation attached. Happy to open the PR against this repo if the approach is acceptable.
Summary
codex app-serverfires onemcpServer/startupStatus/updatedJSON-RPC notification per server per status transition (starting->ready/failed/cancelled). For a config with N MCP servers, that is ~2N notifications during a single thread bootstrap, and harnesses drivingcodex app-serverover stdio end up writing ad-hoc filters to silence the firehose once they only care about the final readiness state.Codex actually already aggregates this internally as
EventMsg::McpStartupComplete(McpStartupCompleteEvent { ready, failed, cancelled })—codex-mcp/src/connection_manager.rs:294-313fires it after all server tasks join — butapp-server/src/bespoke_event_handling.rsnever lifts it onto the JSON-RPC surface, so external clients cannot subscribe to it. Today they have to track per-server transitions, count the configured server names, and synthesize the aggregate themselves.A real downstream filter that this would make redundant:
The codex TUI's live MCP startup panel (
tui/src/chatwidget/mcp_startup.rs) does want the per-server stream, so the fix can't be "stop emitting individual updates". It needs to be per-deployment opt-out.Proposed solution
New aggregate JSON-RPC notification
mcpServer/startupStatus/completedwith payloadMcpServerStartupCompletedNotification { ready: Vec<String>, failed: Vec<{ name, error }>, cancelled: Vec<String> }, always emitted when the existing internalEventMsg::McpStartupCompletefires. Clients can render the equivalent ofMCP: 5 ready, 1 failed (foo), 0 cancelledfrom a single message.Per-deployment opt-out for the per-server stream via
[notifications]inconfig.toml:Default is
true(preserves the historical "one notification per server per transition" behavior the codex TUI relies on). SettingfalsesuppressesmcpServer/startupStatus/updatedbut never the aggregate, so failure detail is still observable.Encapsulation via a small
ServerNotificationsConfighelper that future[notifications]knobs can extend, rather than threading&Configthrough every event handler.Reference implementation
A working implementation with tests lives on the team-wcv fork:
Touches:
codex-rs/app-server-protocol/src/protocol/v2/mcp.rs—McpServerStartupFailureSummary+McpServerStartupCompletedNotificationpayload types.codex-rs/app-server-protocol/src/protocol/common.rs— registerMcpServerStartupCompleted => \"mcpServer/startupStatus/completed\".codex-rs/config/src/config_toml.rs—NotificationsToml { mcp_startup_individual }+pub notifications: Option<NotificationsToml>onConfigToml.codex-rs/core/src/config/mod.rs— resolves into a flatConfig::notifications_mcp_startup_individual: bool(defaulttrue).codex-rs/core/config.schema.json— regenerated viajust write-config-schema.codex-rs/app-server/src/server_notifications_config.rs— new module withServerNotificationsConfig::{from_config, emit_individual_mcp_startup}plus awith_individual_mcp_startup_disabled()test helper.codex-rs/app-server/src/request_processors/{thread_lifecycle,thread_processor,turn_processor}.rs— plumbnotifications_configthroughListenerTaskContext.codex-rs/app-server/src/bespoke_event_handling.rs— gateMcpServerStatusUpdatedbehindemit_individual_mcp_startup(), handleEventMsg::McpStartupCompleteto emit the aggregate. Three new tests cover (1) per-server emission on default config, (2) per-server suppression when the flag is off, and (3) aggregate emission regardless of the per-server gate.just write-app-server-schema(newv2/McpServerStartupCompletedNotification.{json,ts}+v2/McpServerStartupFailureSummary.ts).cargo test -p codex-app-server --lib→ 221 passed, 0 failed (218 pre-existing + 3 new).cargo test --lib -p codex-app-server -p codex-config -p codex-core→ 1746 passed, 0 failed.cargo clippy --no-depsclean on all touched crates.just fmtapplied.Per `docs/contributing.md`
External contributions are by invitation only, so this is filed as an enhancement request with reference implementation attached. Happy to open the PR against this repo if the approach is acceptable.