//! Thread MCP runtime projection and publication. //! //! This module owns the small correctness boundary between immutable session //! inputs and the mutable [`codex_mcp::McpRuntime`]. Background scheduling //! belongs elsewhere. use super::session::SessionConfiguration; use super::*; use crate::mcp::McpRuntimeProjection; use codex_config::McpServerDisabledReason; use codex_config::McpServerTransportConfig; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::ElicitationReviewerHandle; use codex_mcp::McpEnvironmentAuthority; use codex_mcp::McpServerRegistration; use codex_mcp::McpServerSource; use codex_mcp::McpStartupPolicy; use codex_mcp::PreparedMcpCall; use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_protocol::protocol::EnvironmentConfigState; use std::collections::HashSet; pub(super) struct McpDesiredState { pub(super) config: Arc, pub(super) auth: Option, pub(super) submit_id: String, pub(super) originator: String, pub(super) session_source: SessionSource, pub(super) environments: TurnEnvironmentSnapshot, pub(super) local_process_cwd: PathBuf, pub(super) disabled_plugin_ids: Vec, } impl Session { pub(super) fn mcp_inputs_differ( &self, current: &SessionConfiguration, next: &SessionConfiguration, updates: &SessionSettingsUpdate, ) -> bool { current.cwd() != next.cwd() || current.step_settings.approval_policy.value() != next.step_settings.approval_policy.value() || current.step_settings.approvals_reviewer != next.step_settings.approvals_reviewer || current.permission_profile() != next.permission_profile() || current.windows_sandbox_level != next.windows_sandbox_level || updates.environments.as_ref().is_some_and(|environments| { environments.environments != self.services.turn_environments.selections() }) } /// Waits on this session's refreshed server before tool execution is admitted. pub(crate) async fn wait_for_mcp_server(self: &Arc, server: &str) { self.refresh_mcp_if_dirty().await; self.services .mcp_runtime .wait_for_server_startup(server) .await; } /// Captures this session's current MCP client and catalog for one tool call. pub(crate) async fn prepare_mcp_call( self: &Arc, server: &str, tool: &str, ) -> Option { self.refresh_mcp_if_dirty().await; self.services .mcp_runtime .current_binding_for_call(server) .await? .prepare_call(server, tool) } pub(super) async fn latest_mcp_desired_state( &self, auth: Option, ) -> McpDesiredState { let (session_configuration, disabled_plugin_ids) = { let state = self.state.lock().await; ( state.session_configuration.clone(), state.active_disabled_plugin_ids.clone(), ) }; let environments = self.services.turn_environments.snapshot().await; let cwd = environments .primary() .and_then(|environment| environment.cwd().to_abs_path().ok()) .unwrap_or_else(|| session_configuration.cwd().clone()); let config = self.build_per_turn_config(&session_configuration, cwd); let local_process_cwd = environments .local_environment_cwd() .unwrap_or_else(|| session_configuration.cwd().clone()) .to_path_buf(); McpDesiredState { config: Arc::new(config), auth, submit_id: self.next_internal_sub_id(), originator: session_configuration.originator.clone(), session_source: session_configuration.session_source.clone(), environments, local_process_cwd, disabled_plugin_ids, } } pub(super) async fn install_initial_mcp_runtime( self: &Arc, session_configuration: &SessionConfiguration, auth: Option, mcp_projection: McpRuntimeProjection, resolved_environments: &TurnEnvironmentSnapshot, mcp_runtime_cwd: PathBuf, ) -> anyhow::Result<()> { let cwd = AbsolutePathBuf::from_absolute_path(mcp_runtime_cwd) .unwrap_or_else(|_| session_configuration.cwd().clone()); let config = self.build_per_turn_config(session_configuration, cwd); let local_process_cwd = resolved_environments .local_environment_cwd() .unwrap_or_else(|| session_configuration.cwd().clone()) .to_path_buf(); let desired = McpDesiredState { config: Arc::new(config), auth, submit_id: INITIAL_SUBMIT_ID.to_owned(), originator: session_configuration.originator.clone(), session_source: session_configuration.session_source.clone(), environments: resolved_environments.clone(), local_process_cwd, disabled_plugin_ids: session_configuration.disabled_plugin_ids.clone(), }; self.publish_mcp_runtime( &desired, mcp_projection, /*ready_selected_capability_roots*/ &[], Some(self.mcp_elicitation_reviewer()), ) .instrument(info_span!( "session_init.mcp_manager_init", otel.name = "session_init.mcp_manager_init", )) .await; self.services.mcp_runtime.validate_required_servers().await } /// Adds effective executor-owned configuration from this exact thread snapshot. pub(super) fn project_selected_environment_mcp_servers<'a>( &'a self, config: &'a Config, environments: &'a TurnEnvironmentSnapshot, mut projection: McpRuntimeProjection, ) -> BoxFuture<'a, McpRuntimeProjection> { Box::pin(async move { if self.isolation == codex_extension_api::SessionIsolation::Isolated { return projection; } let mut catalog = None; let mut registered = HashSet::new(); for selected in environments.turn_environments() { let environment = &selected.environment; if !environment.is_remote() { continue; } let environment_id = &selected.selection.environment_id; let discovery = environment .discover_http_mcp_servers(selected.cwd().clone()) .await; let outcome = if discovery.is_ok() { "success" } else { "error" }; // Count completed discovery attempts, including refreshes, before host policy // or MCP startup determines whether the server's tools become available. self.services.session_telemetry.counter( "codex.mcp.executor_discovery", /*inc*/ 1, &[("outcome", outcome)], ); let servers = match discovery { Ok(servers) => servers, Err(error) => { tracing::warn!( environment_id, %error, "failed to discover executor-local MCP servers" ); continue; } }; for (name, mut server) in servers { let outcome = if server.enabled { "found" } else { "unavailable" }; self.services.session_telemetry.counter( "codex.mcp.executor_discovery.server", /*inc*/ 1, &[("server_name", name.as_str()), ("outcome", outcome)], ); if name == CODEX_APPS_MCP_SERVER_NAME || !server.is_local_environment() || projection .config .mcp_server_catalog .server(&name) .is_some_and(|existing| { existing.config().environment_id != *environment_id || !matches!( existing.source(), McpServerSource::Plugin(_) | McpServerSource::SelectedPlugin(_) ) }) || registered.contains(&name) { continue; } let McpServerTransportConfig::StreamableHttp { env_http_headers, http_headers_helper, .. } = &server.transport else { continue; }; if http_headers_helper.is_some() || env_http_headers .as_ref() .is_some_and(|headers| !headers.is_empty()) { tracing::warn!( environment_id, server = name, "executor-local HTTP header helpers are not supported" ); continue; } server.environment_id = environment_id.clone(); if let Some(requirements) = config .config_layer_stack .requirements() .mcp_servers .as_ref() && !requirements .value .get(&name) .is_some_and(|requirement| server.matches_requirement(requirement)) { server.enabled = false; server.disabled_reason = Some(McpServerDisabledReason::Requirements { source: requirements.source.clone(), }); } registered.insert(name.clone()); catalog .get_or_insert_with(|| projection.config.mcp_server_catalog.to_builder()) .register(McpServerRegistration::from_config(name, server)); } } if let Some(catalog) = catalog { let selections = self.services.turn_environments.selections(); projection.config.mcp_server_catalog = catalog.build_with_environment_authority(|environment_id| { let Some(selection) = selections .iter() .find(|selection| selection.environment_id == environment_id) else { return if environment_id == codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID { McpEnvironmentAuthority::Unrestricted } else { McpEnvironmentAuthority::SelectedPluginsOnly }; }; match &selection.config { EnvironmentConfigState::FromThread => { McpEnvironmentAuthority::Unrestricted } EnvironmentConfigState::Pending | EnvironmentConfigState::Failed(_) => { McpEnvironmentAuthority::Unavailable } EnvironmentConfigState::Ready(config) => config .mcp_policy .as_ref() .map_or(McpEnvironmentAuthority::Unrestricted, |policy| { McpEnvironmentAuthority::Restricted(policy) }), } }); } projection }) } #[tracing::instrument(name = "mcp.runtime.refresh", skip_all)] pub(super) async fn publish_mcp_runtime( &self, desired: &McpDesiredState, mcp_projection: McpRuntimeProjection, ready_selected_capability_roots: &[SelectedCapabilityRoot], elicitation_reviewer: Option, ) { let mcp_projection = self .project_selected_environment_mcp_servers( &desired.config, &desired.environments, mcp_projection, ) .await; let selected_plugins = mcp_projection.selected_plugins.clone(); let input = self.build_mcp_runtime_input( desired, mcp_projection, ready_selected_capability_roots, elicitation_reviewer, ); self.services.mcp_runtime.replace(input).await; self.services.thread_extension_data.insert(selected_plugins); } pub(super) fn build_mcp_runtime_input( &self, desired: &McpDesiredState, mcp_projection: McpRuntimeProjection, ready_selected_capability_roots: &[SelectedCapabilityRoot], elicitation_reviewer: Option, ) -> McpRuntimeInput { let auth = desired.auth.clone(); let McpRuntimeProjection { mut config, plugins_available, selected_plugins: _, } = mcp_projection; config.approval_policy = desired.config.permissions.approval_policy.clone(); config.permission_profile = desired.config.permissions.effective_permission_profile(); config.approvals_reviewer = desired.config.approvals_reviewer; config.environment_cwds = desired .environments .turn_environments() .map(|environment| { ( environment.selection.environment_id.clone(), environment.cwd().clone(), ) }) .collect(); config .environment_cwds .entry(codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string()) .or_insert_with(|| PathUri::from_abs_path(&desired.config.cwd)); let mcp_servers = effective_mcp_servers(&config, auth.as_ref()); config.set_server_permission_profiles( &mcp_servers, desired.environments.turn_environments().map(|environment| { ( environment.selection.environment_id.clone(), environment.permission_profile_with_workspace_roots(), ) }), ); let mcp_config = Arc::new(config); let runtime_context = McpRuntimeContext::new( self.services.turn_environments.environment_manager(), desired.local_process_cwd.clone(), ) .with_selected_environments( desired .environments .turn_environments() .map(|environment| { ( environment.selection.environment_id.clone(), Arc::clone(&environment.environment), ) }) .collect(), ); McpRuntimeInput { startup_policy: if matches!(desired.session_source, SessionSource::SubAgent(_)) { McpStartupPolicy::LazyWhenCached } else { McpStartupPolicy::Eager }, config: mcp_config, plugins_available, ready_selected_capability_roots: ready_selected_capability_roots.to_vec(), mcp_servers, submit_id: desired.submit_id.clone(), tx_event: Some(self.get_tx_event()), startup_cancellation_token: CancellationToken::new(), runtime_context, codex_apps_tools_cache: self.services.mcp_manager.codex_apps_tools_cache(), tool_catalog_cache: self.services.mcp_manager.tool_catalog_cache(), codex_apps_tools_cache_key: connector_runtime_context_key(auth.as_ref()), client_mcp_extensions: self.services.client_mcp_extensions.for_mcp_servers(), auth, auth_manager: Some(Arc::clone(&self.services.auth_manager)), elicitation_reviewer, elicitation_lifecycle: Some(self.mcp_elicitation_lifecycle()), } } }