use std::collections::HashMap;use std::sync::Arc;use anyhow::Context;use anyhow::Result;use anyhow::anyhow;use anyhow::bail;use clap::ArgGroup;use codex_config::types::AppToolApproval;use codex_config::types::McpServerConfig;use codex_config::types::McpServerOAuthConfig;use codex_config::types::McpServerTransportConfig;use codex_core::McpManager;use codex_core::config::Config;use codex_core::config::ConfigBuilder;use codex_core::config::LoaderOverrides;use codex_core::config::edit::ConfigEditsBuilder;use codex_core::config::find_codex_home;use codex_core::config::load_global_mcp_servers;use codex_core_plugins::PluginsManager;use codex_mcp::McpOAuthLoginSupport;use codex_mcp::ResolvedMcpOAuthScopes;use codex_mcp::compute_auth_statuses;use codex_mcp::discover_supported_scopes;use codex_mcp::oauth_login_support;use codex_mcp::resolve_oauth_scopes;use codex_mcp::should_retry_without_scopes;use codex_protocol::protocol::McpAuthStatus;use codex_rmcp_client::delete_oauth_tokens;use codex_rmcp_client::perform_oauth_login;use codex_utils_cli::CliConfigOverrides;use codex_utils_cli::format_env_display;/// Subcommands:/// - `list` — list configured servers (with `--json`)/// - `get` — show a single server (with `--json`)/// - `add` — add a server launcher entry to `~/.codex/config.toml`/// - `remove` — delete a server entry/// - `login` — authenticate with MCP server using OAuth/// - `logout` — remove OAuth credentials for MCP server#[derive(Debug, clap::Parser)]pub struct McpCli { #[clap(flatten)] pub config_overrides: CliConfigOverrides, #[command(subcommand)] pub subcommand: McpSubcommand,}#[derive(Debug, clap::Subcommand)]pub enum McpSubcommand { List(ListArgs), Get(GetArgs), Add(AddArgs), Remove(RemoveArgs), Login(LoginArgs), Logout(LogoutArgs),}#[derive(Debug, clap::Parser)]pub struct ListArgs { /// Output the configured servers as JSON. #[arg(long)] pub json: bool,}#[derive(Debug, clap::Parser)]pub struct GetArgs { /// Name of the MCP server to display. pub name: String, /// Output the server configuration as JSON. #[arg(long)] pub json: bool,}#[derive(Debug, clap::Parser)]#[command(override_usage = "codex mcp add [OPTIONS] <NAME> (--url <URL> | -- <COMMAND>...)")]pub struct AddArgs { /// Name for the MCP server configuration. pub name: String, #[command(flatten)] pub transport_args: AddMcpTransportArgs,}#[derive(Debug, clap::Args)]#[command( group( ArgGroup::new("transport") .args(["command", "url"]) .required(true) .multiple(false) ))]pub struct AddMcpTransportArgs { #[command(flatten)] pub stdio: Option<AddMcpStdioArgs>, #[command(flatten)] pub streamable_http: Option<AddMcpStreamableHttpArgs>,}#[derive(Debug, clap::Args)]pub struct AddMcpStdioArgs { /// Command to launch the MCP server. /// Use --url for a streamable HTTP server. #[arg( trailing_var_arg = true, num_args = 0.., )] pub command: Vec<String>, /// Environment variables to set when launching the server. /// Only valid with stdio servers. #[arg( long, value_parser = parse_env_pair, value_name = "KEY=VALUE", )] pub env: Vec<(String, String)>,}#[derive(Debug, clap::Args)]pub struct AddMcpStreamableHttpArgs { /// URL for a streamable HTTP MCP server. #[arg(long)] pub url: String, /// Optional environment variable to read for a bearer token. /// Only valid with streamable HTTP servers. #[arg( long = "bearer-token-env-var", value_name = "ENV_VAR", requires = "url" )] pub bearer_token_env_var: Option<String>, /// Optional OAuth client identifier to use for this MCP server. #[arg(long = "oauth-client-id", value_name = "CLIENT_ID", requires = "url")] pub oauth_client_id: Option<String>, /// Optional OAuth resource parameter to include during MCP login. #[arg(long = "oauth-resource", value_name = "RESOURCE", requires = "url")] pub oauth_resource: Option<String>,}#[derive(Debug, clap::Parser)]pub struct RemoveArgs { /// Name of the MCP server configuration to remove. pub name: String,}#[derive(Debug, clap::Parser)]pub struct LoginArgs { /// Name of the MCP server to authenticate with oauth. pub name: String, /// Comma-separated list of OAuth scopes to request. #[arg(long, value_delimiter = ',', value_name = "SCOPE,SCOPE")] pub scopes: Vec<String>,}#[derive(Debug, clap::Parser)]pub struct LogoutArgs { /// Name of the MCP server to deauthenticate. pub name: String,}impl McpCli { pub async fn run(self, loader_overrides: LoaderOverrides) -> Result<()> { let McpCli { config_overrides, subcommand, } = self; if loader_overrides.user_config_profile.is_some() { validate_profile_v2_migration(&config_overrides, loader_overrides).await?; } match subcommand { McpSubcommand::List(args) => { run_list(&config_overrides, args).await?; } McpSubcommand::Get(args) => { run_get(&config_overrides, args).await?; } McpSubcommand::Add(args) => { run_add(&config_overrides, args).await?; } McpSubcommand::Remove(args) => { run_remove(&config_overrides, args).await?; } McpSubcommand::Login(args) => { run_login(&config_overrides, args).await?; } McpSubcommand::Logout(args) => { run_logout(&config_overrides, args).await?; } } Ok(()) }}/// Preserve compatibility with servers that still expect the legacy empty-scope/// OAuth request. If a discovered-scope request is rejected by the provider,/// retry the login flow once without scopes.#[allow(clippy::too_many_arguments)]async fn perform_oauth_login_retry_without_scopes( name: &str, url: &str, store_mode: codex_config::types::OAuthCredentialsStoreMode, keyring_backend_kind: codex_config::types::AuthKeyringBackendKind, http_headers: Option<HashMap<String, String>>, env_http_headers: Option<HashMap<String, String>>, resolved_scopes: &ResolvedMcpOAuthScopes, oauth_client_id: Option<&str>, oauth_resource: Option<&str>, callback_port: Option<u16>, callback_url: Option<&str>,) -> Result<()> { match perform_oauth_login( name, url, store_mode, keyring_backend_kind, http_headers.clone(), env_http_headers.clone(), &resolved_scopes.scopes, oauth_client_id, oauth_resource, callback_port, callback_url, ) .await { Ok(()) => Ok(()), Err(err) if should_retry_without_scopes(resolved_scopes, &err) => { println!("OAuth provider rejected discovered scopes. Retrying without scopes…"); perform_oauth_login( name, url, store_mode, keyring_backend_kind, http_headers, env_http_headers, &[], oauth_client_id, oauth_resource, callback_port, callback_url, ) .await } Err(err) => Err(err), }}async fn validate_profile_v2_migration( config_overrides: &CliConfigOverrides, loader_overrides: LoaderOverrides,) -> Result<()> { let overrides = config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?; ConfigBuilder::default() .cli_overrides(overrides) .loader_overrides(loader_overrides) .build() .await .context("failed to load configuration")?; Ok(())}async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<()> { // Validate any provided overrides even though they are not currently applied. let overrides = config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?; let config = Config::load_with_cli_overrides(overrides) .await .context("failed to load configuration")?; let AddArgs { name, transport_args, } = add_args; validate_server_name(&name)?; let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; let mut servers = load_global_mcp_servers(&codex_home) .await .with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?; let (transport, oauth_client_id, oauth_resource) = match transport_args { AddMcpTransportArgs { stdio: Some(stdio), .. } => { let mut command_parts = stdio.command.into_iter(); let command_bin = command_parts .next() .ok_or_else(|| anyhow!("command is required"))?; let command_args: Vec<String> = command_parts.collect(); let env_map = if stdio.env.is_empty() { None } else { Some(stdio.env.into_iter().collect::<HashMap<_, _>>()) }; ( McpServerTransportConfig::Stdio { command: command_bin, args: command_args, env: env_map, env_vars: Vec::new(), cwd: None, }, None, None, ) } AddMcpTransportArgs { streamable_http: Some(AddMcpStreamableHttpArgs { url, bearer_token_env_var, oauth_client_id, oauth_resource, }), .. } => ( McpServerTransportConfig::StreamableHttp { url, bearer_token_env_var, http_headers: None, env_http_headers: None, }, oauth_client_id, oauth_resource, ), AddMcpTransportArgs { .. } => bail!("exactly one of --command or --url must be provided"), }; let new_entry = McpServerConfig { transport: transport.clone(), environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: true, required: false, supports_parallel_tool_calls: false, 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: oauth_client_id .clone() .map(|client_id| McpServerOAuthConfig { client_id: Some(client_id), }), oauth_resource: oauth_resource.clone(), tools: HashMap::new(), }; servers.insert(name.clone(), new_entry); ConfigEditsBuilder::new(&codex_home) .replace_mcp_servers(&servers) .apply() .await .with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?; println!("Added global MCP server '{name}'."); match oauth_login_support(&transport).await { McpOAuthLoginSupport::Supported(oauth_config) => { println!("Detected OAuth support. Starting OAuth flow…"); let resolved_scopes = resolve_oauth_scopes( /*explicit_scopes*/ None, /*configured_scopes*/ None, oauth_config.discovered_scopes.clone(), ); perform_oauth_login_retry_without_scopes( &name, &oauth_config.url, config.mcp_oauth_credentials_store_mode, config.auth_keyring_backend_kind(), oauth_config.http_headers, oauth_config.env_http_headers, &resolved_scopes, oauth_client_id.as_deref(), oauth_resource.as_deref(), config.mcp_oauth_callback_port, config.mcp_oauth_callback_url.as_deref(), ) .await?; println!("Successfully logged in."); } McpOAuthLoginSupport::Unsupported => {} McpOAuthLoginSupport::Unknown(_) => println!( "MCP server may or may not require login. Run `codex mcp login {name}` to login." ), } Ok(())}async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) -> Result<()> { config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?; let RemoveArgs { name } = remove_args; validate_server_name(&name)?; let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; let mut servers = load_global_mcp_servers(&codex_home) .await .with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?; let removed = servers.remove(&name).is_some(); if removed { ConfigEditsBuilder::new(&codex_home) .replace_mcp_servers(&servers) .apply() .await .with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?; } if removed { println!("Removed global MCP server '{name}'."); } else { println!("No MCP server named '{name}' found."); } Ok(())}async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs) -> Result<()> { let overrides = config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?; let config = Config::load_with_cli_overrides(overrides) .await .context("failed to load configuration")?; let mcp_manager = McpManager::new(Arc::new(PluginsManager::new( config.codex_home.to_path_buf(), ))); let mcp_servers = mcp_manager.configured_servers(&config).await; let LoginArgs { name, scopes } = login_args; let Some(server) = mcp_servers.get(&name) else { bail!("No MCP server named '{name}' found."); }; let (url, http_headers, env_http_headers) = match &server.transport { McpServerTransportConfig::StreamableHttp { url, http_headers, env_http_headers, .. } => (url.clone(), http_headers.clone(), env_http_headers.clone()), _ => bail!("OAuth login is only supported for streamable HTTP servers."), }; let explicit_scopes = (!scopes.is_empty()).then_some(scopes); let discovered_scopes = if explicit_scopes.is_none() && server.scopes.is_none() { discover_supported_scopes(&server.transport).await } else { None }; let resolved_scopes = resolve_oauth_scopes(explicit_scopes, server.scopes.clone(), discovered_scopes); perform_oauth_login_retry_without_scopes( &name, &url, config.mcp_oauth_credentials_store_mode, config.auth_keyring_backend_kind(), http_headers, env_http_headers, &resolved_scopes, server.oauth_client_id(), server.oauth_resource.as_deref(), config.mcp_oauth_callback_port, config.mcp_oauth_callback_url.as_deref(), ) .await?; println!("Successfully logged in to MCP server '{name}'."); Ok(())}async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutArgs) -> Result<()> { let overrides = config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?; let config = Config::load_with_cli_overrides(overrides) .await .context("failed to load configuration")?; let mcp_manager = McpManager::new(Arc::new(PluginsManager::new( config.codex_home.to_path_buf(), ))); let mcp_servers = mcp_manager.configured_servers(&config).await; let LogoutArgs { name } = logout_args; let server = mcp_servers .get(&name) .ok_or_else(|| anyhow!("No MCP server named '{name}' found in configuration."))?; let url = match &server.transport { McpServerTransportConfig::StreamableHttp { url, .. } => url.clone(), _ => bail!("OAuth logout is only supported for streamable_http transports."), }; match delete_oauth_tokens( &name, &url, config.mcp_oauth_credentials_store_mode, config.auth_keyring_backend_kind(), ) { Ok(true) => println!("Removed OAuth credentials for '{name}'."), Ok(false) => println!("No OAuth credentials stored for '{name}'."), Err(err) => return Err(anyhow!("failed to delete OAuth credentials: {err}")), } Ok(())}async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> Result<()> { let overrides = config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?; let config = Config::load_with_cli_overrides(overrides) .await .context("failed to load configuration")?; let mcp_manager = McpManager::new(Arc::new(PluginsManager::new( config.codex_home.to_path_buf(), ))); 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_key(|(name, _)| *name); let auth_statuses = compute_auth_statuses( effective_mcp_servers.iter(), config.mcp_oauth_credentials_store_mode, config.auth_keyring_backend_kind(), /*auth*/ None, ) .await; if list_args.json { let json_entries: Vec<_> = entries .into_iter() .map(|(name, cfg)| { let auth_status = auth_statuses .get(name.as_str()) .map(|entry| entry.auth_status) .unwrap_or(McpAuthStatus::Unsupported); let transport = match &cfg.transport { McpServerTransportConfig::Stdio { command, args, env, env_vars, cwd, } => serde_json::json!({ "type": "stdio", "command": command, "args": args, "env": env, "env_vars": env_vars, "cwd": cwd, }), McpServerTransportConfig::StreamableHttp { url, bearer_token_env_var, http_headers, env_http_headers, } => { serde_json::json!({ "type": "streamable_http", "url": url, "bearer_token_env_var": bearer_token_env_var, "http_headers": http_headers, "env_http_headers": env_http_headers, }) } }; serde_json::json!({ "name": name, "enabled": cfg.enabled, "disabled_reason": cfg.disabled_reason.as_ref().map(ToString::to_string), "transport": transport, "startup_timeout_sec": cfg .startup_timeout_sec .map(|timeout| timeout.as_secs_f64()), "tool_timeout_sec": cfg .tool_timeout_sec .map(|timeout| timeout.as_secs_f64()), "auth_status": auth_status, }) }) .collect(); let output = serde_json::to_string_pretty(&json_entries)?; println!("{output}"); return Ok(()); } if entries.is_empty() { println!("No MCP servers configured yet. Try `codex mcp add my-tool -- my-command`."); return Ok(()); } let mut stdio_rows: Vec<[String; 7]> = Vec::new(); let mut http_rows: Vec<[String; 5]> = Vec::new(); for (name, cfg) in entries { match &cfg.transport { McpServerTransportConfig::Stdio { command, args, env, env_vars, cwd, } => { let args_display = if args.is_empty() { "-".to_string() } else { args.join(" ") }; let env_display = format_env_display(env.as_ref(), env_vars); let cwd_display = cwd .as_ref() .map(|path| path.display().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| "-".to_string()); let status = format_mcp_status(cfg); let auth_status = auth_statuses .get(name.as_str()) .map(|entry| entry.auth_status) .unwrap_or(McpAuthStatus::Unsupported) .to_string(); stdio_rows.push([ name.clone(), command.clone(), args_display, env_display, cwd_display, status, auth_status, ]); } McpServerTransportConfig::StreamableHttp { url, bearer_token_env_var, .. } => { let status = format_mcp_status(cfg); let auth_status = auth_statuses .get(name.as_str()) .map(|entry| entry.auth_status) .unwrap_or(McpAuthStatus::Unsupported) .to_string(); let bearer_token_display = bearer_token_env_var.as_deref().unwrap_or("-").to_string(); http_rows.push([ name.clone(), url.clone(), bearer_token_display, status, auth_status, ]); } } } if !stdio_rows.is_empty() { let mut widths = [ "Name".len(), "Command".len(), "Args".len(), "Env".len(), "Cwd".len(), "Status".len(), "Auth".len(), ]; for row in &stdio_rows { for (i, cell) in row.iter().enumerate() { widths[i] = widths[i].max(cell.len()); } } println!( "{name:<name_w$} {command:<cmd_w$} {args:<args_w$} {env:<env_w$} {cwd:<cwd_w$} {status:<status_w$} {auth:<auth_w$}", name = "Name", command = "Command", args = "Args", env = "Env", cwd = "Cwd", status = "Status", auth = "Auth", name_w = widths[0], cmd_w = widths[1], args_w = widths[2], env_w = widths[3], cwd_w = widths[4], status_w = widths[5], auth_w = widths[6], ); for row in &stdio_rows { println!( "{name:<name_w$} {command:<cmd_w$} {args:<args_w$} {env:<env_w$} {cwd:<cwd_w$} {status:<status_w$} {auth:<auth_w$}", name = row[0].as_str(), command = row[1].as_str(), args = row[2].as_str(), env = row[3].as_str(), cwd = row[4].as_str(), status = row[5].as_str(), auth = row[6].as_str(), name_w = widths[0], cmd_w = widths[1], args_w = widths[2], env_w = widths[3], cwd_w = widths[4], status_w = widths[5], auth_w = widths[6], ); } } if !stdio_rows.is_empty() && !http_rows.is_empty() { println!(); } if !http_rows.is_empty() { let mut widths = [ "Name".len(), "Url".len(), "Bearer Token Env Var".len(), "Status".len(), "Auth".len(), ]; for row in &http_rows { for (i, cell) in row.iter().enumerate() { widths[i] = widths[i].max(cell.len()); } } println!( "{name:<name_w$} {url:<url_w$} {token:<token_w$} {status:<status_w$} {auth:<auth_w$}", name = "Name", url = "Url", token = "Bearer Token Env Var", status = "Status", auth = "Auth", name_w = widths[0], url_w = widths[1], token_w = widths[2], status_w = widths[3], auth_w = widths[4], ); for row in &http_rows { println!( "{name:<name_w$} {url:<url_w$} {token:<token_w$} {status:<status_w$} {auth:<auth_w$}", name = row[0].as_str(), url = row[1].as_str(), token = row[2].as_str(), status = row[3].as_str(), auth = row[4].as_str(), name_w = widths[0], url_w = widths[1], token_w = widths[2], status_w = widths[3], auth_w = widths[4], ); } } Ok(())}async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Result<()> { let overrides = config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?; let config = Config::load_with_cli_overrides(overrides) .await .context("failed to load configuration")?; let mcp_manager = McpManager::new(Arc::new(PluginsManager::new( config.codex_home.to_path_buf(), ))); 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); }; if get_args.json { let transport = match &server.transport { McpServerTransportConfig::Stdio { command, args, env, env_vars, cwd, } => serde_json::json!({ "type": "stdio", "command": command, "args": args, "env": env, "env_vars": env_vars, "cwd": cwd, }), McpServerTransportConfig::StreamableHttp { url, bearer_token_env_var, http_headers, env_http_headers, } => serde_json::json!({ "type": "streamable_http", "url": url, "bearer_token_env_var": bearer_token_env_var, "http_headers": http_headers, "env_http_headers": env_http_headers, }), }; let output = serde_json::to_string_pretty(&serde_json::json!({ "name": get_args.name, "enabled": server.enabled, "disabled_reason": server.disabled_reason.as_ref().map(ToString::to_string), "transport": transport, "enabled_tools": server.enabled_tools.clone(), "disabled_tools": server.disabled_tools.clone(), "startup_timeout_sec": server .startup_timeout_sec .map(|timeout| timeout.as_secs_f64()), "tool_timeout_sec": server .tool_timeout_sec .map(|timeout| timeout.as_secs_f64()), }))?; println!("{output}"); return Ok(()); } if !server.enabled { if let Some(reason) = server.disabled_reason.as_ref() { println!("{name} (disabled: {reason})", name = get_args.name); } else { println!("{name} (disabled)", name = get_args.name); } return Ok(()); } println!("{}", get_args.name); println!(" enabled: {}", server.enabled); let format_tool_list = |tools: &Option<Vec<String>>| -> String { match tools { Some(list) if list.is_empty() => "[]".to_string(), Some(list) => list.join(", "), None => "-".to_string(), } }; if server.enabled_tools.is_some() { let enabled_tools_display = format_tool_list(&server.enabled_tools); println!(" enabled_tools: {enabled_tools_display}"); } if server.disabled_tools.is_some() { let disabled_tools_display = format_tool_list(&server.disabled_tools); println!(" disabled_tools: {disabled_tools_display}"); } match &server.transport { McpServerTransportConfig::Stdio { command, args, env, env_vars, cwd, } => { println!(" transport: stdio"); println!(" command: {command}"); let args_display = if args.is_empty() { "-".to_string() } else { args.join(" ") }; println!(" args: {args_display}"); let cwd_display = cwd .as_ref() .map(|path| path.display().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| "-".to_string()); println!(" cwd: {cwd_display}"); let env_display = format_env_display(env.as_ref(), env_vars); println!(" env: {env_display}"); } McpServerTransportConfig::StreamableHttp { url, bearer_token_env_var, http_headers, env_http_headers, } => { println!(" transport: streamable_http"); println!(" url: {url}"); let bearer_token_display = bearer_token_env_var.as_deref().unwrap_or("-"); println!(" bearer_token_env_var: {bearer_token_display}"); let headers_display = match http_headers { Some(map) if !map.is_empty() => { let mut pairs: Vec<_> = map.iter().collect(); pairs.sort_by_key(|(name, _)| *name); pairs .into_iter() .map(|(k, _)| format!("{k}=*****")) .collect::<Vec<_>>() .join(", ") } _ => "-".to_string(), }; println!(" http_headers: {headers_display}"); let env_headers_display = match env_http_headers { Some(map) if !map.is_empty() => { let mut pairs: Vec<_> = map.iter().collect(); pairs.sort_by_key(|(name, _)| *name); pairs .into_iter() .map(|(k, var)| format!("{k}={var}")) .collect::<Vec<_>>() .join(", ") } _ => "-".to_string(), }; println!(" env_http_headers: {env_headers_display}"); } } if let Some(timeout) = server.startup_timeout_sec { println!(" startup_timeout_sec: {}", timeout.as_secs_f64()); } if let Some(timeout) = server.tool_timeout_sec { println!(" tool_timeout_sec: {}", timeout.as_secs_f64()); } if let Some(approval_mode) = server.default_tools_approval_mode { let approval_mode = match approval_mode { AppToolApproval::Auto => "auto", AppToolApproval::Prompt => "prompt", AppToolApproval::Approve => "approve", }; println!(" default_tools_approval_mode: {approval_mode}"); } println!(" remove: codex mcp remove {}", get_args.name); Ok(())}fn parse_env_pair(raw: &str) -> Result<(String, String), String> { let mut parts = raw.splitn(2, '='); let key = parts .next() .map(str::trim) .filter(|s| !s.is_empty()) .ok_or_else(|| "environment entries must be in KEY=VALUE form".to_string())?; let value = parts .next() .map(str::to_string) .ok_or_else(|| "environment entries must be in KEY=VALUE form".to_string())?; Ok((key.to_string(), value))}fn validate_server_name(name: &str) -> Result<()> { let is_valid = !name.is_empty() && name .chars() .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'); if is_valid { Ok(()) } else { bail!("invalid server name '{name}' (use letters, numbers, '-', '_')"); }}fn format_mcp_status(config: &McpServerConfig) -> String { if config.enabled { "enabled".to_string() } else if let Some(reason) = config.disabled_reason.as_ref() { format!("disabled: {reason}") } else { "disabled".to_string() }}