Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f549049
Surface MCP startup failures in tui_app_server
etraut-openai Mar 27, 2026
916a160
codex: address PR review feedback (#16041)
etraut-openai Mar 27, 2026
04d8195
codex: address PR review feedback (#16041)
etraut-openai Mar 27, 2026
a055cdd
codex: address PR review feedback (#16041)
etraut-openai Mar 27, 2026
5fe6436
Merge branch 'main' of github.com:openai/codex into etraut/tuiappserv…
etraut-openai Mar 28, 2026
ab30524
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
6ae5dcb
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
5249448
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
0915e75
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
13ecd08
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
dedb278
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
f940073
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
a5d2cc4
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
e524e32
Merge origin/main into etraut/tuiappserver-mcp-regression
etraut-openai Mar 28, 2026
c101820
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
5c5381f
codex: address PR review feedback (#16041)
etraut-openai Mar 28, 2026
26277b4
Merge branch 'main' into etraut/tuiappserver-mcp-regression
etraut-openai Mar 28, 2026
7a9f162
codex: address PR review feedback (#16041)
etraut-openai Mar 29, 2026
6207cc4
codex: address PR review feedback (#16041)
etraut-openai Mar 29, 2026
5c9ea7b
Merge branch 'main' into etraut/tuiappserver-mcp-regression
etraut-openai Mar 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions codex-rs/tui_app_server/src/app/app_server_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::exec_command::split_command_string;
use codex_app_server_client::AppServerEvent;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::McpServerStartupState;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
#[cfg(test)]
Expand Down Expand Up @@ -119,6 +120,7 @@ impl App {
skipped,
"app-server event consumer lagged; dropping ignored events"
);
self.chat_widget.finish_mcp_startup_after_lag();
}
AppServerEvent::ServerNotification(notification) => {
self.handle_server_notification_event(app_server_client, notification)
Expand Down Expand Up @@ -146,6 +148,22 @@ impl App {
self.pending_app_server_requests
.resolve_notification(&notification.request_id);
}
ServerNotification::McpServerStatusUpdated(notification) => {
if notification.status == McpServerStartupState::Starting
&& self.chat_widget.needs_mcp_startup_expected_servers()
{
let enabled_config_mcp_servers: Vec<String> = self
.chat_widget
.config_ref()
.mcp_servers
.get()
.iter()
.filter_map(|(name, server)| server.enabled.then_some(name.clone()))
.collect();
self.chat_widget
.set_mcp_startup_expected_servers(enabled_config_mcp_servers);
}
}
ServerNotification::AccountRateLimitsUpdated(notification) => {
self.chat_widget.on_rate_limit_snapshot(Some(
app_server_rate_limit_snapshot_to_core(notification.rate_limits.clone()),
Expand Down
153 changes: 138 additions & 15 deletions codex-rs/tui_app_server/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ use codex_app_server_protocol::ErrorNotification;
use codex_app_server_protocol::FileChangeRequestApprovalParams;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
use codex_app_server_protocol::McpServerStartupState;
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ThreadItem;
Expand Down Expand Up @@ -795,6 +797,10 @@ pub(crate) struct ChatWidget {
/// bottom pane is treated as "running" while this is populated, even if no agent turn is
/// currently executing.
mcp_startup_status: Option<HashMap<String, McpStartupStatus>>,
/// Expected MCP servers for the current startup round, seeded from enabled local config.
mcp_startup_expected_servers: Option<HashSet<String>>,
/// After startup settles, ignore late terminal updates until a new startup round begins.
mcp_startup_ignore_updates_until_next_start: bool,
connectors_cache: ConnectorsCacheState,
connectors_partial_snapshot: Option<ConnectorsSnapshot>,
connectors_prefetch_in_flight: bool,
Expand Down Expand Up @@ -2780,15 +2786,51 @@ impl ChatWidget {
self.request_redraw();
}

#[cfg(test)]
fn on_mcp_startup_update(&mut self, ev: McpStartupUpdateEvent) {
let mut status = self.mcp_startup_status.take().unwrap_or_default();
if let McpStartupStatus::Failed { error } = &ev.status {
fn update_mcp_startup_status(
&mut self,
server: String,
status: McpStartupStatus,
complete_when_settled: bool,
) {
if self.mcp_startup_ignore_updates_until_next_start {
if !matches!(status, McpStartupStatus::Starting) {
return;
}
self.mcp_startup_ignore_updates_until_next_start = false;
}
let mut startup_status = self.mcp_startup_status.take().unwrap_or_default();
if let McpStartupStatus::Failed { error } = &status {
self.on_warning(error);
}
status.insert(ev.server, ev.status);
self.mcp_startup_status = Some(status);
startup_status.insert(server, status);
self.mcp_startup_status = Some(startup_status);
self.update_task_running_state();
if complete_when_settled
&& let Some(current) = &self.mcp_startup_status
&& let Some(expected_servers) = &self.mcp_startup_expected_servers
&& !current.is_empty()
&& expected_servers
.iter()
.all(|name| current.contains_key(name))
&& current
.values()
.all(|state| !matches!(state, McpStartupStatus::Starting))
{
let mut failed = Vec::new();
let mut cancelled = Vec::new();
for (name, state) in current {
match state {
McpStartupStatus::Ready => {}
McpStartupStatus::Failed { .. } => failed.push(name.clone()),
McpStartupStatus::Cancelled => cancelled.push(name.clone()),
McpStartupStatus::Starting => {}
}
}
failed.sort();
cancelled.sort();
self.finish_mcp_startup(failed, cancelled);
return;
}
if let Some(current) = &self.mcp_startup_status {
let total = current.len();
let mut starting: Vec<_> = current
Expand Down Expand Up @@ -2827,29 +2869,106 @@ impl ChatWidget {
self.request_redraw();
}

pub(crate) fn needs_mcp_startup_expected_servers(&self) -> bool {
self.mcp_startup_expected_servers.is_none()
}

pub(crate) fn set_mcp_startup_expected_servers<I>(&mut self, server_names: I)
where
I: IntoIterator<Item = String>,
{
self.mcp_startup_expected_servers = Some(server_names.into_iter().collect());
}

#[cfg(test)]
fn on_mcp_startup_complete(&mut self, ev: McpStartupCompleteEvent) {
let mut parts = Vec::new();
if !ev.failed.is_empty() {
let failed_servers: Vec<_> = ev.failed.iter().map(|f| f.server.clone()).collect();
parts.push(format!("failed: {}", failed_servers.join(", ")));
}
if !ev.cancelled.is_empty() {
fn on_mcp_startup_update(&mut self, ev: McpStartupUpdateEvent) {
self.update_mcp_startup_status(ev.server, ev.status, /*complete_when_settled*/ false);
}

fn finish_mcp_startup(&mut self, failed: Vec<String>, cancelled: Vec<String>) {
if !cancelled.is_empty() {
self.on_warning(format!(
"MCP startup interrupted. The following servers were not initialized: {}",
ev.cancelled.join(", ")
cancelled.join(", ")
));
}
let mut parts = Vec::new();
if !failed.is_empty() {
parts.push(format!("failed: {}", failed.join(", ")));
}
if !parts.is_empty() {
self.on_warning(format!("MCP startup incomplete ({})", parts.join("; ")));
}

self.mcp_startup_status = None;
self.mcp_startup_expected_servers = None;
self.mcp_startup_ignore_updates_until_next_start = true;
self.update_task_running_state();
self.maybe_send_next_queued_input();
self.request_redraw();
}

pub(crate) fn finish_mcp_startup_after_lag(&mut self) {
let Some(current) = &self.mcp_startup_status else {
return;
};

let mut failed = Vec::new();
let mut cancelled = Vec::new();

if let Some(expected_servers) = &self.mcp_startup_expected_servers {
for name in expected_servers {
match current.get(name) {
Some(McpStartupStatus::Ready) => {}
Some(McpStartupStatus::Failed { .. }) => failed.push(name.clone()),
Some(McpStartupStatus::Cancelled | McpStartupStatus::Starting) | None => {
cancelled.push(name.clone());
}
}
}
} else {
for (name, state) in current {
match state {
McpStartupStatus::Ready => {}
McpStartupStatus::Failed { .. } => failed.push(name.clone()),
McpStartupStatus::Cancelled | McpStartupStatus::Starting => {
cancelled.push(name.clone());
}
}
}
}

failed.sort();
failed.dedup();
cancelled.sort();
cancelled.dedup();
self.finish_mcp_startup(failed, cancelled);
}

#[cfg(test)]
fn on_mcp_startup_complete(&mut self, ev: McpStartupCompleteEvent) {
let failed = ev.failed.into_iter().map(|f| f.server).collect();
self.finish_mcp_startup(failed, ev.cancelled);
}

fn on_mcp_server_status_updated(&mut self, notification: McpServerStatusUpdatedNotification) {
let status = match notification.status {
McpServerStartupState::Starting => McpStartupStatus::Starting,
McpServerStartupState::Ready => McpStartupStatus::Ready,
McpServerStartupState::Failed => McpStartupStatus::Failed {
error: notification.error.unwrap_or_else(|| {
format!("MCP client for `{}` failed to start", notification.name)
}),
},
McpServerStartupState::Cancelled => McpStartupStatus::Cancelled,
};
self.update_mcp_startup_status(
notification.name,
status,
/*complete_when_settled*/ true,
);
}

/// Handle a turn aborted due to user interrupt (Esc).
/// When there are queued user messages, restore them into the composer
/// separated by newlines rather than auto‑submitting the next one.
Expand Down Expand Up @@ -4552,6 +4671,8 @@ impl ChatWidget {
agent_turn_running: false,
mcp_startup_status: None,
pending_turn_copyable_output: None,
mcp_startup_expected_servers: None,
mcp_startup_ignore_updates_until_next_start: false,
connectors_cache: ConnectorsCacheState::default(),
connectors_partial_snapshot: None,
connectors_prefetch_in_flight: false,
Expand Down Expand Up @@ -6318,6 +6439,9 @@ impl ChatWidget {
.map(|details| format!("{}: {details}", notification.summary))
.unwrap_or(notification.summary),
),
ServerNotification::McpServerStatusUpdated(notification) => {
self.on_mcp_server_status_updated(notification)
}
ServerNotification::ItemGuardianApprovalReviewStarted(notification) => {
self.on_guardian_review_notification(
notification.target_item_id,
Expand Down Expand Up @@ -6401,7 +6525,6 @@ impl ChatWidget {
| ServerNotification::RawResponseItemCompleted(_)
| ServerNotification::CommandExecOutputDelta(_)
| ServerNotification::McpToolCallProgress(_)
| ServerNotification::McpServerStatusUpdated(_)
| ServerNotification::McpServerOauthLoginCompleted(_)
| ServerNotification::AppListUpdated(_)
| ServerNotification::FsChanged(_)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: tui_app_server/src/chatwidget/tests.rs
assertion_line: 11571
expression: term.backend().vt100().screen().contents()
---



⚠ MCP client for `alpha` failed to start: handshake failed
⚠ MCP startup incomplete (failed: alpha)


› Ask Codex to do anything

? for shortcuts 100% context left
Loading
Loading