mirror of
https://github.com/openai/codex.git
synced 2026-05-02 12:21:26 +03:00
spitballing
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
pub mod debug_sandbox;
|
||||
mod exit_status;
|
||||
pub mod login;
|
||||
pub mod mcp_cmd;
|
||||
pub mod proto;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
@@ -11,6 +11,8 @@ use codex_cli::login::run_login_status;
|
||||
use codex_cli::login::run_login_with_api_key;
|
||||
use codex_cli::login::run_login_with_chatgpt;
|
||||
use codex_cli::login::run_logout;
|
||||
use codex_cli::mcp_cmd;
|
||||
use codex_cli::mcp_cmd::McpCli;
|
||||
use codex_cli::proto;
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_exec::Cli as ExecCli;
|
||||
@@ -56,8 +58,8 @@ enum Subcommand {
|
||||
/// Remove stored authentication credentials.
|
||||
Logout(LogoutCommand),
|
||||
|
||||
/// Experimental: run Codex as an MCP server.
|
||||
Mcp,
|
||||
/// Experimental: run Codex as an MCP server and manage MCP config.
|
||||
Mcp(McpCli),
|
||||
|
||||
/// Run the Protocol stream via stdin/stdout
|
||||
#[clap(visible_alias = "p")]
|
||||
@@ -158,8 +160,9 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
||||
prepend_config_flags(&mut exec_cli.config_overrides, cli.config_overrides);
|
||||
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
|
||||
}
|
||||
Some(Subcommand::Mcp) => {
|
||||
codex_mcp_server::run_main(codex_linux_sandbox_exe, cli.config_overrides).await?;
|
||||
Some(Subcommand::Mcp(mut mcp_cli)) => {
|
||||
prepend_config_flags(&mut mcp_cli.config_overrides, cli.config_overrides);
|
||||
mcp_cmd::run_main(mcp_cli, codex_linux_sandbox_exe).await?;
|
||||
}
|
||||
Some(Subcommand::Login(mut login_cli)) => {
|
||||
prepend_config_flags(&mut login_cli.config_overrides, cli.config_overrides);
|
||||
|
||||
641
codex-rs/cli/src/mcp_cmd.rs
Normal file
641
codex-rs/cli/src/mcp_cmd.rs
Normal file
@@ -0,0 +1,641 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::config::load_config_as_toml_with_cli_overrides;
|
||||
use codex_core::config_types::McpServerConfig;
|
||||
use codex_core::git_info::resolve_root_git_project_for_trust;
|
||||
use codex_core::mcp_toml::McpToml;
|
||||
use codex_core::mcp_toml::McpTomlEntry;
|
||||
use codex_core::mcp_toml::load_project_overlays;
|
||||
use codex_core::mcp_toml::to_mcp_server_config;
|
||||
use serde_json::json;
|
||||
use tempfile as _;
|
||||
use toml::Value as TomlValue;
|
||||
use toml_edit as _; // ensure dependency is linked
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
about = "Manage MCP servers and run Codex as an MCP server",
|
||||
long_about = "Manage Model Context Protocol (MCP) servers configured for Codex.\n\nUse subcommands to add, import, list, inspect, or remove servers.\nIf no subcommand is provided, this runs the built-in MCP server (back-compat).",
|
||||
after_help = "Examples:\n # Add a local stdio server (everything after -- is the server command)\n codex mcp add airtable --env AIRTABLE_API_KEY=YOUR_KEY -- npx -y airtable-mcp-server\n\n # Import multiple servers from a TOML file into project scope\n codex mcp add-toml --scope project ./mcp.toml\n\n # List configured servers (merged view with precedence local > project > user)\n codex mcp list --json\n\n # Show details for a specific server\n codex mcp get airtable --json\n\n # Remove a server from the user scope\n codex mcp remove airtable --scope user\n\n # Remove a server from all scopes\n codex mcp remove airtable --all\n\n # Windows: wrap npx with cmd /c\n codex mcp add my-svc -- cmd /c npx -y @some/package"
|
||||
)]
|
||||
pub struct McpCli {
|
||||
#[clap(skip)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub cmd: Option<McpSub>,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum McpSub {
|
||||
/// Run Codex as an MCP server (back-compat: `codex mcp`).
|
||||
Serve,
|
||||
/// List configured MCP servers (merged view).
|
||||
List {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Get details for a specific server name (merged view).
|
||||
Get {
|
||||
name: String,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Add an MCP stdio server entry to a given scope.
|
||||
Add(AddArgs),
|
||||
/// Remove an MCP server entry from a given scope or all scopes.
|
||||
Remove(RemoveArgs),
|
||||
/// Import one or more MCP servers from a TOML file with a [mcp_servers] table.
|
||||
AddToml(AddTomlArgs),
|
||||
}
|
||||
|
||||
pub async fn run_main(mcp_cli: McpCli, codex_linux_sandbox_exe: Option<PathBuf>) -> Result<()> {
|
||||
match mcp_cli.cmd.unwrap_or(McpSub::Serve) {
|
||||
McpSub::Serve => {
|
||||
// Preserve the historical `codex mcp` behavior.
|
||||
codex_mcp_server::run_main(codex_linux_sandbox_exe, mcp_cli.config_overrides).await?
|
||||
}
|
||||
McpSub::List { json } => {
|
||||
list_servers(mcp_cli.config_overrides, json)?;
|
||||
}
|
||||
McpSub::Get { name, json } => {
|
||||
get_server(mcp_cli.config_overrides, &name, json)?;
|
||||
}
|
||||
McpSub::Add(args) => {
|
||||
add_server(mcp_cli.config_overrides, args)?;
|
||||
}
|
||||
McpSub::Remove(args) => {
|
||||
remove_server(mcp_cli.config_overrides, args)?;
|
||||
}
|
||||
McpSub::AddToml(args) => {
|
||||
add_toml(mcp_cli.config_overrides, args)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_cli_overrides(overrides: CliConfigOverrides) -> Vec<(String, TomlValue)> {
|
||||
overrides.parse_overrides().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn load_user_project_local_maps(
|
||||
cli_overrides: CliConfigOverrides,
|
||||
) -> Result<(
|
||||
HashMap<String, McpServerConfig>,
|
||||
HashMap<String, McpServerConfig>,
|
||||
HashMap<String, McpServerConfig>,
|
||||
)> {
|
||||
// User map via `~/.codex/config.toml` (+ -c overrides)
|
||||
let codex_home = find_codex_home()?;
|
||||
let user_cfg =
|
||||
load_config_as_toml_with_cli_overrides(&codex_home, parse_cli_overrides(cli_overrides))?;
|
||||
let mut user_map = user_cfg.mcp_servers;
|
||||
|
||||
// Project/local overlays via current project root
|
||||
let cwd = std::env::current_dir()?;
|
||||
let project_root = resolve_root_git_project_for_trust(&cwd).unwrap_or(cwd);
|
||||
let overlays = load_project_overlays(&project_root)?;
|
||||
|
||||
let mut project_map = HashMap::new();
|
||||
let mut local_map = HashMap::new();
|
||||
for (scope, overlay) in overlays {
|
||||
for (name, entry) in overlay.mcp_servers.into_iter() {
|
||||
// Convert permissive overlay entry → strict config, expanding env vars.
|
||||
if let Ok(cfg) = to_mcp_server_config(&entry, |k| std::env::var(k).ok()) {
|
||||
match scope {
|
||||
codex_core::mcp_toml::Scope::Project => {
|
||||
project_map.insert(name, cfg);
|
||||
}
|
||||
codex_core::mcp_toml::Scope::Local => {
|
||||
local_map.insert(name, cfg);
|
||||
}
|
||||
codex_core::mcp_toml::Scope::User => {
|
||||
user_map.insert(name, cfg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((user_map, project_map, local_map))
|
||||
}
|
||||
|
||||
fn list_servers(cli_overrides: CliConfigOverrides, json_out: bool) -> Result<()> {
|
||||
let (user_map, project_map, local_map) = load_user_project_local_maps(cli_overrides)?;
|
||||
let mut names: BTreeSet<String> = BTreeSet::new();
|
||||
names.extend(user_map.keys().cloned());
|
||||
names.extend(project_map.keys().cloned());
|
||||
names.extend(local_map.keys().cloned());
|
||||
|
||||
if json_out {
|
||||
let mut arr = Vec::new();
|
||||
for name in names {
|
||||
let (scope, cfg, shadowed_by) =
|
||||
pick_with_scope(&name, &user_map, &project_map, &local_map);
|
||||
arr.push(json!({
|
||||
"name": name,
|
||||
"scope": scope,
|
||||
"config": cfg_to_json(cfg),
|
||||
"shadowed_by": shadowed_by,
|
||||
}));
|
||||
}
|
||||
println!("{}", serde_json::to_string_pretty(&arr)?);
|
||||
} else {
|
||||
for name in names {
|
||||
let (scope, cfg, _) = pick_with_scope(&name, &user_map, &project_map, &local_map);
|
||||
let args_preview = if cfg.args.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", cfg.args.join(" "))
|
||||
};
|
||||
println!("{} [{}] -> {}{}", name, scope, cfg.command, args_preview);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_server(cli_overrides: CliConfigOverrides, name: &str, json_out: bool) -> Result<()> {
|
||||
let (user_map, project_map, local_map) = load_user_project_local_maps(cli_overrides)?;
|
||||
if !user_map.contains_key(name)
|
||||
&& !project_map.contains_key(name)
|
||||
&& !local_map.contains_key(name)
|
||||
{
|
||||
anyhow::bail!("MCP server `{}` not found in any scope", name);
|
||||
}
|
||||
let (scope, cfg, shadowed_by) = pick_with_scope(name, &user_map, &project_map, &local_map);
|
||||
if json_out {
|
||||
let obj = json!({
|
||||
"name": name,
|
||||
"scope": scope,
|
||||
"config": cfg_to_json(cfg),
|
||||
"shadowed_by": shadowed_by,
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&obj)?);
|
||||
} else {
|
||||
let args_preview = if cfg.args.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", cfg.args.join(" "))
|
||||
};
|
||||
println!("{} [{}] -> {}{}", name, scope, cfg.command, args_preview);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pick_with_scope<'a>(
|
||||
name: &str,
|
||||
user_map: &'a HashMap<String, McpServerConfig>,
|
||||
project_map: &'a HashMap<String, McpServerConfig>,
|
||||
local_map: &'a HashMap<String, McpServerConfig>,
|
||||
) -> (&'static str, &'a McpServerConfig, Vec<&'static str>) {
|
||||
if let Some(cfg) = local_map.get(name) {
|
||||
(
|
||||
"local",
|
||||
cfg,
|
||||
vec![
|
||||
if project_map.contains_key(name) {
|
||||
"project"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if user_map.contains_key(name) {
|
||||
"user"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect(),
|
||||
)
|
||||
} else if let Some(cfg) = project_map.get(name) {
|
||||
(
|
||||
"project",
|
||||
cfg,
|
||||
vec![if user_map.contains_key(name) {
|
||||
"user"
|
||||
} else {
|
||||
""
|
||||
}]
|
||||
.into_iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect(),
|
||||
)
|
||||
} else if let Some(cfg) = user_map.get(name) {
|
||||
("user", cfg, vec![])
|
||||
} else {
|
||||
// Should not occur because callers pre-check membership. Return a
|
||||
// fallback to avoid panics in release builds.
|
||||
let fallback = user_map
|
||||
.iter()
|
||||
.next()
|
||||
.or_else(|| project_map.iter().next())
|
||||
.or_else(|| local_map.iter().next());
|
||||
let (k, v) = match fallback {
|
||||
Some(kv) => kv,
|
||||
None => panic!("internal error: no MCP server entries found across scopes"),
|
||||
};
|
||||
let _ = k; // suppress unused warning
|
||||
("user", v, vec![])
|
||||
}
|
||||
}
|
||||
|
||||
fn cfg_to_json(cfg: &McpServerConfig) -> serde_json::Value {
|
||||
json!({
|
||||
"command": cfg.command,
|
||||
"args": cfg.args,
|
||||
"env": cfg.env,
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Add/remove writers
|
||||
// ------------------------------
|
||||
|
||||
#[derive(Copy, Clone, Debug, clap::ValueEnum)]
|
||||
enum ScopeArg {
|
||||
Local,
|
||||
Project,
|
||||
User,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AddArgs {
|
||||
/// Unique server name (^[A-Za-z0-9_-]+$)
|
||||
name: String,
|
||||
/// Target scope
|
||||
#[arg(long, value_enum, default_value_t = ScopeArg::Local)]
|
||||
scope: ScopeArg,
|
||||
/// Environment variables KEY=VALUE (repeatable)
|
||||
#[arg(long = "env")]
|
||||
env: Vec<String>,
|
||||
/// Command and args to launch the MCP server (after `--`)
|
||||
#[arg(trailing_var_arg = true)]
|
||||
cmd: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct RemoveArgs {
|
||||
/// Server name
|
||||
name: String,
|
||||
/// Scope to remove from; omit with --all to remove everywhere
|
||||
#[arg(long, value_enum)]
|
||||
scope: Option<ScopeArg>,
|
||||
/// Remove from all scopes
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
}
|
||||
|
||||
fn add_server(cli_overrides: CliConfigOverrides, args: AddArgs) -> Result<()> {
|
||||
validate_server_name(&args.name)?;
|
||||
if args.cmd.is_empty() {
|
||||
anyhow::bail!(
|
||||
"missing server command; use: codex mcp add <name> [--scope ...] [--env KEY=VALUE]... -- <command> [args...]"
|
||||
);
|
||||
}
|
||||
let command = args.cmd[0].clone();
|
||||
let cmd_args: Vec<String> = args.cmd.iter().skip(1).cloned().collect();
|
||||
let env_map = parse_env_kv(args.env.iter())?;
|
||||
|
||||
let path = match args.scope {
|
||||
ScopeArg::User => {
|
||||
write_user_scope(&args.name, &command, &cmd_args, &env_map, cli_overrides)?
|
||||
}
|
||||
ScopeArg::Project => write_overlay_scope(&args.name, &command, &cmd_args, &env_map, false)?,
|
||||
ScopeArg::Local => write_overlay_scope(&args.name, &command, &cmd_args, &env_map, true)?,
|
||||
};
|
||||
println!(
|
||||
"Added MCP server '{}' (scope: {}) → wrote {}",
|
||||
args.name,
|
||||
match args.scope {
|
||||
ScopeArg::Local => "local",
|
||||
ScopeArg::Project => "project",
|
||||
ScopeArg::User => "user",
|
||||
},
|
||||
path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_server(cli_overrides: CliConfigOverrides, args: RemoveArgs) -> Result<()> {
|
||||
if args.all && args.scope.is_some() {
|
||||
anyhow::bail!("cannot use --scope with --all");
|
||||
}
|
||||
|
||||
if args.all {
|
||||
let u = remove_user_scope(&args.name, cli_overrides.clone())?;
|
||||
if u.wrote {
|
||||
println!("Removed '{}' → wrote {}", args.name, u.path.display());
|
||||
}
|
||||
let p = remove_overlay_scope(&args.name, false)?;
|
||||
if p.wrote {
|
||||
println!("Removed '{}' → wrote {}", args.name, p.path.display());
|
||||
}
|
||||
let l = remove_overlay_scope(&args.name, true)?;
|
||||
if l.wrote {
|
||||
println!("Removed '{}' → wrote {}", args.name, l.path.display());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let outcome = match args.scope.unwrap_or(ScopeArg::Local) {
|
||||
ScopeArg::User => remove_user_scope(&args.name, cli_overrides)?,
|
||||
ScopeArg::Project => remove_overlay_scope(&args.name, false)?,
|
||||
ScopeArg::Local => remove_overlay_scope(&args.name, true)?,
|
||||
};
|
||||
if outcome.wrote {
|
||||
println!("Removed '{}' → wrote {}", args.name, outcome.path.display());
|
||||
} else {
|
||||
println!(
|
||||
"No changes for '{}' at {}",
|
||||
args.name,
|
||||
outcome.path.display()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AddTomlArgs {
|
||||
/// Path to a TOML file containing a [mcp_servers] table
|
||||
path: PathBuf,
|
||||
/// Target scope to import into
|
||||
#[arg(long, value_enum, default_value_t = ScopeArg::Local)]
|
||||
scope: ScopeArg,
|
||||
}
|
||||
|
||||
fn add_toml(_cli_overrides: CliConfigOverrides, args: AddTomlArgs) -> Result<()> {
|
||||
let contents = std::fs::read_to_string(&args.path)?;
|
||||
let parsed: McpToml = toml::from_str(&contents)?;
|
||||
let mut accepted: Vec<(String, McpTomlEntry)> = Vec::new();
|
||||
let mut rejected: Vec<(String, String)> = Vec::new();
|
||||
for (name, entry) in parsed.mcp_servers.into_iter() {
|
||||
if let Some(t) = entry.r#type.as_deref()
|
||||
&& !t.eq_ignore_ascii_case("stdio")
|
||||
{
|
||||
rejected.push((name, format!("unsupported transport `{}`", t)));
|
||||
continue;
|
||||
}
|
||||
if entry.command.is_none() {
|
||||
rejected.push((name, "missing command".to_string()));
|
||||
continue;
|
||||
}
|
||||
accepted.push((name, entry));
|
||||
}
|
||||
|
||||
let path = match args.scope {
|
||||
ScopeArg::User => write_user_batch(&accepted)?,
|
||||
ScopeArg::Project => write_overlay_batch(&accepted, false)?,
|
||||
ScopeArg::Local => write_overlay_batch(&accepted, true)?,
|
||||
};
|
||||
println!(
|
||||
"Imported {} MCP server(s) into {}",
|
||||
accepted.len(),
|
||||
path.display()
|
||||
);
|
||||
|
||||
if !rejected.is_empty() {
|
||||
for (n, why) in rejected {
|
||||
eprintln!("skipped `{}`: {}", n, why);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_env_kv<'a>(pairs: impl Iterator<Item = &'a String>) -> Result<HashMap<String, String>> {
|
||||
let mut map = HashMap::new();
|
||||
for p in pairs {
|
||||
if let Some((k, v)) = p.split_once('=') {
|
||||
if k.is_empty() {
|
||||
anyhow::bail!("invalid --env '{}': empty key", p);
|
||||
}
|
||||
map.insert(k.to_string(), v.to_string());
|
||||
} else {
|
||||
anyhow::bail!("invalid --env '{}': expected KEY=VALUE", p);
|
||||
}
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn validate_server_name(name: &str) -> Result<()> {
|
||||
let ok = !name.is_empty()
|
||||
&& name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
|
||||
if ok {
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"invalid server name '{}': must match ^[a-zA-Z0-9_-]+$",
|
||||
name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_codex_home_for_write() -> Result<PathBuf> {
|
||||
if let Ok(val) = std::env::var("CODEX_HOME")
|
||||
&& !val.is_empty()
|
||||
{
|
||||
let p = PathBuf::from(val);
|
||||
if !p.exists() {
|
||||
std::fs::create_dir_all(&p)?;
|
||||
}
|
||||
return Ok(p.canonicalize().unwrap_or(p));
|
||||
}
|
||||
let p = find_codex_home()?;
|
||||
if !p.exists() {
|
||||
std::fs::create_dir_all(&p)?;
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
fn write_user_scope(
|
||||
name: &str,
|
||||
command: &str,
|
||||
args: &[String],
|
||||
env_map: &HashMap<String, String>,
|
||||
cli_overrides: CliConfigOverrides,
|
||||
) -> Result<PathBuf> {
|
||||
let codex_home = resolve_codex_home_for_write()?;
|
||||
let path = codex_home.join("config.toml");
|
||||
let contents = std::fs::read_to_string(&path).unwrap_or_default();
|
||||
let mut doc = contents
|
||||
.parse::<toml_edit::DocumentMut>()
|
||||
.unwrap_or_default();
|
||||
upsert_mcp_entry(&mut doc, name, command, args, env_map);
|
||||
write_doc_atomic(&doc, &path)?;
|
||||
let _ = cli_overrides;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn write_overlay_scope(
|
||||
name: &str,
|
||||
command: &str,
|
||||
args: &[String],
|
||||
env_map: &HashMap<String, String>,
|
||||
local: bool,
|
||||
) -> Result<PathBuf> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let project_root = resolve_root_git_project_for_trust(&cwd).unwrap_or(cwd);
|
||||
let fname = if local {
|
||||
".mcp.local.toml"
|
||||
} else {
|
||||
".mcp.toml"
|
||||
};
|
||||
let path = project_root.join(fname);
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let contents = std::fs::read_to_string(&path).unwrap_or_default();
|
||||
let mut doc = contents
|
||||
.parse::<toml_edit::DocumentMut>()
|
||||
.unwrap_or_default();
|
||||
upsert_mcp_entry(&mut doc, name, command, args, env_map);
|
||||
write_doc_atomic(&doc, &path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn write_user_batch(entries: &[(String, McpTomlEntry)]) -> Result<PathBuf> {
|
||||
let codex_home = resolve_codex_home_for_write()?;
|
||||
let path = codex_home.join("config.toml");
|
||||
let contents = std::fs::read_to_string(&path).unwrap_or_default();
|
||||
let mut doc = contents
|
||||
.parse::<toml_edit::DocumentMut>()
|
||||
.unwrap_or_default();
|
||||
for (name, entry) in entries {
|
||||
let args = entry.args.clone();
|
||||
let env_map = entry.env.clone();
|
||||
let command = entry.command.clone().unwrap_or_default();
|
||||
upsert_mcp_entry(&mut doc, name, &command, &args, &env_map);
|
||||
}
|
||||
write_doc_atomic(&doc, &path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn write_overlay_batch(entries: &[(String, McpTomlEntry)], local: bool) -> Result<PathBuf> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let project_root = resolve_root_git_project_for_trust(&cwd).unwrap_or(cwd);
|
||||
let fname = if local {
|
||||
".mcp.local.toml"
|
||||
} else {
|
||||
".mcp.toml"
|
||||
};
|
||||
let path = project_root.join(fname);
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let contents = std::fs::read_to_string(&path).unwrap_or_default();
|
||||
let mut doc = contents
|
||||
.parse::<toml_edit::DocumentMut>()
|
||||
.unwrap_or_default();
|
||||
for (name, entry) in entries {
|
||||
let args = entry.args.clone();
|
||||
let env_map = entry.env.clone();
|
||||
let command = entry.command.clone().unwrap_or_default();
|
||||
upsert_mcp_entry(&mut doc, name, &command, &args, &env_map);
|
||||
}
|
||||
write_doc_atomic(&doc, &path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
struct RemoveOutcome {
|
||||
path: PathBuf,
|
||||
wrote: bool,
|
||||
}
|
||||
|
||||
fn remove_user_scope(name: &str, _cli_overrides: CliConfigOverrides) -> Result<RemoveOutcome> {
|
||||
let codex_home = resolve_codex_home_for_write()?;
|
||||
let path = codex_home.join("config.toml");
|
||||
if !path.exists() {
|
||||
return Ok(RemoveOutcome { path, wrote: false });
|
||||
}
|
||||
let contents = std::fs::read_to_string(&path)?;
|
||||
let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
|
||||
if let Some(tbl) = doc.get_mut("mcp_servers").and_then(|i| i.as_table_mut()) {
|
||||
if tbl.remove(name).is_some() {
|
||||
write_doc_atomic(&doc, &path)?;
|
||||
return Ok(RemoveOutcome { path, wrote: true });
|
||||
}
|
||||
}
|
||||
Ok(RemoveOutcome { path, wrote: false })
|
||||
}
|
||||
|
||||
fn remove_overlay_scope(name: &str, local: bool) -> Result<RemoveOutcome> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let project_root = resolve_root_git_project_for_trust(&cwd).unwrap_or(cwd);
|
||||
let fname = if local {
|
||||
".mcp.local.toml"
|
||||
} else {
|
||||
".mcp.toml"
|
||||
};
|
||||
let path = project_root.join(fname);
|
||||
if !path.exists() {
|
||||
return Ok(RemoveOutcome { path, wrote: false });
|
||||
}
|
||||
let contents = std::fs::read_to_string(&path)?;
|
||||
let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
|
||||
if let Some(tbl) = doc.get_mut("mcp_servers").and_then(|i| i.as_table_mut()) {
|
||||
if tbl.remove(name).is_some() {
|
||||
write_doc_atomic(&doc, &path)?;
|
||||
return Ok(RemoveOutcome { path, wrote: true });
|
||||
}
|
||||
}
|
||||
Ok(RemoveOutcome { path, wrote: false })
|
||||
}
|
||||
|
||||
fn upsert_mcp_entry(
|
||||
doc: &mut toml_edit::DocumentMut,
|
||||
name: &str,
|
||||
command: &str,
|
||||
args: &[String],
|
||||
env_map: &HashMap<String, String>,
|
||||
) {
|
||||
if !doc.as_table().contains_key("mcp_servers") {
|
||||
doc.insert("mcp_servers", toml_edit::table());
|
||||
}
|
||||
let tbl = doc["mcp_servers"].as_table_mut().expect("table");
|
||||
tbl.set_implicit(false);
|
||||
|
||||
if !tbl.contains_key(name) {
|
||||
tbl.insert(name, toml_edit::table());
|
||||
}
|
||||
let st = tbl[name].as_table_mut().expect("subtable");
|
||||
st.set_implicit(false);
|
||||
|
||||
st["command"] = toml_edit::value(command);
|
||||
let mut arr = toml_edit::Array::new();
|
||||
for a in args {
|
||||
arr.push(a.as_str());
|
||||
}
|
||||
st["args"] = toml_edit::Item::Value(toml_edit::Value::Array(arr));
|
||||
|
||||
if env_map.is_empty() {
|
||||
if st.contains_key("env") {
|
||||
st.remove("env");
|
||||
}
|
||||
} else {
|
||||
let mut kv = toml_edit::InlineTable::new();
|
||||
for (k, v) in env_map {
|
||||
kv.get_or_insert(k, toml_edit::Value::from(v.as_str()));
|
||||
}
|
||||
st["env"] = toml_edit::Item::Value(toml_edit::Value::InlineTable(kv));
|
||||
}
|
||||
}
|
||||
|
||||
fn write_doc_atomic(doc: &toml_edit::DocumentMut, path: &PathBuf) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let tmp = tempfile::NamedTempFile::new_in(
|
||||
path.parent().unwrap_or_else(|| std::path::Path::new(".")),
|
||||
)?;
|
||||
std::fs::write(tmp.path(), doc.to_string())?;
|
||||
tmp.persist(path)?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user