From ed80e5f5583d85e6f61d6839842c50b5c0630d1d Mon Sep 17 00:00:00 2001 From: jif-oai Date: Fri, 22 May 2026 10:40:33 +0200 Subject: [PATCH] mcp: surface profile migration guidance under --profile (#23890) ## Why `codex --profile mcp ...` should reach the same profile-v2 migration guard as runtime commands. Otherwise legacy `[profiles.]` users see the generic command-scope rejection instead of the existing guidance to move settings into `$CODEX_HOME/.config.toml`. ## What - Allow `codex mcp` through the `--profile` subcommand gate. - Pass profile loader overrides into the MCP entry point only to validate profile-v2 migration when a profile is present. - Keep MCP add/remove/list/get/login/logout behavior otherwise unchanged; this does not add profile-scoped MCP server management. - Cover the legacy profile migration error for `codex --profile work mcp list`. ## Testing - `cargo test -p codex-cli` --- codex-rs/cli/src/main.rs | 13 +++++++++++-- codex-rs/cli/src/mcp_cmd.rs | 24 +++++++++++++++++++++++- codex-rs/cli/tests/mcp_add_remove.rs | 22 ++++++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 4382dee292..929d1b8654 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -918,7 +918,9 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { )?; // Propagate any root-level config overrides (e.g. `-c key=value`). prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone()); - mcp_cli.run().await?; + let loader_overrides = + loader_overrides_for_profile(interactive.config_profile_v2.as_ref())?; + mcp_cli.run(loader_overrides).await?; } Some(Subcommand::Plugin(plugin_cli)) => { reject_remote_mode_for_subcommand( @@ -1459,11 +1461,12 @@ fn profile_v2_for_subcommand<'a>( | Subcommand::Review(_) | Subcommand::Resume(_) | Subcommand::Fork(_) + | Subcommand::Mcp(_) | Subcommand::Debug(DebugCommand { subcommand: DebugSubcommand::PromptInput(_), }) => Ok(Some(profile_v2)), _ => anyhow::bail!( - "--profile only applies to runtime commands: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, and `codex debug prompt-input`." + "--profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, `codex mcp`, and `codex debug prompt-input`." ), } } @@ -2264,6 +2267,12 @@ mod tests { .as_deref(), Some("work") ); + assert_eq!( + profile_v2_for_args(&["codex", "--profile", "work", "mcp", "list"]) + .expect("mcp supports profile-v2") + .as_deref(), + Some("work") + ); } #[test] diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index f52d24160b..3104262c23 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -11,6 +11,8 @@ use codex_config::types::McpServerConfig; 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; @@ -157,12 +159,16 @@ pub struct LogoutArgs { } impl McpCli { - pub async fn run(self) -> Result<()> { + 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?; @@ -239,6 +245,22 @@ async fn perform_oauth_login_retry_without_scopes( } } +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 diff --git a/codex-rs/cli/tests/mcp_add_remove.rs b/codex-rs/cli/tests/mcp_add_remove.rs index 15afaf0828..ecfbed1264 100644 --- a/codex-rs/cli/tests/mcp_add_remove.rs +++ b/codex-rs/cli/tests/mcp_add_remove.rs @@ -68,6 +68,28 @@ async fn add_and_remove_server_updates_global_config() -> Result<()> { Ok(()) } +#[tokio::test] +async fn profile_mcp_reports_legacy_profile_migration() -> Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("config.toml"), + r#"[profiles.work] +model = "gpt-5" +"#, + )?; + + let mut list_cmd = codex_command(codex_home.path())?; + list_cmd + .args(["--profile", "work", "mcp", "list"]) + .assert() + .failure() + .stderr(contains("--profile `work` cannot be used")) + .stderr(contains("[profiles.work]")) + .stderr(contains("work.config.toml")); + + Ok(()) +} + #[tokio::test] async fn add_with_env_preserves_key_order_and_values() -> Result<()> { let codex_home = TempDir::new()?;