feat: make built-in MCPs first-class runtime servers (#21356)

## DISCLAIMER
This is experimental and no production service must rely on this

## Why

Built-in MCPs are product-owned runtime capabilities, but they were
previously flattened into the same config-backed stdio path as
user-configured servers. That made them depend on a hidden `codex
builtin-mcp` re-exec path, exposed them through config-oriented CLI
flows, and erased distinctions the runtime needs to preserve—most
notably whether an MCP call should count as external context for
memory-mode pollution.

## What changed

- Model product-owned built-ins separately from config-backed MCP
servers via `BuiltinMcpServer` and `EffectiveMcpServer`.
- Launch built-ins in process through a reusable async transport instead
of the hidden `builtin-mcp` stdio subcommand.
- Keep config-oriented CLI operations such as `codex mcp
list/get/login/logout` scoped to configured servers, while merging
built-ins only into the effective runtime server set.
- Retain server metadata after launch so parallel-tool support and
context classification come from the live server set; built-in
`memories` is now classified as local Codex state rather than external
context.

## Test plan

- `cargo test -p codex-mcp`
- `cargo test -p codex-core --test suite
builtin_memories_mcp_call_does_not_mark_thread_memory_mode_polluted_when_configured`

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
jif-oaiandCodex authored and GitHub committed 2026-05-07 10:36:32 +02:00
1 parent 40e282849c
commit b2268999fe
35 files changed
+668 -345

No files matched your search

+22 -50
View File
@@ -3360,7 +3360,6 @@ async fn to_mcp_config_empty_mcp_requirements_preserve_builtin_mcps() -> anyhow:
}))
.build()
.await?;
config.codex_self_exe = Some(PathBuf::from("/tmp/codex"));
let _ = config.features.enable(Feature::BuiltInMcp);
let _ = config.features.enable(Feature::MemoryTool);
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
@@ -3368,11 +3367,8 @@ async fn to_mcp_config_empty_mcp_requirements_preserve_builtin_mcps() -> anyhow:
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert_eq!(
mcp_config
.configured_mcp_servers
.get(codex_mcp::MEMORIES_MCP_SERVER_NAME)
.map(|server| (server.enabled, server.disabled_reason.clone())),
Some((true, None))
mcp_config.builtin_mcp_servers,
vec![codex_mcp::BuiltinMcpServer::Memories]
);
Ok(())
@@ -3399,7 +3395,6 @@ async fn to_mcp_config_nonempty_mcp_requirements_preserve_builtin_mcps() -> anyh
}))
.build()
.await?;
config.codex_self_exe = Some(PathBuf::from("/tmp/codex"));
let _ = config.features.enable(Feature::BuiltInMcp);
let _ = config.features.enable(Feature::MemoryTool);
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
@@ -3407,11 +3402,8 @@ async fn to_mcp_config_nonempty_mcp_requirements_preserve_builtin_mcps() -> anyh
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert_eq!(
mcp_config
.configured_mcp_servers
.get(codex_mcp::MEMORIES_MCP_SERVER_NAME)
.map(|server| (server.enabled, server.disabled_reason.clone())),
Some((true, None))
mcp_config.builtin_mcp_servers,
vec![codex_mcp::BuiltinMcpServer::Memories]
);
Ok(())
@@ -4244,10 +4236,7 @@ async fn to_mcp_config_includes_enabled_builtin_mcps() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let mut config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides {
codex_self_exe: Some(PathBuf::from("/tmp/codex")),
..ConfigOverrides::default()
},
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
@@ -4258,22 +4247,13 @@ async fn to_mcp_config_includes_enabled_builtin_mcps() -> std::io::Result<()> {
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert_eq!(
mcp_config
mcp_config.builtin_mcp_servers,
vec![codex_mcp::BuiltinMcpServer::Memories]
);
assert!(
!mcp_config
.configured_mcp_servers
.get(codex_mcp::MEMORIES_MCP_SERVER_NAME)
.map(|server| &server.transport),
Some(&McpServerTransportConfig::Stdio {
command: "/tmp/codex".to_string(),
args: vec![
"builtin-mcp".to_string(),
"memories".to_string(),
"--codex-home".to_string(),
codex_home.path().display().to_string(),
],
env: None,
env_vars: Vec::new(),
cwd: None,
})
.contains_key(codex_mcp::MEMORIES_MCP_SERVER_NAME)
);
Ok(())
@@ -4284,10 +4264,7 @@ async fn to_mcp_config_omits_builtin_mcps_when_feature_is_disabled() -> std::io:
let codex_home = TempDir::new()?;
let mut config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides {
codex_self_exe: Some(PathBuf::from("/tmp/codex")),
..ConfigOverrides::default()
},
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
@@ -4296,11 +4273,7 @@ async fn to_mcp_config_omits_builtin_mcps_when_feature_is_disabled() -> std::io:
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert!(
!mcp_config
.configured_mcp_servers
.contains_key(codex_mcp::MEMORIES_MCP_SERVER_NAME)
);
assert!(mcp_config.builtin_mcp_servers.is_empty());
Ok(())
}
@@ -4316,10 +4289,7 @@ async fn to_mcp_config_reserves_enabled_builtin_mcp_names() -> std::io::Result<(
)]),
..ConfigToml::default()
},
ConfigOverrides {
codex_self_exe: Some(PathBuf::from("/tmp/codex")),
..ConfigOverrides::default()
},
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
@@ -4329,13 +4299,15 @@ async fn to_mcp_config_reserves_enabled_builtin_mcp_names() -> std::io::Result<(
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert!(matches!(
mcp_config
assert_eq!(
mcp_config.builtin_mcp_servers,
vec![codex_mcp::BuiltinMcpServer::Memories]
);
assert!(
!mcp_config
.configured_mcp_servers
.get(codex_mcp::MEMORIES_MCP_SERVER_NAME)
.map(|server| &server.transport),
Some(McpServerTransportConfig::Stdio { .. })
));
.contains_key(codex_mcp::MEMORIES_MCP_SERVER_NAME)
);
Ok(())
}
+6 -5
View File
@@ -72,7 +72,7 @@ use codex_git_utils::resolve_root_git_project_for_trust;
use codex_login::AuthManagerConfig;
use codex_mcp::BuiltinMcpServerOptions;
use codex_mcp::McpConfig;
use codex_mcp::configured_builtin_mcp_servers;
use codex_mcp::enabled_builtin_mcp_servers;
use codex_memories_read::memory_root;
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
use codex_model_provider_info::ModelProviderInfo;
@@ -1091,9 +1091,7 @@ impl Config {
) -> McpConfig {
let plugins_input = self.plugins_config_input();
let loaded_plugins = plugins_manager.plugins_for_config(&plugins_input).await;
let builtin_mcp_servers = configured_builtin_mcp_servers(BuiltinMcpServerOptions {
codex_self_exe: self.codex_self_exe.as_deref(),
codex_home: self.codex_home.as_path(),
let builtin_mcp_servers = enabled_builtin_mcp_servers(BuiltinMcpServerOptions {
memories_enabled: self.features.enabled(Feature::BuiltInMcp)
&& self.features.enabled(Feature::MemoryTool)
&& self.memories.use_memories,
@@ -1122,7 +1120,9 @@ impl Config {
// allowlists.
filter_mcp_servers_by_requirements(&mut configured_mcp_servers, Some(mcp_requirements));
}
configured_mcp_servers.extend(builtin_mcp_servers);
for builtin_server in &builtin_mcp_servers {
configured_mcp_servers.remove(builtin_server.name());
}
McpConfig {
chatgpt_base_url: self.chatgpt_base_url.clone(),
@@ -1139,6 +1139,7 @@ impl Config {
use_legacy_landlock: self.features.use_legacy_landlock(),
apps_enabled: self.features.enabled(Feature::Apps),
configured_mcp_servers,
builtin_mcp_servers,
plugin_capability_summaries: loaded_plugins.capability_summaries().to_vec(),
}
}