Support OAuth options in codex mcp add (#24120)

## Summary
- add `--oauth-client-id` and `--oauth-resource` options for streamable
HTTP `codex mcp add` registrations
- persist those options in MCP server config and use them during the
immediate OAuth login flow
- cover add-time serialization of both OAuth options in the CLI
integration tests

## Testing
- `just fmt`
- `cargo test -p codex-cli`
- `just fix -p codex-cli`
This commit is contained in:
Matthew Zeng
2026-05-22 13:21:01 -07:00
committed by GitHub
parent 3c83e57bfa
commit 6963145cb6
2 changed files with 77 additions and 18 deletions

View File

@@ -8,6 +8,7 @@ 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;
@@ -134,6 +135,14 @@ pub struct AddMcpStreamableHttpArgs {
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)]
@@ -282,7 +291,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
.await
.with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?;
let transport = match transport_args {
let (transport, oauth_client_id, oauth_resource) = match transport_args {
AddMcpTransportArgs {
stdio: Some(stdio), ..
} => {
@@ -297,27 +306,37 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
} 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,
}
(
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,
},
} => (
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"),
};
@@ -334,8 +353,12 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth: None,
oauth_resource: None,
oauth: oauth_client_id
.clone()
.map(|client_id| McpServerOAuthConfig {
client_id: Some(client_id),
}),
oauth_resource: oauth_resource.clone(),
tools: HashMap::new(),
};
@@ -364,8 +387,8 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
oauth_config.http_headers,
oauth_config.env_http_headers,
&resolved_scopes,
/*oauth_client_id*/ None,
/*oauth_resource*/ None,
oauth_client_id.as_deref(),
oauth_resource.as_deref(),
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(),
)

View File

@@ -198,6 +198,42 @@ async fn add_streamable_http_with_custom_env_var() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn add_streamable_http_with_oauth_options() -> Result<()> {
let codex_home = TempDir::new()?;
let mut add_cmd = codex_command(codex_home.path())?;
add_cmd
.args([
"mcp",
"add",
"oauth-server",
"--url",
"https://example.com/mcp",
"--oauth-client-id",
"eci-prd-pub-codex-123",
"--oauth-resource",
"https://resource.example.com",
])
.assert()
.success();
let servers = load_global_mcp_servers(codex_home.path()).await?;
let oauth_server = servers
.get("oauth-server")
.expect("oauth server should exist");
assert_eq!(
oauth_server.oauth_client_id(),
Some("eci-prd-pub-codex-123")
);
assert_eq!(
oauth_server.oauth_resource.as_deref(),
Some("https://resource.example.com")
);
Ok(())
}
#[tokio::test]
async fn add_streamable_http_rejects_removed_flag() -> Result<()> {
let codex_home = TempDir::new()?;