Move MCP OAuth orchestration downstream

Keep app-server OAuth callsites thin by moving server/runtime-aware OAuth discovery, scope resolution, and runtime HTTP client selection into codex-mcp helpers.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-04-27 10:03:34 +00:00
parent a4de53c661
commit a3abb8ba6e
7 changed files with 249 additions and 170 deletions

View File

@@ -310,11 +310,9 @@ use codex_mcp::McpRuntimeEnvironment;
use codex_mcp::McpServerStatusSnapshot;
use codex_mcp::McpSnapshotDetail;
use codex_mcp::collect_mcp_server_status_snapshot_with_detail;
use codex_mcp::discover_supported_scopes;
use codex_mcp::effective_mcp_servers;
use codex_mcp::http_client_for_server;
use codex_mcp::perform_oauth_login_return_url_for_server;
use codex_mcp::read_mcp_resource as read_mcp_resource_without_thread;
use codex_mcp::resolve_oauth_scopes;
use codex_model_provider::ProviderAccountError;
use codex_model_provider::create_model_provider;
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
@@ -356,7 +354,6 @@ use codex_protocol::protocol::USER_MESSAGE_BEGIN;
use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
use codex_protocol::user_input::UserInput as CoreInputItem;
use codex_rmcp_client::perform_oauth_login_return_url_with_client;
use codex_rollout::state_db::StateDbHandle;
use codex_rollout::state_db::get_state_db;
use codex_rollout::state_db::reconcile_rollout;
@@ -5942,13 +5939,8 @@ impl CodexMessageProcessor {
return;
};
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()),
match &server.transport {
McpServerTransportConfig::StreamableHttp { .. } => {}
_ => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
@@ -5969,39 +5961,16 @@ impl CodexMessageProcessor {
config.cwd.to_path_buf(),
),
};
let http_client = match http_client_for_server(server, runtime_environment) {
Ok(http_client) => http_client,
Err(err) => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("failed to resolve MCP server OAuth environment: {err}"),
data: None,
};
self.outgoing.send_error(request_id, error).await;
return;
}
};
let discovered_scopes = if scopes.is_none() && server.scopes.is_none() {
discover_supported_scopes(&server.transport, http_client.clone()).await
} else {
None
};
let resolved_scopes =
resolve_oauth_scopes(scopes, server.scopes.clone(), discovered_scopes);
match perform_oauth_login_return_url_with_client(
match perform_oauth_login_return_url_for_server(
&name,
&url,
server,
config.mcp_oauth_credentials_store_mode,
http_headers,
env_http_headers,
&resolved_scopes.scopes,
server.oauth_resource.as_deref(),
scopes,
timeout_secs,
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(),
http_client,
runtime_environment,
)
.await
{

View File

@@ -5,14 +5,9 @@ use codex_app_server_protocol::McpServerOauthLoginCompletedNotification;
use codex_app_server_protocol::ServerNotification;
use codex_config::types::McpServerConfig;
use codex_core::config::Config;
use codex_mcp::McpOAuthLoginSupport;
use codex_mcp::McpOAuthLoginOutcome;
use codex_mcp::McpRuntimeEnvironment;
use codex_mcp::http_client_for_server;
use codex_mcp::oauth_login_support;
use codex_mcp::resolve_oauth_scopes;
use codex_mcp::should_retry_without_scopes;
use codex_rmcp_client::perform_oauth_login_silent_with_client;
use tracing::warn;
use codex_mcp::perform_oauth_login_silent_for_server;
use super::CodexMessageProcessor;
@@ -33,33 +28,6 @@ impl CodexMessageProcessor {
config.cwd.to_path_buf(),
),
};
let http_client = match http_client_for_server(&server, runtime_environment) {
Ok(http_client) => http_client,
Err(err) => {
warn!(
"failed to resolve MCP OAuth environment for plugin install {name}: {err}"
);
continue;
}
};
let oauth_config = match oauth_login_support(&server.transport, http_client.clone())
.await
{
McpOAuthLoginSupport::Supported(config) => config,
McpOAuthLoginSupport::Unsupported => continue,
McpOAuthLoginSupport::Unknown(err) => {
warn!(
"MCP server may or may not require login for plugin install {name}: {err}"
);
continue;
}
};
let resolved_scopes = resolve_oauth_scopes(
/*explicit_scopes*/ None,
server.scopes.clone(),
oauth_config.discovered_scopes.clone(),
);
let store_mode = config.mcp_oauth_credentials_store_mode;
let callback_port = config.mcp_oauth_callback_port;
@@ -68,41 +36,20 @@ impl CodexMessageProcessor {
let notification_name = name.clone();
tokio::spawn(async move {
let first_attempt = perform_oauth_login_silent_with_client(
let final_result = perform_oauth_login_silent_for_server(
&name,
&oauth_config.url,
&server,
store_mode,
oauth_config.http_headers.clone(),
oauth_config.env_http_headers.clone(),
&resolved_scopes.scopes,
server.oauth_resource.as_deref(),
/*explicit_scopes*/ None,
callback_port,
callback_url.as_deref(),
http_client.clone(),
runtime_environment,
)
.await;
let final_result = match first_attempt {
Err(err) if should_retry_without_scopes(&resolved_scopes, &err) => {
perform_oauth_login_silent_with_client(
&name,
&oauth_config.url,
store_mode,
oauth_config.http_headers,
oauth_config.env_http_headers,
&[],
server.oauth_resource.as_deref(),
callback_port,
callback_url.as_deref(),
http_client,
)
.await
}
result => result,
};
let (success, error) = match final_result {
Ok(()) => (true, None),
Ok(McpOAuthLoginOutcome::Completed) => (true, None),
Ok(McpOAuthLoginOutcome::Unsupported) => return,
Err(err) => (false, Some(err.to_string())),
};

View File

@@ -16,7 +16,6 @@ use codex_core::config::find_codex_home;
use codex_core::config::load_global_mcp_servers;
use codex_core::plugins::PluginsManager;
use codex_exec_server::Environment;
use codex_exec_server::ReqwestHttpClient;
use codex_mcp::McpOAuthLoginSupport;
use codex_mcp::McpRuntimeEnvironment;
use codex_mcp::ResolvedMcpOAuthScopes;
@@ -326,7 +325,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
println!("Added global MCP server '{name}'.");
match oauth_login_support(&transport, Arc::new(ReqwestHttpClient)).await {
match oauth_login_support(&transport).await {
McpOAuthLoginSupport::Supported(oauth_config) => {
println!("Detected OAuth support. Starting OAuth flow…");
let resolved_scopes = resolve_oauth_scopes(
@@ -420,7 +419,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
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, Arc::new(ReqwestHttpClient)).await
discover_supported_scopes(&server.transport).await
} else {
None
};

View File

@@ -24,13 +24,19 @@ pub use mcp::read_mcp_resource;
pub use mcp::McpAuthStatusEntry;
pub use mcp::McpOAuthLoginConfig;
pub use mcp::McpOAuthLoginOutcome;
pub use mcp::McpOAuthLoginSupport;
pub use mcp::McpOAuthScopesSource;
pub use mcp::ResolvedMcpOAuthScopes;
pub use mcp::compute_auth_statuses;
pub use mcp::discover_supported_scopes;
pub use mcp::discover_supported_scopes_for_server;
pub use mcp::http_client_for_server;
pub use mcp::oauth_login_support;
pub use mcp::oauth_login_support_for_server;
pub use mcp::perform_oauth_login_for_server;
pub use mcp::perform_oauth_login_return_url_for_server;
pub use mcp::perform_oauth_login_silent_for_server;
pub use mcp::resolve_oauth_scopes;
pub use mcp::should_retry_without_scopes;

View File

@@ -9,8 +9,12 @@ use codex_exec_server::ReqwestHttpClient;
use codex_login::CodexAuth;
use codex_protocol::protocol::McpAuthStatus;
use codex_rmcp_client::OAuthProviderError;
use codex_rmcp_client::OauthLoginHandle;
use codex_rmcp_client::determine_streamable_http_auth_status_with_client;
use codex_rmcp_client::discover_streamable_http_oauth_with_client;
use codex_rmcp_client::perform_oauth_login_return_url_with_client;
use codex_rmcp_client::perform_oauth_login_silent_with_client;
use codex_rmcp_client::perform_oauth_login_with_client;
use futures::future::join_all;
use std::sync::Arc;
use tracing::warn;
@@ -54,7 +58,28 @@ pub struct McpAuthStatusEntry {
pub auth_status: McpAuthStatus,
}
pub async fn oauth_login_support(
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum McpOAuthLoginOutcome {
Completed,
Unsupported,
}
pub async fn oauth_login_support(transport: &McpServerTransportConfig) -> McpOAuthLoginSupport {
oauth_login_support_with_client(transport, Arc::new(ReqwestHttpClient)).await
}
pub async fn oauth_login_support_for_server(
config: &McpServerConfig,
runtime_environment: McpRuntimeEnvironment,
) -> McpOAuthLoginSupport {
let http_client = match http_client_for_server(config, runtime_environment) {
Ok(http_client) => http_client,
Err(err) => return McpOAuthLoginSupport::Unknown(err),
};
oauth_login_support_with_client(&config.transport, http_client).await
}
async fn oauth_login_support_with_client(
transport: &McpServerTransportConfig,
http_client: Arc<dyn HttpClient>,
) -> McpOAuthLoginSupport {
@@ -93,9 +118,25 @@ pub async fn oauth_login_support(
pub async fn discover_supported_scopes(
transport: &McpServerTransportConfig,
) -> Option<Vec<String>> {
discover_supported_scopes_with_client(transport, Arc::new(ReqwestHttpClient)).await
}
pub async fn discover_supported_scopes_for_server(
config: &McpServerConfig,
runtime_environment: McpRuntimeEnvironment,
) -> Option<Vec<String>> {
match oauth_login_support_for_server(config, runtime_environment).await {
McpOAuthLoginSupport::Supported(config) => config.discovered_scopes,
McpOAuthLoginSupport::Unsupported | McpOAuthLoginSupport::Unknown(_) => None,
}
}
async fn discover_supported_scopes_with_client(
transport: &McpServerTransportConfig,
http_client: Arc<dyn HttpClient>,
) -> Option<Vec<String>> {
match oauth_login_support(transport, http_client).await {
match oauth_login_support_with_client(transport, http_client).await {
McpOAuthLoginSupport::Supported(config) => config.discovered_scopes,
McpOAuthLoginSupport::Unsupported | McpOAuthLoginSupport::Unknown(_) => None,
}
@@ -140,6 +181,172 @@ pub fn should_retry_without_scopes(scopes: &ResolvedMcpOAuthScopes, error: &anyh
&& error.downcast_ref::<OAuthProviderError>().is_some()
}
#[allow(clippy::too_many_arguments)]
pub async fn perform_oauth_login_return_url_for_server(
server_name: &str,
config: &McpServerConfig,
store_mode: OAuthCredentialsStoreMode,
explicit_scopes: Option<Vec<String>>,
timeout_secs: Option<i64>,
callback_port: Option<u16>,
callback_url: Option<&str>,
runtime_environment: McpRuntimeEnvironment,
) -> Result<OauthLoginHandle> {
let McpServerTransportConfig::StreamableHttp {
url,
http_headers,
env_http_headers,
..
} = &config.transport
else {
anyhow::bail!("OAuth login is only supported for streamable HTTP servers.");
};
let http_client = http_client_for_server(config, runtime_environment)?;
let discovered_scopes = if explicit_scopes.is_none() && config.scopes.is_none() {
discover_supported_scopes_with_client(&config.transport, http_client.clone()).await
} else {
None
};
let resolved_scopes =
resolve_oauth_scopes(explicit_scopes, config.scopes.clone(), discovered_scopes);
perform_oauth_login_return_url_with_client(
server_name,
url,
store_mode,
http_headers.clone(),
env_http_headers.clone(),
&resolved_scopes.scopes,
config.oauth_resource.as_deref(),
timeout_secs,
callback_port,
callback_url,
http_client,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn perform_oauth_login_silent_for_server(
server_name: &str,
config: &McpServerConfig,
store_mode: OAuthCredentialsStoreMode,
explicit_scopes: Option<Vec<String>>,
callback_port: Option<u16>,
callback_url: Option<&str>,
runtime_environment: McpRuntimeEnvironment,
) -> Result<McpOAuthLoginOutcome> {
let http_client = http_client_for_server(config, runtime_environment)?;
let oauth_config =
match oauth_login_support_with_client(&config.transport, http_client.clone()).await {
McpOAuthLoginSupport::Supported(config) => config,
McpOAuthLoginSupport::Unsupported => return Ok(McpOAuthLoginOutcome::Unsupported),
McpOAuthLoginSupport::Unknown(err) => return Err(err),
};
let resolved_scopes = resolve_oauth_scopes(
explicit_scopes,
config.scopes.clone(),
oauth_config.discovered_scopes.clone(),
);
let first_attempt = perform_oauth_login_silent_with_client(
server_name,
&oauth_config.url,
store_mode,
oauth_config.http_headers.clone(),
oauth_config.env_http_headers.clone(),
&resolved_scopes.scopes,
config.oauth_resource.as_deref(),
callback_port,
callback_url,
http_client.clone(),
)
.await;
let final_result = match first_attempt {
Err(err) if should_retry_without_scopes(&resolved_scopes, &err) => {
perform_oauth_login_silent_with_client(
server_name,
&oauth_config.url,
store_mode,
oauth_config.http_headers,
oauth_config.env_http_headers,
&[],
config.oauth_resource.as_deref(),
callback_port,
callback_url,
http_client,
)
.await
}
result => result,
};
final_result.map(|()| McpOAuthLoginOutcome::Completed)
}
#[allow(clippy::too_many_arguments)]
pub async fn perform_oauth_login_for_server(
server_name: &str,
config: &McpServerConfig,
store_mode: OAuthCredentialsStoreMode,
explicit_scopes: Option<Vec<String>>,
callback_port: Option<u16>,
callback_url: Option<&str>,
runtime_environment: McpRuntimeEnvironment,
) -> Result<McpOAuthLoginOutcome> {
let http_client = http_client_for_server(config, runtime_environment)?;
let oauth_config =
match oauth_login_support_with_client(&config.transport, http_client.clone()).await {
McpOAuthLoginSupport::Supported(config) => config,
McpOAuthLoginSupport::Unsupported => return Ok(McpOAuthLoginOutcome::Unsupported),
McpOAuthLoginSupport::Unknown(err) => return Err(err),
};
let resolved_scopes = resolve_oauth_scopes(
explicit_scopes,
config.scopes.clone(),
oauth_config.discovered_scopes.clone(),
);
let first_attempt = perform_oauth_login_with_client(
server_name,
&oauth_config.url,
store_mode,
oauth_config.http_headers.clone(),
oauth_config.env_http_headers.clone(),
&resolved_scopes.scopes,
config.oauth_resource.as_deref(),
callback_port,
callback_url,
http_client.clone(),
)
.await;
let final_result = match first_attempt {
Err(err) if should_retry_without_scopes(&resolved_scopes, &err) => {
perform_oauth_login_with_client(
server_name,
&oauth_config.url,
store_mode,
oauth_config.http_headers,
oauth_config.env_http_headers,
&[],
config.oauth_resource.as_deref(),
callback_port,
callback_url,
http_client,
)
.await
}
result => result,
};
final_result.map(|()| McpOAuthLoginOutcome::Completed)
}
pub async fn compute_auth_statuses<'a, I>(
servers: I,
store_mode: OAuthCredentialsStoreMode,

View File

@@ -1,12 +1,18 @@
pub use auth::McpAuthStatusEntry;
pub use auth::McpOAuthLoginConfig;
pub use auth::McpOAuthLoginOutcome;
pub use auth::McpOAuthLoginSupport;
pub use auth::McpOAuthScopesSource;
pub use auth::ResolvedMcpOAuthScopes;
pub use auth::compute_auth_statuses;
pub use auth::discover_supported_scopes;
pub use auth::discover_supported_scopes_for_server;
pub use auth::http_client_for_server;
pub use auth::oauth_login_support;
pub use auth::oauth_login_support_for_server;
pub use auth::perform_oauth_login_for_server;
pub use auth::perform_oauth_login_return_url_for_server;
pub use auth::perform_oauth_login_silent_for_server;
pub use auth::resolve_oauth_scopes;
pub use auth::should_retry_without_scopes;

View File

@@ -11,7 +11,6 @@ use codex_protocol::request_user_input::RequestUserInputArgs;
use codex_protocol::request_user_input::RequestUserInputQuestion;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_rmcp_client::perform_oauth_login_with_client;
use tokio_util::sync::CancellationToken;
use tracing::warn;
@@ -19,13 +18,10 @@ use crate::SkillMetadata;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::skills::model::SkillToolDependency;
use codex_mcp::McpOAuthLoginSupport;
use codex_mcp::McpOAuthLoginOutcome;
use codex_mcp::McpRuntimeEnvironment;
use codex_mcp::http_client_for_server;
use codex_mcp::mcp_permission_prompt_is_auto_approved;
use codex_mcp::oauth_login_support;
use codex_mcp::resolve_oauth_scopes;
use codex_mcp::should_retry_without_scopes;
use codex_mcp::perform_oauth_login_for_server;
const SKILL_MCP_DEPENDENCY_PROMPT_ID: &str = "skill_mcp_dependency_install";
const MCP_DEPENDENCY_OPTION_INSTALL: &str = "Install";
@@ -135,23 +131,6 @@ pub(crate) async fn maybe_install_mcp_dependencies(
.unwrap_or_else(|| sess.services.environment_manager.local_environment()),
turn_context.cwd.to_path_buf(),
);
let http_client = match http_client_for_server(&server_config, runtime_environment) {
Ok(http_client) => http_client,
Err(err) => {
warn!("failed to resolve MCP OAuth environment for dependency {name}: {err}");
continue;
}
};
let oauth_config =
match oauth_login_support(&server_config.transport, http_client.clone()).await {
McpOAuthLoginSupport::Supported(config) => config,
McpOAuthLoginSupport::Unsupported => continue,
McpOAuthLoginSupport::Unknown(err) => {
warn!("MCP server may or may not require login for dependency {name}: {err}");
continue;
}
};
sess.notify_background_event(
turn_context,
format!(
@@ -160,54 +139,20 @@ pub(crate) async fn maybe_install_mcp_dependencies(
)
.await;
let resolved_scopes = resolve_oauth_scopes(
/*explicit_scopes*/ None,
server_config.scopes.clone(),
oauth_config.discovered_scopes.clone(),
);
let first_attempt = perform_oauth_login_with_client(
match perform_oauth_login_for_server(
&name,
&oauth_config.url,
&server_config,
config.mcp_oauth_credentials_store_mode,
oauth_config.http_headers.clone(),
oauth_config.env_http_headers.clone(),
&resolved_scopes.scopes,
server_config.oauth_resource.as_deref(),
/*explicit_scopes*/ None,
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(),
http_client.clone(),
runtime_environment,
)
.await;
if let Err(err) = first_attempt {
if should_retry_without_scopes(&resolved_scopes, &err) {
sess.notify_background_event(
turn_context,
format!(
"Retrying MCP {name} authentication without scopes after provider rejection."
),
)
.await;
if let Err(err) = perform_oauth_login_with_client(
&name,
&oauth_config.url,
config.mcp_oauth_credentials_store_mode,
oauth_config.http_headers,
oauth_config.env_http_headers,
&[],
server_config.oauth_resource.as_deref(),
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(),
http_client,
)
.await
{
warn!("failed to login to MCP dependency {name}: {err}");
}
} else {
warn!("failed to login to MCP dependency {name}: {err}");
}
.await
{
Ok(McpOAuthLoginOutcome::Completed) => {}
Ok(McpOAuthLoginOutcome::Unsupported) => {}
Err(err) => warn!("failed to login to MCP dependency {name}: {err}"),
}
}