diff --git a/codex-rs/ext/mcp/src/lib.rs b/codex-rs/ext/mcp/src/lib.rs index 9c22bad5a0..114af22972 100644 --- a/codex-rs/ext/mcp/src/lib.rs +++ b/codex-rs/ext/mcp/src/lib.rs @@ -5,10 +5,33 @@ use codex_extension_api::McpServerContribution; use codex_extension_api::McpServerContributionContext; use codex_extension_api::McpServerContributor; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::codex_apps_mcp_server_config; use codex_mcp::hosted_plugin_runtime_mcp_server_config; mod executor_plugin; +const CODEX_APPS_MCP_BASE_URL_ENV_VAR: &str = "CODEX_APPS_MCP_BASE_URL"; + +#[derive(Debug, Eq, PartialEq)] +enum AppsMcpServerTarget<'a> { + HostedPluginRuntime(&'a str), + CodexApps(&'a str), +} + +fn apps_mcp_server_target<'a>( + chatgpt_base_url: &'a str, + apps_mcp_base_url_override: Option<&'a str>, +) -> AppsMcpServerTarget<'a> { + if let Some(apps_mcp_base_url) = apps_mcp_base_url_override + .map(str::trim) + .filter(|url| !url.is_empty()) + { + return AppsMcpServerTarget::CodexApps(apps_mcp_base_url); + } + + AppsMcpServerTarget::HostedPluginRuntime(chatgpt_base_url) +} + struct HostedPluginRuntimeExtension; impl McpServerContributor for HostedPluginRuntimeExtension { @@ -27,17 +50,32 @@ impl McpServerContributor for HostedPluginRuntimeExtension { return vec![McpServerContribution::Remove { name }]; } + let apps_mcp_base_url_override = std::env::var(CODEX_APPS_MCP_BASE_URL_ENV_VAR).ok(); + let apps_mcp_product_sku = config.apps_mcp_product_sku.as_deref(); + let server_config = match apps_mcp_server_target( + &config.chatgpt_base_url, + apps_mcp_base_url_override.as_deref(), + ) { + AppsMcpServerTarget::HostedPluginRuntime(base_url) => { + hosted_plugin_runtime_mcp_server_config(base_url, apps_mcp_product_sku) + } + AppsMcpServerTarget::CodexApps(base_url) => { + codex_apps_mcp_server_config(base_url, apps_mcp_product_sku) + } + }; + vec![McpServerContribution::Set { name, - config: Box::new(hosted_plugin_runtime_mcp_server_config( - &config.chatgpt_base_url, - config.apps_mcp_product_sku.as_deref(), - )), + config: Box::new(server_config), }] }) } } +#[cfg(test)] +#[path = "lib_tests.rs"] +mod tests; + pub fn install(builder: &mut ExtensionRegistryBuilder) { builder.mcp_server_contributor(std::sync::Arc::new(HostedPluginRuntimeExtension)); } diff --git a/codex-rs/ext/mcp/src/lib_tests.rs b/codex-rs/ext/mcp/src/lib_tests.rs new file mode 100644 index 0000000000..38eeb0bb58 --- /dev/null +++ b/codex-rs/ext/mcp/src/lib_tests.rs @@ -0,0 +1,31 @@ +use pretty_assertions::assert_eq; + +use super::AppsMcpServerTarget; +use super::apps_mcp_server_target; + +#[test] +fn default_uses_hosted_plugin_runtime() { + assert_eq!( + apps_mcp_server_target( + "https://chatgpt.com", + /*apps_mcp_base_url_override*/ None, + ), + AppsMcpServerTarget::HostedPluginRuntime("https://chatgpt.com"), + ); +} + +#[test] +fn empty_override_uses_hosted_plugin_runtime() { + assert_eq!( + apps_mcp_server_target("https://chatgpt.com", Some(" ")), + AppsMcpServerTarget::HostedPluginRuntime("https://chatgpt.com"), + ); +} + +#[test] +fn explicit_override_uses_local_codex_apps_endpoint() { + assert_eq!( + apps_mcp_server_target("https://chatgpt.com", Some("http://127.0.0.1:8061"),), + AppsMcpServerTarget::CodexApps("http://127.0.0.1:8061"), + ); +} diff --git a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs index 3836234b84..9b57c46970 100644 --- a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs +++ b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs @@ -11,12 +11,14 @@ use codex_extension_api::McpServerContributionContext; use codex_extension_api::McpServerContributor; use codex_login::CodexAuth; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::codex_apps_mcp_server_config; +use codex_mcp::hosted_plugin_runtime_mcp_server_config; use pretty_assertions::assert_eq; type TestResult = Result<(), Box>; #[tokio::test] -async fn contributes_hosted_plugin_runtime_without_an_executor() -> TestResult { +async fn contributes_configured_apps_mcp_without_an_executor() -> TestResult { let codex_home = tempfile::tempdir()?; let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) @@ -34,11 +36,31 @@ async fn contributes_hosted_plugin_runtime_without_an_executor() -> TestResult { let server = servers .get(CODEX_APPS_MCP_SERVER_NAME) .and_then(|server| server.configured_config()) - .ok_or("hosted plugin runtime should be contributed as a configured server")?; + .ok_or("Apps MCP should be contributed as a configured server")?; let McpServerTransportConfig::StreamableHttp { url, .. } = &server.transport else { - panic!("hosted plugin runtime should use streamable HTTP"); + panic!("Apps MCP should use streamable HTTP"); }; - assert_eq!(url, "https://chatgpt.com/backend-api/ps/mcp"); + let apps_mcp_base_url_override = std::env::var("CODEX_APPS_MCP_BASE_URL").ok(); + let expected_config = match apps_mcp_base_url_override + .as_deref() + .map(str::trim) + .filter(|url| !url.is_empty()) + { + Some(base_url) => { + codex_apps_mcp_server_config(base_url, /*apps_mcp_product_sku*/ None) + } + None => hosted_plugin_runtime_mcp_server_config( + "https://chatgpt.com", + /*apps_mcp_product_sku*/ None, + ), + }; + let McpServerTransportConfig::StreamableHttp { + url: expected_url, .. + } = expected_config.transport + else { + panic!("expected Apps MCP config should use streamable HTTP"); + }; + assert_eq!(url, &expected_url); Ok(()) }