diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index ea55d21379..a06020b2cc 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -109,7 +109,6 @@ pub struct McpConnectionManager { server_metadata: HashMap, required_servers: Vec, tool_plugin_provenance: Arc, - host_owned_codex_apps_enabled: bool, prefix_mcp_tool_names: bool, elicitation_requests: ElicitationRequestManager, startup_cancellation_token: CancellationToken, @@ -130,7 +129,6 @@ impl McpConnectionManager { runtime_context: McpRuntimeContext, codex_home: PathBuf, codex_apps_tools_cache_key: CodexAppsToolsCacheKey, - host_owned_codex_apps_enabled: bool, prefix_mcp_tool_names: bool, client_elicitation_capability: ElicitationCapability, supports_openai_form_elicitation: bool, @@ -252,7 +250,6 @@ impl McpConnectionManager { server_metadata, required_servers, tool_plugin_provenance, - host_owned_codex_apps_enabled, prefix_mcp_tool_names, elicitation_requests: elicitation_requests.clone(), startup_cancellation_token: startup_cancellation_token.clone(), @@ -338,7 +335,6 @@ impl McpConnectionManager { server_metadata: HashMap::new(), required_servers: Vec::new(), tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()), - host_owned_codex_apps_enabled: false, prefix_mcp_tool_names, elicitation_requests: ElicitationRequestManager::new( approval_policy.value(), @@ -405,8 +401,10 @@ impl McpConnectionManager { .unwrap_or_default() } - pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool { - self.host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME + pub fn server_supports_connector_auth_repair(&self, server_name: &str) -> bool { + self.server_metadata + .get(server_name) + .is_some_and(McpServerMetadata::supports_connector_auth_repair) } pub fn set_approval_policy(&self, approval_policy: &Constrained) { diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 52194b07f1..c0f7755e61 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -1147,6 +1147,7 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() { "https://docs.example".to_string(), )), supports_parallel_tool_calls: true, + supports_connector_auth_repair: false, default_tools_approval_mode: None, tool_approval_modes: HashMap::new(), }, @@ -1276,7 +1277,6 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() { chatgpt_user_id: None, is_workspace_account: false, }, - /*host_owned_codex_apps_enabled*/ false, /*prefix_mcp_tool_names*/ true, ElicitationCapability::default(), /*supports_openai_form_elicitation*/ false, diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index b56a4f9072..04698977ba 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -41,7 +41,6 @@ pub use mcp::codex_apps_mcp_server_config; pub use mcp::configured_mcp_servers; pub use mcp::effective_mcp_servers; pub use mcp::effective_mcp_servers_from_configured; -pub use mcp::host_owned_codex_apps_enabled; pub use mcp::hosted_plugin_runtime_mcp_server_config; pub use mcp::tool_plugin_provenance; pub use plugin_config::PluginMcpConfigParseOutcome; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 4076880c1d..d79570bbf3 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -231,10 +231,6 @@ impl ToolPluginProvenance { } } -pub fn host_owned_codex_apps_enabled(config: &McpConfig, auth: Option<&CodexAuth>) -> bool { - config.apps_enabled && auth.is_some_and(CodexAuth::uses_codex_backend) -} - pub fn configured_mcp_servers(config: &McpConfig) -> HashMap { config.mcp_server_catalog.configured_servers() } @@ -255,14 +251,24 @@ pub fn effective_mcp_servers_from_configured( config: &McpConfig, auth: Option<&CodexAuth>, ) -> HashMap { - let mut servers = configured_servers + let codex_apps_available = + config.apps_enabled && auth.is_some_and(CodexAuth::uses_codex_backend); + configured_servers .into_iter() - .map(|(name, server)| (name, EffectiveMcpServer::configured(server))) - .collect::>(); - if !host_owned_codex_apps_enabled(config, auth) { - servers.remove(CODEX_APPS_MCP_SERVER_NAME); - } - servers + .filter_map(|(name, server)| { + if name == CODEX_APPS_MCP_SERVER_NAME && !codex_apps_available { + return None; + } + + let server = EffectiveMcpServer::configured(server); + let server = if name == CODEX_APPS_MCP_SERVER_NAME { + server.with_connector_auth_repair() + } else { + server + }; + Some((name, server)) + }) + .collect() } pub fn tool_plugin_provenance(config: &McpConfig) -> ToolPluginProvenance { @@ -277,7 +283,6 @@ pub async fn read_mcp_resource( uri: &str, ) -> anyhow::Result { let mut mcp_servers = effective_mcp_servers(config, auth); - let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(config, auth); mcp_servers.retain(|name, _| name == server); let auth_statuses = compute_auth_statuses( mcp_servers.iter(), @@ -302,7 +307,6 @@ pub async fn read_mcp_resource( runtime_context, config.codex_home.clone(), codex_apps_tools_cache_key(auth), - host_owned_codex_apps_enabled, config.prefix_mcp_tool_names, config.client_elicitation_capability.clone(), /*supports_openai_form_elicitation*/ false, @@ -337,7 +341,6 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( detail: McpSnapshotDetail, ) -> McpServerStatusSnapshot { let mcp_servers = effective_mcp_servers(config, auth); - let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(config, auth); let tool_plugin_provenance = tool_plugin_provenance(config); if mcp_servers.is_empty() { return McpServerStatusSnapshot { @@ -377,7 +380,6 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( runtime_context, config.codex_home.clone(), codex_apps_tools_cache_key(auth), - host_owned_codex_apps_enabled, config.prefix_mcp_tool_names, config.client_elicitation_capability.clone(), /*supports_openai_form_elicitation*/ false, diff --git a/codex-rs/codex-mcp/src/server.rs b/codex-rs/codex-mcp/src/server.rs index 39287b2136..6b8a7fba25 100644 --- a/codex-rs/codex-mcp/src/server.rs +++ b/codex-rs/codex-mcp/src/server.rs @@ -14,15 +14,22 @@ pub(crate) enum McpServerLaunch { #[derive(Debug, Clone)] pub struct EffectiveMcpServer { launch: McpServerLaunch, + connector_auth_repair: bool, } impl EffectiveMcpServer { pub fn configured(config: McpServerConfig) -> Self { Self { launch: McpServerLaunch::Configured(Box::new(config)), + connector_auth_repair: false, } } + pub(crate) fn with_connector_auth_repair(mut self) -> Self { + self.connector_auth_repair = true; + self + } + pub(crate) fn launch(&self) -> &McpServerLaunch { &self.launch } @@ -44,6 +51,10 @@ impl EffectiveMcpServer { McpServerLaunch::Configured(config) => config.required, } } + + fn supports_connector_auth_repair(&self) -> bool { + self.connector_auth_repair + } } /// Transport origin retained for metrics and diagnostics after server launch. @@ -79,11 +90,16 @@ pub(crate) struct McpServerMetadata { pub pollutes_memory: bool, pub origin: Option, pub supports_parallel_tool_calls: bool, + pub supports_connector_auth_repair: bool, pub default_tools_approval_mode: Option, pub tool_approval_modes: HashMap, } impl McpServerMetadata { + pub fn supports_connector_auth_repair(&self) -> bool { + self.supports_connector_auth_repair + } + pub fn tool_approval_mode(&self, tool_name: &str) -> AppToolApproval { self.tool_approval_modes .get(tool_name) @@ -101,6 +117,7 @@ impl From<&EffectiveMcpServer> for McpServerMetadata { pollutes_memory: true, origin: McpServerOrigin::from_transport(&config.transport), supports_parallel_tool_calls: config.supports_parallel_tool_calls, + supports_connector_auth_repair: server.supports_connector_auth_repair(), default_tools_approval_mode: config.default_tools_approval_mode, tool_approval_modes: config .tools diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 86e5f07f9a..87c3097162 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -40,7 +40,6 @@ use codex_mcp::ToolPluginProvenance; use codex_mcp::codex_apps_tools_cache_key; use codex_mcp::compute_auth_statuses; use codex_mcp::effective_mcp_servers; -use codex_mcp::host_owned_codex_apps_enabled; use codex_mcp::tool_plugin_provenance; const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30); @@ -240,7 +239,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager( let mut mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref()); mcp_servers.retain(|name, _| name == CODEX_APPS_MCP_SERVER_NAME); - let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(&mcp_config, auth.as_ref()); if mcp_servers.is_empty() { return Ok(AccessibleConnectorsStatus { connectors: Vec::new(), @@ -275,7 +273,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager( McpRuntimeContext::new(environment_manager, config.cwd.to_path_buf()), config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), - host_owned_codex_apps_enabled, mcp_config.prefix_mcp_tool_names, mcp_config.client_elicitation_capability, /*supports_openai_form_elicitation*/ false, diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 56d6e678a0..db795d5328 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -635,7 +635,7 @@ async fn maybe_request_codex_apps_auth_elicitation( .services .mcp_connection_manager .load_full() - .is_host_owned_codex_apps_server(server) + .server_supports_connector_auth_repair(server) { return result; } diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index eb500b0f6c..5b0ccacd7d 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -8,6 +8,7 @@ use crate::state::ActiveTurn; use crate::test_support::models_manager_with_provider; use crate::tools::hook_names::HookToolName; use crate::turn_metadata::McpTurnMetadataContext; +use async_channel::unbounded; use codex_config::CONFIG_TOML_FILE; use codex_config::config_toml::ConfigToml; use codex_config::types::AppConfig; @@ -20,6 +21,9 @@ use codex_config::types::McpServerToolConfig; use codex_features::Features; use codex_hooks::Hooks; use codex_hooks::HooksConfig; +use codex_login::CodexAuth; +use codex_mcp::codex_apps_mcp_server_config; +use codex_mcp::effective_mcp_servers_from_configured; use codex_model_provider::create_model_provider; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; @@ -1333,17 +1337,37 @@ fn codex_apps_auth_failure_metadata() -> McpToolApprovalMetadata { ) } -async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: &TurnContext) { - let auth = session.services.auth_manager.auth().await; +async fn install_connector_auth_repair_manager(session: &Session, turn_context: &TurnContext) { + let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); + let mut mcp_config = turn_context + .config + .to_mcp_config(session.services.plugins_manager.as_ref()) + .await; + mcp_config.apps_enabled = true; + let mcp_servers = effective_mcp_servers_from_configured( + HashMap::from([( + CODEX_APPS_MCP_SERVER_NAME.to_string(), + codex_apps_mcp_server_config( + &turn_context.config.chatgpt_base_url, + /*apps_mcp_product_sku*/ None, + ), + )]), + &mcp_config, + Some(&auth), + ); + let (tx_event, rx_event) = unbounded(); + drop(rx_event); + let startup_cancellation_token = CancellationToken::new(); + startup_cancellation_token.cancel(); let manager = codex_mcp::McpConnectionManager::new( - &HashMap::new(), + &mcp_servers, turn_context.config.mcp_oauth_credentials_store_mode, turn_context.config.auth_keyring_backend_kind(), HashMap::new(), &turn_context.approval_policy, turn_context.sub_id.clone(), - session.get_tx_event(), - CancellationToken::new(), + tx_event, + startup_cancellation_token, turn_context.permission_profile(), codex_mcp::McpRuntimeContext::new( session.services.turn_environments.environment_manager(), @@ -1353,13 +1377,12 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: }, ), turn_context.config.codex_home.to_path_buf(), - codex_mcp::codex_apps_tools_cache_key(auth.as_ref()), - /*host_owned_codex_apps_enabled*/ true, + codex_mcp::codex_apps_tools_cache_key(Some(&auth)), turn_context.config.prefix_mcp_tool_names(), rmcp::model::ElicitationCapability::default(), /*supports_openai_form_elicitation*/ false, codex_mcp::ToolPluginProvenance::default(), - auth.as_ref(), + Some(&auth), /*elicitation_reviewer*/ None, ) .await; @@ -1372,7 +1395,7 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: #[tokio::test] async fn codex_apps_auth_elicitation_feature_disabled_returns_original_result() { let (session, turn_context, rx_event) = make_session_and_context_with_rx().await; - install_host_owned_codex_apps_manager(&session, &turn_context).await; + install_connector_auth_repair_manager(&session, &turn_context).await; let result = codex_apps_auth_failure_result(); let metadata = codex_apps_auth_failure_metadata(); @@ -1391,7 +1414,7 @@ async fn codex_apps_auth_elicitation_feature_disabled_returns_original_result() } #[tokio::test] -async fn codex_apps_auth_elicitation_non_host_owned_server_returns_original_result() { +async fn codex_apps_auth_elicitation_server_without_repair_capability_returns_original_result() { let (session, mut turn_context, rx_event) = make_session_and_context_with_rx().await; let mut features = Features::with_defaults(); features.enable(Feature::AuthElicitation); @@ -1417,7 +1440,7 @@ async fn codex_apps_auth_elicitation_non_host_owned_server_returns_original_resu #[tokio::test] async fn codex_apps_auth_elicitation_disallowed_by_policy_returns_original_result() { let (session, mut turn_context, rx_event) = make_session_and_context_with_rx().await; - install_host_owned_codex_apps_manager(&session, &turn_context).await; + install_connector_auth_repair_manager(&session, &turn_context).await; let mut features = Features::with_defaults(); features.enable(Feature::AuthElicitation); let turn_context = Arc::get_mut(&mut turn_context).expect("single turn context ref"); @@ -1446,7 +1469,7 @@ async fn codex_apps_auth_elicitation_disallowed_by_policy_returns_original_resul #[tokio::test] async fn codex_apps_auth_elicitation_granular_mcp_disabled_returns_original_result() { let (session, mut turn_context, rx_event) = make_session_and_context_with_rx().await; - install_host_owned_codex_apps_manager(&session, &turn_context).await; + install_connector_auth_repair_manager(&session, &turn_context).await; let mut features = Features::with_defaults(); features.enable(Feature::AuthElicitation); let turn_context = Arc::get_mut(&mut turn_context).expect("single turn context ref"); @@ -1481,7 +1504,7 @@ async fn codex_apps_auth_elicitation_granular_mcp_disabled_returns_original_resu #[tokio::test] async fn codex_apps_auth_elicitation_feature_enabled_requests_elicitation() { let (session, mut turn_context, rx_event) = make_session_and_context_with_rx().await; - install_host_owned_codex_apps_manager(&session, &turn_context).await; + install_connector_auth_repair_manager(&session, &turn_context).await; *session.active_turn.lock().await = Some(ActiveTurn::default()); let mut features = Features::with_defaults(); features.enable(Feature::AuthElicitation); diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index d03576aadc..42c274fdb4 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -317,8 +317,6 @@ impl Session { let tool_plugin_provenance = codex_mcp::tool_plugin_provenance(&mcp_config); let mcp_servers = effective_mcp_servers_from_configured(mcp_servers, &mcp_config, auth.as_ref()); - let host_owned_codex_apps_enabled = - host_owned_codex_apps_enabled(&mcp_config, auth.as_ref()); let auth_statuses = compute_auth_statuses( mcp_servers.iter(), store_mode, @@ -359,7 +357,6 @@ impl Session { mcp_runtime_context, config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), - host_owned_codex_apps_enabled, mcp_config.prefix_mcp_tool_names, mcp_config.client_elicitation_capability, self.services diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 1bd5bae8bf..09181c8e5e 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -343,7 +343,6 @@ use codex_git_utils::get_git_repo_root; use codex_mcp::McpConfig; use codex_mcp::compute_auth_statuses; use codex_mcp::effective_mcp_servers_from_configured; -use codex_mcp::host_owned_codex_apps_enabled; use codex_otel::SessionTelemetry; use codex_otel::THREAD_STARTED_METRIC; use codex_otel::TelemetryAuthMode; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index b4740bea99..69f1959cce 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1138,9 +1138,6 @@ impl Session { sess.send_event_raw(event).await; } - let host_owned_codex_apps_enabled = config - .features - .apps_enabled_for_auth(auth.as_ref().is_some_and(|auth| auth.uses_codex_backend())); let client_elicitation_capability = if config.features.enabled(Feature::AuthElicitation) { ElicitationCapability { form: Some(FormElicitationCapability::default()), @@ -1183,7 +1180,6 @@ impl Session { mcp_runtime_context, config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth), - host_owned_codex_apps_enabled, config.prefix_mcp_tool_names(), client_elicitation_capability, sess.services