//! 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_file_system::WindowsSandboxSelection; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::ElicitationReviewerHandle; use codex_mcp::McpServerRegistration; use codex_mcp::McpServerSource; use codex_mcp::McpStartupPolicy; use codex_mcp::PreparedMcpCall; use codex_mcp::ToolInfo; use codex_otel::auth_storage::AuthStorageOriginator; use codex_protocol::capabilities::SelectedCapabilityRoot; 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, ) -> bool { current.cwd() != next.cwd() || current.runtime_workspace_roots != next.runtime_workspace_roots || 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 } /// 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, advertised_tool: &ToolInfo, ) -> Option { self.refresh_mcp_if_dirty().await; self.services .mcp_runtime .prepare_call(advertised_tool) .await } pub(super) async fn latest_mcp_desired_state( &self, auth: Option, environments: TurnEnvironmentSnapshot, ) -> McpDesiredState { let (session_configuration, disabled_plugin_ids) = { let state = self.state.lock().await; ( state.session_configuration.clone(), state.active_disabled_plugin_ids.clone(), ) }; let config = self.build_per_turn_config(&session_configuration, environments.local_selection()); let local_process_cwd = config.cwd.to_path_buf(); McpDesiredState { config: Arc::new(config), auth, submit_id: self.next_internal_sub_id(), originator: session_configuration.originator, session_source: session_configuration.session_source, 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, ) -> anyhow::Result<()> { let config = self.build_per_turn_config( session_configuration, resolved_environments.local_selection(), ); let local_process_cwd = config.cwd.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 || self .services .mcp_thread_init .get::() .is_some() { 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_executor_config(name, server)); } } if let Some(catalog) = catalog { let selections = environments.all_selections(); let environment_scope = McpEnvironmentScope::Selected(&selections); projection.config.mcp_server_catalog = catalog.build_with_environment_authority(|environment_id| { environment_scope.authority_for(environment_id) }); } 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, ); AuthStorageOriginator::from_client_name(&desired.originator) .scope(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)); config.environment_use_mxc = desired .environments .turn_environments() .map(|environment| { ( environment.selection.environment_id.clone(), environment.windows_sandbox_selection_for_turn_metadata() == WindowsSandboxSelection::Mxc, ) }) .collect(); config .environment_use_mxc .entry(codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string()) .or_insert( cfg!(windows) && desired.config.effective_local_windows_sandbox_type() == SandboxType::WindowsMxc, ); 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 = self.mcp_runtime_context(&desired.environments, &desired.local_process_cwd); McpRuntimeInput { network_policy_metadata: None, 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()), } } }