diff --git a/codex-rs/codex-mcp/src/runtime.rs b/codex-rs/codex-mcp/src/runtime.rs index c96fbc4311..ff58c7cb94 100644 --- a/codex-rs/codex-mcp/src/runtime.rs +++ b/codex-rs/codex-mcp/src/runtime.rs @@ -141,13 +141,24 @@ impl McpRuntime { /// Reconciles configured servers and publishes their immutable runtime snapshot. pub async fn replace(&self, input: McpRuntimeInput) { let current = self.current.load_full(); + self.publish(input, Some(current.connections.as_ref())) + .await; + } + + /// Starts fresh connections and returns their complete, refreshed Apps catalog. + pub async fn replace_fresh(&self, input: McpRuntimeInput) -> anyhow::Result> { + self.publish(input, /*previous*/ None).await; + self.latest_hard_refresh_codex_apps_tools_cache().await + } + + async fn publish(&self, input: McpRuntimeInput, previous: Option<&McpConnectionSet>) { let (publish, publication_gate) = McpPublicationGate::pending(); let config = Arc::clone(&input.config); let plugins_available = input.plugins_available; let ready_selected_capability_roots = input.ready_selected_capability_roots.clone(); let connections = Arc::new( McpConnectionSet::new( - Some(current.connections.as_ref()), + previous, publication_gate, input, self.elicitation_router.clone(), diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 1c556bd0b6..76f83306c0 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -3,6 +3,7 @@ use codex_exec_server::ExecutorCapabilityDiscoveryCache; use codex_exec_server::ExecutorCapabilityDiscoverySnapshot; use codex_exec_server::MAX_SELECTED_CAPABILITY_ROOTS; use codex_exec_server::ResolvedSelectedCapabilityRoot; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::ElicitationReviewRequest; use codex_mcp::ElicitationReviewer; use codex_mcp::ElicitationReviewerHandle; @@ -202,7 +203,7 @@ impl Session { } } - /// Refreshes the future Apps catalog without making an exact step a refresh owner. + /// Reconnects the runtime so refreshed Apps tools belong to their new exact client. pub(crate) async fn hard_refresh_latest_codex_apps_tools( self: &Arc, ) -> anyhow::Result> { @@ -212,10 +213,43 @@ impl Session { .acquire() .await .map_err(|_| anyhow::anyhow!("MCP runtime refresh semaphore closed"))?; - self.services - .mcp_runtime - .latest_hard_refresh_codex_apps_tools_cache() - .await + let desired = self.latest_mcp_desired_state().await; + let selected_capability_roots = self + .resolve_selected_capability_roots_for_step(&desired.environments) + .await; + let ready_selected_capability_roots = + Self::ready_selected_capability_roots(&selected_capability_roots); + let executor_capability_discovery = self + .executor_capability_discovery_for_step( + &desired.config, + &ready_selected_capability_roots, + ) + .await; + let mcp_projection = self + .services + .mcp_manager + .runtime_config_for_step( + &desired.config, + &self.services.mcp_thread_init, + &self.services.thread_extension_data, + &desired.originator, + &ready_selected_capability_roots, + executor_capability_discovery.as_deref(), + ) + .await; + let input = self + .build_mcp_runtime_input( + &desired, + mcp_projection, + &ready_selected_capability_roots, + Some(self.mcp_elicitation_reviewer()), + ) + .await; + anyhow::ensure!( + input.mcp_servers.contains_key(CODEX_APPS_MCP_SERVER_NAME), + "unknown MCP server '{CODEX_APPS_MCP_SERVER_NAME}'" + ); + self.services.mcp_runtime.replace_fresh(input).await } pub(super) fn mark_mcp_runtime_dirty(&self) { diff --git a/codex-rs/core/src/session/mcp_runtime.rs b/codex-rs/core/src/session/mcp_runtime.rs index 03f57463ba..f8c4c08199 100644 --- a/codex-rs/core/src/session/mcp_runtime.rs +++ b/codex-rs/core/src/session/mcp_runtime.rs @@ -100,7 +100,7 @@ impl Session { self.services.mcp_runtime.replace(input).await; } - async fn build_mcp_runtime_input( + pub(super) async fn build_mcp_runtime_input( &self, desired: &McpDesiredState, mcp_projection: McpRuntimeProjection, diff --git a/codex-rs/core/tests/suite/request_plugin_install.rs b/codex-rs/core/tests/suite/request_plugin_install.rs index c8ca9308f6..1592577107 100644 --- a/codex-rs/core/tests/suite/request_plugin_install.rs +++ b/codex-rs/core/tests/suite/request_plugin_install.rs @@ -592,6 +592,11 @@ async fn run_remote_plugin_install_refresh_case(refreshed_tools: RefreshedAppsTo ev_assistant_message("msg-1", "done"), ev_completed("resp-2"), ]), + sse(vec![ + ev_response_created("resp-3"), + ev_assistant_message("msg-2", "catalog still current"), + ev_completed("resp-3"), + ]), ], ) .await; @@ -644,6 +649,19 @@ async fn run_remote_plugin_install_refresh_case(refreshed_tools: RefreshedAppsTo .any(|name| name == REQUEST_PLUGIN_INSTALL_TOOL_NAME), "the refreshed installed-plugin cache should filter the cached recommendation" ); + drop(requests); + test.codex.refresh_runtime_config(test.config.clone()).await; + test.submit_turn("check whether Calendar is still installed") + .await?; + let requests = mock.requests(); + assert_eq!(requests.len(), 3); + assert_eq!( + requests[2] + .tool_by_name(CALENDAR_NAMESPACE, CALENDAR_CREATE_EVENT_TOOL) + .is_some(), + completed, + "an unrelated runtime publication must retain the refreshed Apps catalog" + ); Ok(()) }