Collapse MCP startup notifications into a single aggregate readiness summary
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/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:
``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
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:
- Branch: https://github.com/team-wcv/codex/tree/feat/collapse-mcp-startup-notifications
- Diff: https://github.com/team-wcv/codex/compare/main...feat/collapse-mcp-startup-notifications
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.- App-server protocol schema artifacts regenerated via
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-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.