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

+53 -33
View File
@@ -7,6 +7,7 @@
//! `codex-core`.
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -29,6 +30,8 @@ use crate::rmcp_client::StartupOutcomeError;
use crate::rmcp_client::list_tools_for_client_uncached;
use crate::runtime::McpRuntimeEnvironment;
use crate::runtime::emit_duration;
use crate::server::EffectiveMcpServer;
use crate::server::McpServerMetadata;
use crate::tools::ToolInfo;
use crate::tools::filter_tools;
use crate::tools::qualify_tools;
@@ -38,7 +41,6 @@ use anyhow::Result;
use anyhow::anyhow;
use async_channel::Sender;
use codex_config::Constrained;
use codex_config::McpServerConfig;
use codex_config::McpServerTransportConfig;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_login::CodexAuth;
@@ -65,12 +67,11 @@ use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use tracing::warn;
use url::Url;
/// A thin wrapper around a set of running [`RmcpClient`] instances.
pub struct McpConnectionManager {
clients: HashMap<String, AsyncManagedClient>,
server_origins: HashMap<String, String>,
server_metadata: HashMap<String, McpServerMetadata>,
host_owned_codex_apps_enabled: bool,
elicitation_requests: ElicitationRequestManager,
startup_cancellation_token: CancellationToken,
@@ -83,7 +84,7 @@ impl McpConnectionManager {
) -> Self {
Self {
clients: HashMap::new(),
server_origins: HashMap::new(),
server_metadata: HashMap::new(),
host_owned_codex_apps_enabled: false,
elicitation_requests: ElicitationRequestManager::new(
approval_policy.value(),
@@ -103,7 +104,7 @@ impl McpConnectionManager {
pub fn begin_shutdown(&mut self) -> impl std::future::Future<Output = ()> + Send + 'static {
self.startup_cancellation_token.cancel();
let clients = std::mem::take(&mut self.clients);
self.server_origins.clear();
self.server_metadata.clear();
async move {
for client in clients.into_values() {
client.shutdown().await;
@@ -117,7 +118,27 @@ impl McpConnectionManager {
}
pub fn server_origin(&self, server_name: &str) -> Option<&str> {
self.server_origins.get(server_name).map(String::as_str)
self.server_metadata
.get(server_name)
.and_then(|metadata| metadata.origin.as_ref())
.map(super::server::McpServerOrigin::as_str)
}
pub fn server_pollutes_memory(&self, server_name: &str) -> bool {
self.server_metadata
.get(server_name)
.is_none_or(|metadata| metadata.pollutes_memory)
}
pub fn parallel_tool_call_server_names(&self) -> HashSet<String> {
self.server_metadata
.iter()
.filter_map(|(name, metadata)| {
metadata
.supports_parallel_tool_calls
.then_some(name.clone())
})
.collect()
}
pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool {
@@ -146,7 +167,7 @@ impl McpConnectionManager {
#[allow(clippy::new_ret_no_self, clippy::too_many_arguments)]
pub async fn new(
mcp_servers: &HashMap<String, McpServerConfig>,
mcp_servers: &HashMap<String, EffectiveMcpServer>,
store_mode: OAuthCredentialsStoreMode,
auth_entries: HashMap<String, McpAuthStatusEntry>,
approval_policy: &Constrained<AskForApproval>,
@@ -163,7 +184,7 @@ impl McpConnectionManager {
) -> (Self, CancellationToken) {
let cancel_token = CancellationToken::new();
let mut clients = HashMap::new();
let mut server_origins = HashMap::new();
let mut server_metadata = HashMap::new();
let mut join_set = JoinSet::new();
let elicitation_requests = ElicitationRequestManager::new(
approval_policy.value(),
@@ -176,10 +197,11 @@ impl McpConnectionManager {
.filter(|auth| auth.uses_codex_backend())
.map(codex_model_provider::auth_provider_from_auth);
let mcp_servers = mcp_servers.clone();
for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) {
if let Some(origin) = transport_origin(&cfg.transport) {
server_origins.insert(server_name.clone(), origin);
}
for (server_name, server) in mcp_servers
.into_iter()
.filter(|(_, server)| server.enabled())
{
server_metadata.insert(server_name.clone(), McpServerMetadata::from(&server));
let cancel_token = cancel_token.child_token();
let _ = emit_update(
startup_submit_id.as_str(),
@@ -198,13 +220,16 @@ impl McpConnectionManager {
} else {
None
};
let uses_env_bearer_token = match &cfg.transport {
McpServerTransportConfig::StreamableHttp {
bearer_token_env_var,
..
} => bearer_token_env_var.is_some(),
McpServerTransportConfig::Stdio { .. } => false,
};
let uses_env_bearer_token =
server
.configured_config()
.is_some_and(|config| match &config.transport {
McpServerTransportConfig::StreamableHttp {
bearer_token_env_var,
..
} => bearer_token_env_var.is_some(),
McpServerTransportConfig::Stdio { .. } => false,
});
let runtime_auth_provider =
if server_name == CODEX_APPS_MCP_SERVER_NAME && !uses_env_bearer_token {
codex_apps_auth_provider.clone()
@@ -213,7 +238,7 @@ impl McpConnectionManager {
};
let async_managed_client = AsyncManagedClient::new(
server_name.clone(),
cfg,
server,
store_mode,
cancel_token.clone(),
tx_event.clone(),
@@ -221,6 +246,7 @@ impl McpConnectionManager {
codex_apps_tools_cache_context,
Arc::clone(&tool_plugin_provenance),
runtime_environment.clone(),
codex_home.clone(),
runtime_auth_provider,
);
clients.insert(server_name.clone(), async_managed_client.clone());
@@ -260,7 +286,7 @@ impl McpConnectionManager {
}
let manager = Self {
clients,
server_origins,
server_metadata,
host_owned_codex_apps_enabled,
elicitation_requests: elicitation_requests.clone(),
startup_cancellation_token: cancel_token.clone(),
@@ -672,16 +698,6 @@ async fn emit_update(
.await
}
fn transport_origin(transport: &McpServerTransportConfig) -> Option<String> {
match transport {
McpServerTransportConfig::StreamableHttp { url, .. } => {
let parsed = Url::parse(url).ok()?;
Some(parsed.origin().ascii_serialization())
}
McpServerTransportConfig::Stdio { .. } => Some("stdio".to_string()),
}
}
fn mcp_init_error_display(
server_name: &str,
entry: Option<&McpAuthStatusEntry>,
@@ -692,7 +708,7 @@ fn mcp_init_error_display(
bearer_token_env_var,
http_headers,
..
}) = &entry.map(|entry| &entry.config.transport)
}) = entry.and_then(|entry| entry.config.as_ref().map(|config| &config.transport))
&& url == "https://api.githubcopilot.com/mcp/"
&& bearer_token_env_var.is_none()
&& http_headers.as_ref().map(HashMap::is_empty).unwrap_or(true)
@@ -706,7 +722,11 @@ fn mcp_init_error_display(
)
} else if is_mcp_client_startup_timeout_error(err) {
let startup_timeout_secs = match entry {
Some(entry) => match entry.config.startup_timeout_sec {
Some(entry) => match entry
.config
.as_ref()
.and_then(|config| config.startup_timeout_sec)
{
Some(timeout) => timeout,
None => DEFAULT_STARTUP_TIMEOUT,
},