diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 0a5be0dc23..b753fbee98 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -17,6 +17,7 @@ use codex_core::mcp::auth::compute_auth_statuses; use codex_core::protocol::McpAuthStatus; use codex_rmcp_client::delete_oauth_tokens; use codex_rmcp_client::perform_oauth_login; +use codex_rmcp_client::supports_oauth_login; /// [experimental] Launch Codex as an MCP server or manage configured MCP servers. /// @@ -189,7 +190,10 @@ impl McpCli { async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<()> { // Validate any provided overrides even though they are not currently applied. - config_overrides.parse_overrides().map_err(|e| anyhow!(e))?; + let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?; + let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default()) + .await + .context("failed to load configuration")?; let AddArgs { name, @@ -225,17 +229,21 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re } } AddMcpTransportArgs { - streamable_http: Some(streamable_http), + streamable_http: + Some(AddMcpStreamableHttpArgs { + url, + bearer_token_env_var, + }), .. } => McpServerTransportConfig::StreamableHttp { - url: streamable_http.url, - bearer_token_env_var: streamable_http.bearer_token_env_var, + url, + bearer_token_env_var, }, AddMcpTransportArgs { .. } => bail!("exactly one of --command or --url must be provided"), }; let new_entry = McpServerConfig { - transport, + transport: transport.clone(), enabled: true, startup_timeout_sec: None, tool_timeout_sec: None, @@ -248,6 +256,17 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re println!("Added global MCP server '{name}'."); + if let McpServerTransportConfig::StreamableHttp { + url, + bearer_token_env_var: None, + } = transport + && matches!(supports_oauth_login(&url).await, Ok(true)) + { + println!("Detected OAuth support. Starting OAuth flow…"); + perform_oauth_login(&name, &url, config.mcp_oauth_credentials_store_mode).await?; + println!("Successfully logged in."); + } + Ok(()) } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 22b34b1cd0..68854aee18 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -17,6 +17,7 @@ use codex_apply_patch::ApplyPatchAction; use codex_protocol::ConversationId; use codex_protocol::protocol::ConversationPathResponseEvent; use codex_protocol::protocol::ExitedReviewModeEvent; +use codex_protocol::protocol::McpAuthStatus; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionSource; @@ -370,10 +371,25 @@ impl Session { ); let default_shell_fut = shell::default_user_shell(); let history_meta_fut = crate::message_history::history_metadata(&config); + let auth_statuses_fut = compute_auth_statuses( + config.mcp_servers.iter(), + config.mcp_oauth_credentials_store_mode, + ); // Join all independent futures. - let (rollout_recorder, mcp_res, default_shell, (history_log_id, history_entry_count)) = - tokio::join!(rollout_fut, mcp_fut, default_shell_fut, history_meta_fut); + let ( + rollout_recorder, + mcp_res, + default_shell, + (history_log_id, history_entry_count), + auth_statuses, + ) = tokio::join!( + rollout_fut, + mcp_fut, + default_shell_fut, + history_meta_fut, + auth_statuses_fut + ); let rollout_recorder = rollout_recorder.map_err(|e| { error!("failed to initialize rollout recorder: {e:#}"); @@ -400,11 +416,24 @@ impl Session { // Surface individual client start-up failures to the user. if !failed_clients.is_empty() { for (server_name, err) in failed_clients { - let message = format!("MCP client for `{server_name}` failed to start: {err:#}"); - error!("{message}"); + let log_message = + format!("MCP client for `{server_name}` failed to start: {err:#}"); + error!("{log_message}"); + let display_message = if matches!( + auth_statuses.get(&server_name), + Some(McpAuthStatus::NotLoggedIn) + ) { + format!( + "The {server_name} MCP server is not logged in. Run `codex mcp login {server_name}` to log in." + ) + } else { + log_message + }; post_session_configured_error_events.push(Event { id: INITIAL_SUBMIT_ID.to_owned(), - msg: EventMsg::Error(ErrorEvent { message }), + msg: EventMsg::Error(ErrorEvent { + message: display_message, + }), }); } } diff --git a/codex-rs/rmcp-client/src/auth_status.rs b/codex-rs/rmcp-client/src/auth_status.rs index 0281c0ffe8..5e32eed485 100644 --- a/codex-rs/rmcp-client/src/auth_status.rs +++ b/codex-rs/rmcp-client/src/auth_status.rs @@ -44,7 +44,7 @@ pub async fn determine_streamable_http_auth_status( } /// Attempt to determine whether a streamable HTTP MCP server advertises OAuth login. -async fn supports_oauth_login(url: &str) -> Result { +pub async fn supports_oauth_login(url: &str) -> Result { let base_url = Url::parse(url)?; let client = Client::builder().timeout(DISCOVERY_TIMEOUT).build()?; diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs index 05412da184..ca99a7bb90 100644 --- a/codex-rs/rmcp-client/src/lib.rs +++ b/codex-rs/rmcp-client/src/lib.rs @@ -7,6 +7,7 @@ mod rmcp_client; mod utils; pub use auth_status::determine_streamable_http_auth_status; +pub use auth_status::supports_oauth_login; pub use codex_protocol::protocol::McpAuthStatus; pub use oauth::OAuthCredentialsStoreMode; pub use oauth::StoredOAuthTokens;