diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index a8575d8c74..b9c94326c5 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -269,6 +269,38 @@ pub fn effective_mcp_servers( effective_mcp_servers_from_configured(configured_mcp_servers(config), config, auth) } +fn is_trusted_chatgpt_mcp_server( + transport: &McpServerTransportConfig, + chatgpt_base_url: &str, +) -> bool { + let McpServerTransportConfig::StreamableHttp { url, .. } = transport else { + return false; + }; + let Ok(server_url) = url::Url::parse(url) else { + return false; + }; + if !matches!(server_url.scheme(), "http" | "https") { + return false; + } + + if url::Url::parse(CHATGPT_CODEX_BASE_URL) + .ok() + .is_some_and(|chatgpt_url| server_url.origin() == chatgpt_url.origin()) + { + return true; + } + + url::Url::parse(chatgpt_base_url) + .ok() + .is_some_and(|staging_url| { + staging_url.scheme() == "https" + && staging_url.domain().is_some_and(|host| { + host == "chatgpt-staging.com" || host.ends_with(".chatgpt-staging.com") + }) + && server_url.origin() == staging_url.origin() + }) +} + /// Converts a materialized server map to its auth-gated runtime view. /// /// Compatibility built-ins and extension overlays must already be reflected in @@ -278,24 +310,12 @@ pub fn effective_mcp_servers_from_configured( config: &McpConfig, auth: Option<&CodexAuth>, ) -> HashMap { - let chatgpt_origin = url::Url::parse(CHATGPT_CODEX_BASE_URL) - .ok() - .map(|url| url.origin()); let mut servers = configured_servers .into_iter() .map(|(name, mut server)| { match server.auth.clone() { McpServerAuth::ChatGpt => { - let server_origin = match &server.transport { - McpServerTransportConfig::StreamableHttp { url, .. } => { - url::Url::parse(url) - .ok() - .filter(|url| matches!(url.scheme(), "http" | "https")) - .map(|url| url.origin()) - } - McpServerTransportConfig::Stdio { .. } => None, - }; - if server_origin.as_ref() != chatgpt_origin.as_ref() { + if !is_trusted_chatgpt_mcp_server(&server.transport, &config.chatgpt_base_url) { server.auth = McpServerAuth::OAuth; } } diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index ef44d83507..f27319de20 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -362,6 +362,25 @@ fn codex_apps_server_config_forwards_originator_and_configured_product_sku_heade } } +#[test] +fn effective_mcp_servers_preserve_chatgpt_auth_for_staging() { + for url in [ + "https://chatgpt-staging.com", + "https://preview.chatgpt-staging.com", + ] { + let mut config = test_mcp_config(PathBuf::new()); + config.chatgpt_base_url = url.to_string(); + let server = codex_apps_mcp_server_config( + url, /*apps_mcp_product_sku*/ None, /*originator*/ None, + ); + let configured = HashMap::from([("staging".to_string(), server)]); + let effective = + effective_mcp_servers_from_configured(configured, &config, /*auth*/ None); + + assert_eq!(effective["staging"].config().auth, McpServerAuth::ChatGpt); + } +} + #[tokio::test] async fn effective_mcp_servers_preserve_runtime_servers() { let codex_home = tempfile::tempdir().expect("tempdir");