Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion codex-rs/builtin-mcps/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ workspace = true

[dependencies]
anyhow = { workspace = true }
codex-config = { workspace = true }
codex-memories-mcp = { workspace = true }
codex-utils-absolute-path = { workspace = true }
tokio = { workspace = true, features = ["io-util"] }

[dev-dependencies]
pretty_assertions = { workspace = true }
169 changes: 69 additions & 100 deletions codex-rs/builtin-mcps/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,87 +1,77 @@
//! 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`.
//! This crate owns the catalog of product-owned MCP servers and the small
//! amount of server-specific dispatch needed to run them. Runtime placement is
//! chosen by `codex-mcp`; built-ins should not be flattened into user-facing
//! MCP server config just to make them launchable.

use codex_config::McpServerConfig;
use codex_config::McpServerTransportConfig;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;

use tokio::io::AsyncRead;
use tokio::io::AsyncWrite;

pub const MEMORIES_MCP_SERVER_NAME: &str = "memories";
const BUILTIN_MCP_SUBCOMMAND: &str = "builtin-mcp";

/// Product-owned MCP servers that Codex can provide without user config.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuiltinMcpServer {
Memories,
}

#[derive(Debug, Clone, Copy)]
pub struct BuiltinMcpServerOptions<'a> {
pub codex_self_exe: Option<&'a Path>,
pub codex_home: &'a Path,
pub memories_enabled: bool,
struct BuiltinMcpServerMetadata {
name: &'static str,
supports_parallel_tool_calls: bool,
pollutes_memory: bool,
}

pub fn configured_builtin_mcp_servers(
options: BuiltinMcpServerOptions<'_>,
) -> HashMap<String, McpServerConfig> {
let Some(codex_self_exe) = options.codex_self_exe else {
return HashMap::new();
};
impl BuiltinMcpServer {
const fn metadata(self) -> BuiltinMcpServerMetadata {
match self {
Self::Memories => BuiltinMcpServerMetadata {
name: MEMORIES_MCP_SERVER_NAME,
supports_parallel_tool_calls: true,
pollutes_memory: false,
},
}
}

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,
),
);
pub const fn name(self) -> &'static str {
self.metadata().name
}

pub const fn supports_parallel_tool_calls(self) -> bool {
self.metadata().supports_parallel_tool_calls
}
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}"),
pub const fn pollutes_memory(self) -> bool {
self.metadata().pollutes_memory
}

pub async fn serve<T>(self, codex_home: &Path, transport: T) -> anyhow::Result<()>
where
T: AsyncRead + AsyncWrite + Send + 'static,
{
match self {
Self::Memories => {
let codex_home = codex_utils_absolute_path::AbsolutePathBuf::try_from(codex_home)?;
codex_memories_mcp::run_server(&codex_home, transport).await
}
}
}
}

#[derive(Debug, Clone, Copy)]
pub struct BuiltinMcpServerOptions {
pub memories_enabled: bool,
}

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(),
pub fn enabled_builtin_mcp_servers(options: BuiltinMcpServerOptions) -> Vec<BuiltinMcpServer> {
let mut servers = Vec::new();
if options.memories_enabled {
servers.push(BuiltinMcpServer::Memories);
}
servers
}

#[cfg(test)]
Expand All @@ -90,43 +80,22 @@ mod tests {
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");
fn enabled_builtin_mcp_servers_adds_memories_when_enabled() {
assert_eq!(
server.transport,
McpServerTransportConfig::Stdio {
command: "/tmp/codex".to_string(),
args: vec![
"builtin-mcp".to_string(),
"memories".to_string(),
"--codex-home".to_string(),
codex_home.as_path().to_string_lossy().into_owned(),
],
env: None,
env_vars: Vec::new(),
cwd: None,
}
enabled_builtin_mcp_servers(BuiltinMcpServerOptions {
memories_enabled: true,
}),
vec![BuiltinMcpServer::Memories]
);
}

#[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());
fn enabled_builtin_mcp_servers_omits_memories_when_disabled() {
assert_eq!(
enabled_builtin_mcp_servers(BuiltinMcpServerOptions {
memories_enabled: false,
}),
Vec::<BuiltinMcpServer>::new()
);
}
}
1 change: 0 additions & 1 deletion codex-rs/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ 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 }
Expand Down
20 changes: 0 additions & 20 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,6 @@ 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),

Expand Down Expand Up @@ -179,13 +175,6 @@ 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 {
Expand Down Expand Up @@ -820,15 +809,6 @@ 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(),
Expand Down
11 changes: 6 additions & 5 deletions codex-rs/cli/src/mcp_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(
config.codex_home.to_path_buf(),
)));
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await;
let mcp_servers = mcp_manager.configured_servers(&config).await;

let LoginArgs { name, scopes } = login_args;

Expand Down Expand Up @@ -450,7 +450,7 @@ async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutAr
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(
config.codex_home.to_path_buf(),
)));
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await;
let mcp_servers = mcp_manager.configured_servers(&config).await;

let LogoutArgs { name } = logout_args;

Expand Down Expand Up @@ -482,12 +482,13 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(
config.codex_home.to_path_buf(),
)));
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await;
let mcp_servers = mcp_manager.configured_servers(&config).await;
let effective_mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await;

let mut entries: Vec<_> = mcp_servers.iter().collect();
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
let auth_statuses = compute_auth_statuses(
mcp_servers.iter(),
effective_mcp_servers.iter(),
config.mcp_oauth_credentials_store_mode,
/*auth*/ None,
)
Expand Down Expand Up @@ -737,7 +738,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(
config.codex_home.to_path_buf(),
)));
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await;
let mcp_servers = mcp_manager.configured_servers(&config).await;

let Some(server) = mcp_servers.get(&get_args.name) else {
bail!("No MCP server named '{name}' found.", name = get_args.name);
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/codex-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha1 = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
tokio = { workspace = true, features = ["io-util", "macros", "rt-multi-thread"] }
tokio-util = { workspace = true, features = ["rt"] }
tracing = { workspace = true }
url = { workspace = true }
Expand Down
39 changes: 39 additions & 0 deletions codex-rs/codex-mcp/src/builtin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::io;
use std::path::PathBuf;

use codex_builtin_mcps::BuiltinMcpServer;
use codex_rmcp_client::InProcessTransportFactory;
use futures::FutureExt;
use futures::future::BoxFuture;

#[derive(Clone)]
pub(crate) struct BuiltinMcpServerFactory {
server: BuiltinMcpServer,
codex_home: PathBuf,
}

impl BuiltinMcpServerFactory {
pub(crate) fn new(server: BuiltinMcpServer, codex_home: PathBuf) -> Self {
Self { server, codex_home }
}
}

impl InProcessTransportFactory for BuiltinMcpServerFactory {
fn open(&self) -> BoxFuture<'static, io::Result<tokio::io::DuplexStream>> {
let server = self.server;
let codex_home = self.codex_home.clone();
async move {
let (client_transport, server_transport) = tokio::io::duplex(64 * 1024);
tokio::spawn(async move {
if let Err(err) = server.serve(&codex_home, server_transport).await {
tracing::warn!(
server = server.name(),
"built-in MCP server exited: {err:#}"
);
}
});
Ok(client_transport)
}
.boxed()
}
}
Loading
Loading