use std::sync::Arc; use codex_config::McpServerTransportConfig; use codex_core::McpManager; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core_plugins::PluginsManager; use codex_extension_api::ExtensionRegistryBuilder; use codex_login::CodexAuth; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use pretty_assertions::assert_eq; #[tokio::test] async fn contributes_hosted_apps_mcp_without_an_executor() -> Result<(), Box> { let codex_home = tempfile::tempdir()?; let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .cli_overrides(vec![ ("features.apps".to_string(), true.into()), ("features.apps_mcp_path_override".to_string(), true.into()), ("chatgpt_base_url".to_string(), "https://chatgpt.com".into()), ]) .build() .await?; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let manager = installed_manager(&config); let runtime_config = manager.runtime_config(&config).await; assert!(!runtime_config.legacy_apps_mcp_loader_enabled); let servers = manager.effective_servers(&config, Some(&auth)).await; let server = servers .get(CODEX_APPS_MCP_SERVER_NAME) .and_then(|server| server.configured_config()) .ok_or("Apps MCP should be contributed as a configured server")?; let McpServerTransportConfig::StreamableHttp { url, .. } = &server.transport else { panic!("Apps MCP should use streamable HTTP"); }; assert_eq!(url, "https://chatgpt.com/backend-api/ps/mcp"); Ok(()) } #[tokio::test] async fn hosted_apps_mcp_requires_chatgpt_auth() -> Result<(), Box> { let codex_home = tempfile::tempdir()?; let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .cli_overrides(vec![("features.apps".to_string(), true.into())]) .build() .await?; let auth = CodexAuth::from_api_key("test"); let manager = installed_manager(&config); let servers = manager.effective_servers(&config, Some(&auth)).await; assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME)); Ok(()) } #[tokio::test] async fn disabled_apps_remove_reserved_server_config() -> Result<(), Box> { let codex_home = tempfile::tempdir()?; let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .cli_overrides(vec![ ("features.apps".to_string(), false.into()), ( "mcp_servers.codex_apps.url".to_string(), "https://example.com/mcp".into(), ), ]) .build() .await?; let manager = installed_manager(&config); let servers = manager.runtime_servers(&config).await; assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME)); Ok(()) } fn installed_manager(config: &Config) -> McpManager { let mut builder = ExtensionRegistryBuilder::new(); codex_mcp_extension::install(&mut builder); McpManager::new_with_extensions( Arc::new(PluginsManager::new(config.codex_home.to_path_buf())), Arc::new(builder.build()), ) }