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-oai
2026-05-07 10:36:32 +02:00
committed by GitHub
co-authored by Codex
parent 40e282849c
commit b2268999fe
35 changed files with 668 additions and 345 deletions
+3 -21
View File
@@ -4,7 +4,6 @@ mod response_adapter;
mod wait_handler;
pub(crate) mod wait_spec;
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
@@ -274,26 +273,9 @@ pub(super) async fn build_enabled_tools(
)]
async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
let nested_tools_config = exec.turn.tools_config.for_code_mode_nested_tools();
let listed_mcp_tools = exec
.session
.services
.mcp_connection_manager
.read()
.await
.list_all_tools()
.await;
let parallel_mcp_server_names = exec
.turn
.config
.mcp_servers
.get()
.iter()
.filter_map(|(server_name, server_config)| {
server_config
.supports_parallel_tool_calls
.then_some(server_name.clone())
})
.collect::<HashSet<_>>();
let mcp_connection_manager = exec.session.services.mcp_connection_manager.read().await;
let listed_mcp_tools = mcp_connection_manager.list_all_tools().await;
let parallel_mcp_server_names = mcp_connection_manager.parallel_tool_call_server_names();
ToolRouter::from_config(
&nested_tools_config,