From 980f60b6641c5907c16db3c39f36ac113e15c93d Mon Sep 17 00:00:00 2001 From: e-provencher Date: Wed, 10 Jun 2026 16:11:20 -0400 Subject: [PATCH] [codex] Preserve disabled MCP servers across runtime overlays (#27414) ## Why Recent MCP runtime overlay changes replace same-name configured server entries with compatibility or extension-provided configs. Those replacement configs default to enabled, so an MCP server explicitly configured with `enabled = false` could be initialized anyway. The connection manager still filters disabled servers correctly, but the configured disabled state was lost before initialization reached that filter. ## What changed - Remember MCP servers that are disabled in the configured view before applying runtime fallbacks and extension overlays. - Restore `enabled = false` for those servers after overlays, while leaving all other overlay fields and `Remove` precedence unchanged. - Add focused extension-backed regression coverage for a disabled `codex_apps` server. ## Testing - `just fmt` - `just test -p codex-mcp-extension` - `just fix -p codex-core` - `just fix -p codex-mcp-extension` The full workspace `just test` suite was not run. --- codex-rs/core/src/mcp.rs | 11 +++++++++ codex-rs/ext/mcp/tests/hosted_apps_mcp.rs | 28 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index 71571deae..abd28ec61 100644 --- a/codex-rs/core/src/mcp.rs +++ b/codex-rs/core/src/mcp.rs @@ -45,6 +45,12 @@ impl McpManager { /// runtime-only extension overlays. pub async fn runtime_config(&self, config: &Config) -> McpConfig { let mut mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await; + let disabled_server_names = mcp_config + .configured_mcp_servers + .iter() + .filter(|(_, server)| !server.enabled) + .map(|(name, _)| name.clone()) + .collect::>(); if mcp_config.apps_enabled { mcp_config.configured_mcp_servers.insert( CODEX_APPS_MCP_SERVER_NAME.to_string(), @@ -60,6 +66,11 @@ impl McpManager { } let contributions = self.contributions(config).await; Self::apply_to_configured_servers(&contributions, &mut mcp_config.configured_mcp_servers); + for name in disabled_server_names { + if let Some(server) = mcp_config.configured_mcp_servers.get_mut(&name) { + server.enabled = false; + } + } mcp_config } diff --git a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs index bb401578f..ea1b330ca 100644 --- a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs +++ b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs @@ -42,6 +42,34 @@ async fn contributes_hosted_plugin_runtime_without_an_executor() -> TestResult { Ok(()) } +#[tokio::test] +async fn runtime_overlay_preserves_disabled_server() -> TestResult { + 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()), + ( + "mcp_servers.codex_apps.url".to_string(), + "https://example.com/mcp".into(), + ), + ("mcp_servers.codex_apps.enabled".to_string(), false.into()), + ]) + .build() + .await?; + let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); + let manager = installed_manager(&config); + + let servers = manager.effective_servers(&config, Some(&auth)).await; + let server = servers + .get(CODEX_APPS_MCP_SERVER_NAME) + .ok_or("hosted plugin runtime should remain configured")?; + + assert!(!server.enabled()); + Ok(()) +} + #[tokio::test] async fn legacy_fallback_overwrites_reserved_config_without_an_extension() -> TestResult { let codex_home = tempfile::tempdir()?;