diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 54283cfcaf..49b07adcd0 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -234,7 +234,7 @@ fn sample_thread_start_response( model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), - mcp_server_names: Vec::new(), + mcp_server_names: None, runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnRequest, diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index 2c322c2bd8..cb95665b63 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -309,7 +309,7 @@ fn sample_thread_start_response() -> ClientResponsePayload { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), - mcp_server_names: Vec::new(), + mcp_server_names: None, runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnRequest, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 1b46445022..0dc90a266b 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -19532,12 +19532,15 @@ "type": "array" }, "mcpServerNames": { - "default": [], + "default": null, "description": "Names of the effective MCP servers in the thread's initial runtime.", "items": { "type": "string" }, - "type": "array" + "type": [ + "array", + "null" + ] }, "model": { "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 7c3017abc1..0239e63bdf 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -17311,12 +17311,15 @@ "type": "array" }, "mcpServerNames": { - "default": [], + "default": null, "description": "Names of the effective MCP servers in the thread's initial runtime.", "items": { "type": "string" }, - "type": "array" + "type": [ + "array", + "null" + ] }, "model": { "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index b75556f47e..319d665607 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -2431,12 +2431,15 @@ "type": "array" }, "mcpServerNames": { - "default": [], + "default": null, "description": "Names of the effective MCP servers in the thread's initial runtime.", "items": { "type": "string" }, - "type": "array" + "type": [ + "array", + "null" + ] }, "model": { "type": "string" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index e934aeac49..b27f7502f8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -12,7 +12,7 @@ import type { Thread } from "./Thread"; export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /** * Names of the effective MCP servers in the thread's initial runtime. */ -mcpServerNames: Array, /** +mcpServerNames: Array | null, /** * Environment-native paths to instruction source files currently loaded for this thread. */ instructionSources: Array, approvalPolicy: AskForApproval, /** diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 60f3e8d03e..7d53b19e9f 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2608,7 +2608,7 @@ mod tests { model_provider: "openai".to_string(), service_tier: None, cwd, - mcp_server_names: vec!["example".to_string()], + mcp_server_names: Some(vec!["example".to_string()]), runtime_workspace_roots: Vec::new(), instruction_sources: vec![ codex_utils_path_uri::LegacyAppPathString::from_abs_path(&absolute_path( diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 77d8a1439b..4ed2a0dac0 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3978,6 +3978,7 @@ fn thread_lifecycle_responses_default_missing_optional_fields() { serde_json::from_value(response.clone()).expect("thread/fork response"); assert_eq!(start.instruction_sources, Vec::::new()); + assert_eq!(start.mcp_server_names, None); assert_eq!(start.thread.parent_thread_id, None); assert_eq!(start.thread.recency_at, None); assert_eq!( diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index cd751461ee..44669829ba 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -175,7 +175,7 @@ pub struct ThreadStartResponse { pub cwd: AbsolutePathBuf, /// Names of the effective MCP servers in the thread's initial runtime. #[serde(default)] - pub mcp_server_names: Vec, + pub mcp_server_names: Option>, /// Thread-scoped runtime workspace roots used to materialize /// `:workspace_roots`. #[experimental("thread/start.runtimeWorkspaceRoots")] diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index be987b17b9..d91f040eba 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -1290,7 +1290,7 @@ impl ThreadRequestProcessor { model_provider: config_snapshot.model_provider_id, service_tier: config_snapshot.service_tier, cwd, - mcp_server_names, + mcp_server_names: Some(mcp_server_names), runtime_workspace_roots: config_snapshot.workspace_roots, instruction_sources, approval_policy: config_snapshot.approval_policy.into(), diff --git a/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs b/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs index 668e379313..36a15e6cce 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs @@ -81,7 +81,7 @@ url = "{mcp_server_url}/mcp" let thread_start_response: ThreadStartResponse = to_response(thread_start_response)?; assert_eq!( thread_start_response.mcp_server_names, - vec!["some-server".to_string()] + Some(vec!["some-server".to_string()]) ); let request_id = mcp diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 93124c41eb..3c86aa6f39 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -782,7 +782,7 @@ fn sample_thread_start_response() -> ThreadStartResponse { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), - mcp_server_names: Vec::new(), + mcp_server_names: None, runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: codex_app_server_protocol::AskForApproval::OnRequest, diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index e4f951a3d8..aa37a05a9a 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -517,6 +517,29 @@ impl App { turns, mcp_server_names, }) => { + let mcp_server_names = match mcp_server_names { + Some(mcp_server_names) => mcp_server_names, + None if self.chat_widget.startup_draft_needs_mcp_server_names() => { + match super::background_requests::fetch_all_mcp_server_statuses( + app_server.request_handle(), + McpServerStatusDetail::ToolsAndAuthOnly, + Some(session.thread_id), + ) + .await + { + Ok(statuses) => { + statuses.into_iter().map(|status| status.name).collect() + } + Err(err) => { + tracing::warn!( + "failed to load startup MCP server names from an older app server: {err}" + ); + Vec::new() + } + } + } + None => Vec::new(), + }; self.chat_widget .set_startup_draft_expected_mcp_servers(mcp_server_names); self.enqueue_primary_thread_session(session, turns).await?; diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index d5c4fae98e..2e7b14e048 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3529,7 +3529,7 @@ async fn primary_thread_ignores_child_mcp_startup_notifications() { AppServerStartedThread { session: test_thread_session(child_thread_id, test_path_buf("/tmp/child")), turns: Vec::new(), - mcp_server_names: Vec::new(), + mcp_server_names: None, }, &mut child_snapshot, ) @@ -5496,7 +5496,7 @@ async fn refreshed_snapshot_session_persists_resumed_turns() { AppServerStartedThread { session: resumed_session.clone(), turns: resumed_turns.clone(), - mcp_server_names: Vec::new(), + mcp_server_names: None, }, &mut snapshot, ) diff --git a/codex-rs/tui/src/app/tests/startup.rs b/codex-rs/tui/src/app/tests/startup.rs index e00115f287..45fcf63308 100644 --- a/codex-rs/tui/src/app/tests/startup.rs +++ b/codex-rs/tui/src/app/tests/startup.rs @@ -165,7 +165,7 @@ async fn restored_startup_draft_waits_for_effective_mcp_settlement() { Ok(AppServerStartedThread { session: test_thread_session(thread_id, test_path_buf("/tmp/project")), turns: Vec::new(), - mcp_server_names: vec!["test".to_string()], + mcp_server_names: Some(vec!["test".to_string()]), }), ) .await @@ -230,6 +230,44 @@ async fn restored_startup_draft_waits_for_effective_mcp_settlement() { assert_eq!(app.chat_widget.composer_text_with_pending(), ""); } +#[tokio::test] +async fn restored_startup_draft_loads_mcp_names_from_older_app_servers() { + let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await; + app.pending_startup_thread_start = true; + app.chat_widget + .set_queue_submissions_until_session_configured(/*queue*/ true); + app.chat_widget + .restore_startup_draft("captured during startup"); + + let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker( + app.chat_widget.config_ref(), + )) + .await + .expect("embedded app server"); + let mut started = app_server + .start_thread(app.chat_widget.config_ref()) + .await + .expect("startup thread"); + started.mcp_server_names = None; + + app.handle_startup_thread_started(&mut app_server, Ok(started)) + .await + .expect("older app-server startup should attach"); + app.chat_widget + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + match next_user_turn_op(&mut op_rx) { + Op::UserTurn { items, .. } => assert_eq!( + items, + vec![UserInput::Text { + text: "captured during startup".to_string(), + text_elements: Vec::new(), + }] + ), + other => panic!("expected restored startup input submission, got {other:?}"), + } +} + #[tokio::test] async fn startup_without_restored_draft_uses_normal_queued_submission() { let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await; @@ -306,7 +344,7 @@ fn stale_startup_thread_started_removes_local_routing_state() -> Result<()> { Ok(AppServerStartedThread { session: test_thread_session(stale_thread_id, test_path_buf("/tmp/project")), turns: Vec::new(), - mcp_server_names: Vec::new(), + mcp_server_names: None, }), ) .await?; diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index d3261b3baf..5d7efa627a 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -203,7 +203,7 @@ impl ThreadParamsMode { pub(crate) struct AppServerStartedThread { pub(crate) session: ThreadSessionState, pub(crate) turns: Vec, - pub(crate) mcp_server_names: Vec, + pub(crate) mcp_server_names: Option>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1548,7 +1548,7 @@ async fn started_thread_from_resume_response( Ok(AppServerStartedThread { session, turns: response.thread.turns, - mcp_server_names: Vec::new(), + mcp_server_names: None, }) } @@ -1564,7 +1564,7 @@ async fn started_thread_from_fork_response( Ok(AppServerStartedThread { session, turns: response.thread.turns, - mcp_server_names: Vec::new(), + mcp_server_names: None, }) } diff --git a/codex-rs/tui/src/chatwidget/input_flow.rs b/codex-rs/tui/src/chatwidget/input_flow.rs index 85d79c02c3..1e385b3d0e 100644 --- a/codex-rs/tui/src/chatwidget/input_flow.rs +++ b/codex-rs/tui/src/chatwidget/input_flow.rs @@ -25,6 +25,10 @@ impl ChatWidget { self.finish_startup_draft_protection_if_ready(); } + pub(crate) fn startup_draft_needs_mcp_server_names(&self) -> bool { + self.startup_draft_pending_mcp_servers.is_some() + } + pub(super) fn finish_startup_draft_protection_if_ready(&mut self) { let startup_ready = self.is_session_configured() && self