mcp: surface profile migration guidance under --profile (#23890)

## Why

`codex --profile <name> mcp ...` should reach the same profile-v2
migration guard as runtime commands. Otherwise legacy
`[profiles.<name>]` users see the generic command-scope rejection
instead of the existing guidance to move settings into
`$CODEX_HOME/<name>.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`
This commit is contained in:
jif-oai
2026-05-22 10:40:33 +02:00
committed by GitHub
Unverified
parent b14f11d3d2
commit ed80e5f558
3 changed files with 56 additions and 3 deletions
+11 -2
View File
@@ -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]
+23 -1
View File
@@ -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
+22
View File
@@ -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()?;