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

@@ -17,6 +17,7 @@ use crate::tools::filter_tools;
use crate::tools::qualify_tools;
use crate::tools::tool_with_model_visible_input_schema;
use codex_config::Constrained;
use codex_config::McpServerConfig;
use codex_protocol::ToolName;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::GranularApprovalConfig;
@@ -820,7 +821,7 @@ fn elicitation_capability_uses_2025_06_18_shape_for_all_servers() {
fn mcp_init_error_display_prompts_for_github_pat() {
let server_name = "github";
let entry = McpAuthStatusEntry {
config: McpServerConfig {
config: Some(McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
url: "https://api.githubcopilot.com/mcp/".to_string(),
bearer_token_env_var: None,
@@ -840,7 +841,7 @@ fn mcp_init_error_display_prompts_for_github_pat() {
scopes: None,
oauth_resource: None,
tools: HashMap::new(),
},
}),
auth_status: McpAuthStatus::Unsupported,
};
let err: StartupOutcomeError = anyhow::anyhow!("OAuth is unsupported").into();
@@ -872,7 +873,7 @@ fn mcp_init_error_display_prompts_for_login_when_auth_required() {
fn mcp_init_error_display_reports_generic_errors() {
let server_name = "custom";
let entry = McpAuthStatusEntry {
config: McpServerConfig {
config: Some(McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com".to_string(),
bearer_token_env_var: Some("TOKEN".to_string()),
@@ -892,7 +893,7 @@ fn mcp_init_error_display_reports_generic_errors() {
scopes: None,
oauth_resource: None,
tools: HashMap::new(),
},
}),
auth_status: McpAuthStatus::Unsupported,
};
let err: StartupOutcomeError = anyhow::anyhow!("boom").into();
@@ -916,31 +917,3 @@ fn mcp_init_error_display_includes_startup_timeout_hint() {
display
);
}
#[test]
fn transport_origin_extracts_http_origin() {
let transport = McpServerTransportConfig::StreamableHttp {
url: "https://example.com:8443/path?query=1".to_string(),
bearer_token_env_var: None,
http_headers: None,
env_http_headers: None,
};
assert_eq!(
transport_origin(&transport),
Some("https://example.com:8443".to_string())
);
}
#[test]
fn transport_origin_is_stdio_for_stdio_transport() {
let transport = McpServerTransportConfig::Stdio {
command: "server".to_string(),
args: Vec::new(),
env: None,
env_vars: Vec::new(),
cwd: None,
};
assert_eq!(transport_origin(&transport), Some("stdio".to_string()));
}