diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b0d9cfe527..6359089371 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2126,6 +2126,17 @@ dependencies = [ "serde_with", ] +[[package]] +name = "codex-builtin-mcps" +version = "0.0.0" +dependencies = [ + "anyhow", + "codex-config", + "codex-memories-mcp", + "codex-utils-absolute-path", + "pretty_assertions", +] + [[package]] name = "codex-bwrap" version = "0.0.0" @@ -2171,6 +2182,7 @@ dependencies = [ "codex-app-server-protocol", "codex-app-server-test-client", "codex-arg0", + "codex-builtin-mcps", "codex-chatgpt", "codex-cloud-tasks", "codex-config", @@ -2998,6 +3010,7 @@ dependencies = [ "async-channel", "codex-api", "codex-async-utils", + "codex-builtin-mcps", "codex-config", "codex-exec-server", "codex-login", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index a58655ed20..ba20cfb906 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -5,6 +5,7 @@ members = [ "agent-graph-store", "agent-identity", "backend-client", + "builtin-mcps", "bwrap", "ansi-escape", "async-utils", @@ -138,6 +139,7 @@ codex-apply-patch = { path = "apply-patch" } codex-arg0 = { path = "arg0" } codex-async-utils = { path = "async-utils" } codex-backend-client = { path = "backend-client" } +codex-builtin-mcps = { path = "builtin-mcps" } codex-chatgpt = { path = "chatgpt" } codex-cli = { path = "cli" } codex-client = { path = "codex-client" } diff --git a/codex-rs/builtin-mcps/BUILD.bazel b/codex-rs/builtin-mcps/BUILD.bazel new file mode 100644 index 0000000000..9c738d636b --- /dev/null +++ b/codex-rs/builtin-mcps/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "builtin-mcps", + crate_name = "codex_builtin_mcps", +) diff --git a/codex-rs/builtin-mcps/Cargo.toml b/codex-rs/builtin-mcps/Cargo.toml new file mode 100644 index 0000000000..de64ec7033 --- /dev/null +++ b/codex-rs/builtin-mcps/Cargo.toml @@ -0,0 +1,21 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-builtin-mcps" +version.workspace = true + +[lib] +name = "codex_builtin_mcps" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true } +codex-config = { workspace = true } +codex-memories-mcp = { workspace = true } +codex-utils-absolute-path = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/builtin-mcps/src/lib.rs b/codex-rs/builtin-mcps/src/lib.rs new file mode 100644 index 0000000000..3fd4483dc1 --- /dev/null +++ b/codex-rs/builtin-mcps/src/lib.rs @@ -0,0 +1,132 @@ +//! Built-in MCP servers shipped with Codex. +//! +//! Built-ins use the same stdio MCP path as user-configured servers, but are +//! declared here so product-owned MCPs do not need to live in `codex-core`. + +use codex_config::McpServerConfig; +use codex_config::McpServerTransportConfig; +use codex_utils_absolute_path::AbsolutePathBuf; +use std::collections::HashMap; +use std::path::Path; + +pub const MEMORIES_MCP_SERVER_NAME: &str = "memories"; +const BUILTIN_MCP_SUBCOMMAND: &str = "builtin-mcp"; + +#[derive(Debug, Clone, Copy)] +pub struct BuiltinMcpServerOptions<'a> { + pub codex_self_exe: Option<&'a Path>, + pub codex_home: &'a Path, + pub memories_enabled: bool, +} + +pub fn configured_builtin_mcp_servers( + options: BuiltinMcpServerOptions<'_>, +) -> HashMap { + let Some(codex_self_exe) = options.codex_self_exe else { + return HashMap::new(); + }; + + let mut servers = HashMap::new(); + if options.memories_enabled { + servers.insert( + MEMORIES_MCP_SERVER_NAME.to_string(), + builtin_stdio_server_config( + codex_self_exe, + options.codex_home, + MEMORIES_MCP_SERVER_NAME, + ), + ); + } + servers +} + +pub async fn run_builtin_mcp_server( + name: &str, + codex_home: &AbsolutePathBuf, +) -> anyhow::Result<()> { + match name { + MEMORIES_MCP_SERVER_NAME => codex_memories_mcp::run_stdio_server(codex_home).await, + _ => anyhow::bail!("unknown built-in MCP server: {name}"), + } +} + +fn builtin_stdio_server_config( + codex_self_exe: &Path, + codex_home: &Path, + name: &str, +) -> McpServerConfig { + McpServerConfig { + transport: McpServerTransportConfig::Stdio { + command: codex_self_exe.to_string_lossy().into_owned(), + args: vec![ + BUILTIN_MCP_SUBCOMMAND.to_string(), + name.to_string(), + "--codex-home".to_string(), + codex_home.to_string_lossy().into_owned(), + ], + env: None, + env_vars: Vec::new(), + cwd: None, + }, + experimental_environment: None, + enabled: true, + required: false, + supports_parallel_tool_calls: true, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + default_tools_approval_mode: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + oauth_resource: None, + tools: HashMap::new(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn configured_builtin_mcp_servers_adds_memories_when_enabled() { + let codex_home = AbsolutePathBuf::try_from("/tmp/codex-home").expect("absolute codex home"); + let servers = configured_builtin_mcp_servers(BuiltinMcpServerOptions { + codex_self_exe: Some(Path::new("/tmp/codex")), + codex_home: codex_home.as_path(), + memories_enabled: true, + }); + + let server = servers + .get(MEMORIES_MCP_SERVER_NAME) + .expect("memories server should exist"); + assert_eq!( + server.transport, + McpServerTransportConfig::Stdio { + command: "/tmp/codex".to_string(), + args: vec![ + "builtin-mcp".to_string(), + "memories".to_string(), + "--codex-home".to_string(), + "/tmp/codex-home".to_string(), + ], + env: None, + env_vars: Vec::new(), + cwd: None, + } + ); + } + + #[test] + fn configured_builtin_mcp_servers_requires_reexec_path() { + let codex_home = AbsolutePathBuf::try_from("/tmp/codex-home").expect("absolute codex home"); + let servers = configured_builtin_mcp_servers(BuiltinMcpServerOptions { + codex_self_exe: None, + codex_home: codex_home.as_path(), + memories_enabled: true, + }); + + assert!(servers.is_empty()); + } +} diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index cdee241b42..b644734f4c 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -24,6 +24,7 @@ codex-app-server = { workspace = true } codex-app-server-protocol = { workspace = true } codex-app-server-test-client = { workspace = true } codex-arg0 = { workspace = true } +codex-builtin-mcps = { workspace = true } codex-chatgpt = { workspace = true } codex-cloud-tasks = { path = "../cloud-tasks" } codex-utils-cli = { workspace = true } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 5a8ffab36a..f3545629c1 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -123,6 +123,10 @@ enum Subcommand { /// Start Codex as an MCP server (stdio). McpServer, + /// Internal: start a Codex-shipped MCP server (stdio). + #[clap(hide = true, name = "builtin-mcp")] + BuiltinMcp(BuiltinMcpCommand), + /// [experimental] Run the app server or related tooling. AppServer(AppServerCommand), @@ -175,6 +179,13 @@ enum Subcommand { Features(FeaturesCli), } +#[derive(Debug, Args)] +struct BuiltinMcpCommand { + name: String, + #[arg(long)] + codex_home: PathBuf, +} + #[derive(Debug, Parser)] #[command(bin_name = "codex plugin")] struct PluginCli { @@ -809,6 +820,15 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { )?; codex_mcp_server::run_main(arg0_paths.clone(), root_config_overrides).await?; } + Some(Subcommand::BuiltinMcp(command)) => { + reject_remote_mode_for_subcommand( + root_remote.as_deref(), + root_remote_auth_token_env.as_deref(), + "builtin-mcp", + )?; + let codex_home = AbsolutePathBuf::try_from(command.codex_home)?; + codex_builtin_mcps::run_builtin_mcp_server(&command.name, &codex_home).await?; + } Some(Subcommand::Mcp(mut mcp_cli)) => { reject_remote_mode_for_subcommand( root_remote.as_deref(), diff --git a/codex-rs/codex-mcp/Cargo.toml b/codex-rs/codex-mcp/Cargo.toml index c3061adca9..c47bd947e6 100644 --- a/codex-rs/codex-mcp/Cargo.toml +++ b/codex-rs/codex-mcp/Cargo.toml @@ -16,6 +16,7 @@ anyhow = { workspace = true } async-channel = { workspace = true } codex-async-utils = { workspace = true } codex-api = { workspace = true } +codex-builtin-mcps = { workspace = true } codex-config = { workspace = true } codex-exec-server = { workspace = true } codex-login = { workspace = true } diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index 9b11cbaec1..54631cd1c1 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -19,6 +19,9 @@ pub use auth_elicitation::build_auth_elicitation_plan; pub use auth_elicitation::connector_auth_failure_from_tool_result; pub use codex_apps::CodexAppsToolsCacheKey; pub use codex_apps::codex_apps_tools_cache_key; +pub use codex_builtin_mcps::BuiltinMcpServerOptions; +pub use codex_builtin_mcps::MEMORIES_MCP_SERVER_NAME; +pub use codex_builtin_mcps::configured_builtin_mcp_servers; pub use mcp::configured_mcp_servers; pub use mcp::effective_mcp_servers; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 2fdbb7ccd4..958677b9e8 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -129,7 +129,11 @@ pub struct McpConfig { /// ChatGPT auth is checked separately at runtime before the built-in apps /// MCP server is added. pub apps_enabled: bool, - /// User-configured and plugin-provided MCP servers keyed by server name. + /// Configured MCP servers keyed by server name. + /// + /// This includes product-owned built-ins, user-configured servers, and + /// plugin-provided servers. Runtime-only additions belong in + /// [`effective_mcp_servers`]. pub configured_mcp_servers: HashMap, /// Plugin metadata used to attribute MCP tools/connectors to plugin display names. pub plugin_capability_summaries: Vec, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 57d3d276b2..9f74b8b7cf 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -379,6 +379,9 @@ "browser_use_external": { "type": "boolean" }, + "builtin_mcp": { + "type": "boolean" + }, "child_agents_md": { "type": "boolean" }, @@ -3931,6 +3934,9 @@ "browser_use_external": { "type": "boolean" }, + "builtin_mcp": { + "type": "boolean" + }, "child_agents_md": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 7001f9015c..f2ae82810a 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -195,12 +195,13 @@ async fn load_config_loads_global_agents_instructions() -> std::io::Result<()> { "\n global instructions \n", )?; - let config = Config::load_from_base_config_with_overrides( + let mut config = Config::load_from_base_config_with_overrides( ConfigToml::default(), ConfigOverrides::default(), codex_home.abs(), ) .await?; + let _ = config.features.enable(Feature::MemoryTool); assert_eq!( config.user_instructions.as_deref(), @@ -3344,6 +3345,77 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result< Ok(()) } +#[tokio::test] +async fn to_mcp_config_empty_mcp_requirements_preserve_builtin_mcps() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + let requirements = codex_config::ConfigRequirementsToml { + mcp_servers: Some(BTreeMap::new()), + ..Default::default() + }; + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .cloud_requirements(CloudRequirementsLoader::new(async move { + Ok(Some(requirements)) + })) + .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()); + + 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)) + ); + + Ok(()) +} + +#[tokio::test] +async fn to_mcp_config_nonempty_mcp_requirements_preserve_builtin_mcps() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + let requirements = codex_config::ConfigRequirementsToml { + mcp_servers: Some(BTreeMap::from([( + "docs".to_string(), + McpServerRequirement { + identity: McpServerIdentity::Command { + command: "docs-mcp".to_string(), + }, + }, + )])), + ..Default::default() + }; + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .cloud_requirements(CloudRequirementsLoader::new(async move { + Ok(Some(requirements)) + })) + .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()); + + 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)) + ); + + Ok(()) +} + #[tokio::test] async fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Result<()> { let codex_home = TempDir::new()?; @@ -4166,6 +4238,107 @@ async fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<( Ok(()) } +#[tokio::test] +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() + }, + codex_home.abs(), + ) + .await?; + let _ = config.features.enable(Feature::BuiltInMcp); + let _ = config.features.enable(Feature::MemoryTool); + let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); + + 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.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, + }) + ); + + Ok(()) +} + +#[tokio::test] +async fn to_mcp_config_omits_builtin_mcps_when_feature_is_disabled() -> 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() + }, + codex_home.abs(), + ) + .await?; + let _ = config.features.enable(Feature::MemoryTool); + let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); + + let mcp_config = config.to_mcp_config(&plugins_manager).await; + + assert!( + !mcp_config + .configured_mcp_servers + .contains_key(codex_mcp::MEMORIES_MCP_SERVER_NAME) + ); + + Ok(()) +} + +#[tokio::test] +async fn to_mcp_config_reserves_enabled_builtin_mcp_names() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let mut config = Config::load_from_base_config_with_overrides( + ConfigToml { + mcp_servers: HashMap::from([( + codex_mcp::MEMORIES_MCP_SERVER_NAME.to_string(), + http_mcp("https://user.example/memories"), + )]), + ..ConfigToml::default() + }, + ConfigOverrides { + codex_self_exe: Some(PathBuf::from("/tmp/codex")), + ..ConfigOverrides::default() + }, + codex_home.abs(), + ) + .await?; + let _ = config.features.enable(Feature::BuiltInMcp); + let _ = config.features.enable(Feature::MemoryTool); + let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); + + let mcp_config = config.to_mcp_config(&plugins_manager).await; + + assert!(matches!( + mcp_config + .configured_mcp_servers + .get(codex_mcp::MEMORIES_MCP_SERVER_NAME) + .map(|server| &server.transport), + Some(McpServerTransportConfig::Stdio { .. }) + )); + + Ok(()) +} + #[tokio::test] async fn load_global_mcp_servers_rejects_inline_bearer_token() -> anyhow::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 43d88bcdd2..d868e8474c 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -70,7 +70,9 @@ use codex_features::FeaturesToml; use codex_features::MultiAgentV2ConfigToml; 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_memories_read::memory_root; use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; use codex_model_provider_info::ModelProviderInfo; @@ -1087,6 +1089,13 @@ 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(), + memories_enabled: self.features.enabled(Feature::BuiltInMcp) + && self.features.enabled(Feature::MemoryTool) + && self.memories.use_memories, + }); let mut configured_mcp_servers = self.mcp_servers.get().clone(); for plugin in loaded_plugins .plugins() @@ -1106,9 +1115,12 @@ impl Config { if let Some(mcp_requirements) = self.config_layer_stack.requirements().mcp_servers.as_ref() && mcp_requirements.value.is_empty() { - // A present empty allowlist bans all MCPs, including plugin MCPs merged above. + // A present empty allowlist bans configurable MCPs, including plugin MCPs merged + // above. Built-ins are product-owned and stay available regardless of admin + // allowlists. filter_mcp_servers_by_requirements(&mut configured_mcp_servers, Some(mcp_requirements)); } + configured_mcp_servers.extend(builtin_mcp_servers); McpConfig { chatgpt_base_url: self.chatgpt_base_url.clone(), diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index dfacc0d55b..19605cd40b 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -134,6 +134,8 @@ pub enum Feature { Sqlite, /// Enable startup memory extraction and file-backed memory consolidation. MemoryTool, + /// Enable product-owned built-in MCP servers. + BuiltInMcp, /// Enable the Chronicle sidecar for passive screen-context memories. Chronicle, /// Append additional AGENTS.md guidance to user instructions. @@ -793,6 +795,12 @@ pub const FEATURES: &[FeatureSpec] = &[ }, default_enabled: false, }, + FeatureSpec { + id: Feature::BuiltInMcp, + key: "builtin_mcp", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::Chronicle, key: "chronicle", diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 4a258141b5..4ba6b76ce8 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -129,6 +129,13 @@ fn remote_compaction_v2_is_under_development() { ); } +#[test] +fn builtin_mcp_is_under_development() { + assert_eq!(Feature::BuiltInMcp.stage(), Stage::UnderDevelopment); + assert_eq!(Feature::BuiltInMcp.default_enabled(), false); + assert_eq!(feature_for_key("builtin_mcp"), Some(Feature::BuiltInMcp)); +} + #[test] fn terminal_resize_reflow_is_experimental_and_enabled_by_default() { assert_eq!( diff --git a/codex-rs/memories/README.md b/codex-rs/memories/README.md index 9195e89ada..73aefde297 100644 --- a/codex-rs/memories/README.md +++ b/codex-rs/memories/README.md @@ -10,6 +10,8 @@ Runtime orchestration for Phase 1 and Phase 2 still lives in `codex-core` under - `codex-rs/memories/read` (`codex-memories-read`) owns the read path: memory developer-instruction injection, memory citation parsing, and read-usage telemetry classification. +- `codex-rs/memories/mcp` (`codex-memories-mcp`) exposes the read-only memory + filesystem through the built-in MCP surface. - `codex-rs/memories/write` (`codex-memories-write`) owns the write path: Phase 1 and Phase 2 prompt rendering, filesystem artifact helpers, workspace diff helpers, and extension resource pruning.