From bf6865b072f438c6060386ad18afefbb6cc148b3 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Thu, 28 May 2026 15:44:59 -0300 Subject: [PATCH] fix(core): preserve explicit mcp readiness after refresh --- codex-rs/core/src/session/turn.rs | 23 ++++++--- codex-rs/core/tests/suite/plugins.rs | 73 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index b562e23826..46ee5cc436 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -505,6 +505,8 @@ async fn build_skills_and_plugins( { explicitly_requested_mcp_servers.insert(CODEX_APPS_MCP_SERVER_NAME.to_string()); } + let explicit_mcp_servers_to_rewait_after_skill_install = + explicitly_requested_mcp_servers.clone(); wait_for_explicit_mcp_servers( sess, turn_context, @@ -553,7 +555,7 @@ async fn build_skills_and_plugins( &skills_outcome.disabled_paths, &connector_slug_counts, ); - let skill_dependency_servers = maybe_prompt_and_install_mcp_dependencies( + let mut skill_dependency_servers = maybe_prompt_and_install_mcp_dependencies( sess, turn_context, cancellation_token, @@ -561,13 +563,18 @@ async fn build_skills_and_plugins( Some(sess.mcp_elicitation_reviewer()), ) .await; - wait_for_explicit_mcp_servers( - sess, - turn_context, - cancellation_token, - skill_dependency_servers, - ) - .await?; + if !skill_dependency_servers.is_empty() { + // Installing a dependency refreshes the manager and restarts any app or + // plugin server already awaited for this same explicit invocation. + skill_dependency_servers.extend(explicit_mcp_servers_to_rewait_after_skill_install); + wait_for_explicit_mcp_servers( + sess, + turn_context, + cancellation_token, + skill_dependency_servers, + ) + .await?; + } let SkillInjections { items: skill_injections, diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 3182992cb9..5ccca71396 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -8,6 +8,8 @@ use std::time::Instant; use anyhow::Result; use codex_features::Feature; use codex_login::CodexAuth; +use codex_protocol::models::PermissionProfile; +use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use core_test_support::apps_test_server::AppsTestServer; @@ -436,6 +438,77 @@ async fn explicitly_selected_skill_waits_for_pending_apps_startup() -> Result<() Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn selected_skill_rewaits_for_app_after_installing_mcp_dependency() -> Result<()> { + skip_if_no_network!(Ok(())); + let server = start_mock_server().await; + let apps_server = AppsTestServer::mount_with_connector_name_and_tools_list_delay( + &server, + "Google Calendar", + Some(Duration::from_secs(/*secs*/ 2)), + ) + .await?; + let mock = mount_sse_once( + &server, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + + let codex_home = Arc::new(TempDir::new()?); + let skill_path = write_plugin_skill_plugin(codex_home.as_ref()); + std::fs::write( + &skill_path, + "---\ndescription: inspect sample data\n---\n\nUse [$calendar](app://calendar).\n", + ) + .expect("write plugin app skill"); + let skill_agents_dir = skill_path.parent().expect("skill dir").join("agents"); + std::fs::create_dir_all(&skill_agents_dir).expect("create skill agents dir"); + let dependency_command = stdio_server_bin()?; + std::fs::write( + skill_agents_dir.join("openai.yaml"), + format!( + "dependencies:\n tools:\n - type: \"mcp\"\n value: \"dependency\"\n transport: \"stdio\"\n command: \"{dependency_command}\"\n" + ), + ) + .expect("write plugin skill dependencies"); + let codex = + build_apps_enabled_plugin_test_codex(&server, codex_home, apps_server.chatgpt_base_url) + .await?; + + codex + .submit(Op::UserInput { + environments: None, + items: vec![codex_protocol::user_input::UserInput::Skill { + name: "sample:sample-search".into(), + path: skill_path, + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { + approval_policy: Some(AskForApproval::Never), + permission_profile: Some(PermissionProfile::Disabled), + ..Default::default() + }, + }) + .await?; + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + let request = mock.single_request(); + assert!( + tool_names(&request.body_json()) + .iter() + .any(|name| name == "mcp__codex_apps__google_calendar"), + "expected app referenced by selected skill after dependency installation refresh" + ); + assert!( + request.tool_by_name("mcp__dependency", "echo").is_some(), + "expected newly installed MCP dependency on the first turn" + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn explicit_plugin_mentions_track_plugin_used_analytics() -> Result<()> { skip_if_no_network!(Ok(()));