From 2e156cbe31212ccfa9173cb2679c1a201a940b18 Mon Sep 17 00:00:00 2001 From: jif Date: Mon, 13 Jul 2026 15:41:09 +0000 Subject: [PATCH] Avoid blocking thread startup on MCP OAuth discovery (#32825) ## Why OAuth metadata discovery for an optional HTTP MCP server can stall, delaying `thread/start` even though that server is not required. ## What changed - Remove eager MCP authentication-status discovery from session and connection manager startup. - When an MCP connection reports an authentication error, inspect configured and stored credentials locally to preserve login and reauthentication failure reasons without making another discovery request. - Continue using server configuration directly for startup error messages and timeout hints. ## Testing Add an app-server regression test with an optional HTTP MCP endpoint that accepts a connection but never responds, and verify that `thread/start` completes without waiting for its configured startup timeout. GitOrigin-RevId: 56fe0188cc1ceaf746b0b65e8b7dd1fd2e13420d --- .../app-server/tests/suite/v2/thread_start.rs | 82 +++++++++++++ codex-rs/codex-mcp/src/connection_manager.rs | 88 +++++++++----- .../codex-mcp/src/connection_manager_tests.rs | 109 ++++++++---------- codex-rs/codex-mcp/src/mcp/mod.rs | 10 -- codex-rs/core/src/connectors.rs | 10 -- codex-rs/core/src/mcp_tool_call_tests.rs | 1 - codex-rs/core/src/session/mcp.rs | 9 -- codex-rs/core/src/session/mod.rs | 1 - codex-rs/core/src/session/session.rs | 20 +--- codex-rs/core/tests/suite/mcp_auth_refresh.rs | 1 - codex-rs/rmcp-client/src/auth_status.rs | 26 +++++ codex-rs/rmcp-client/src/lib.rs | 1 + 12 files changed, 219 insertions(+), 139 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index 50c8e784c3..66c45a9fb5 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -44,6 +44,8 @@ use serde_json::json; use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; +use tokio::net::TcpListener; +use tokio::sync::oneshot; use tokio::time::timeout; use wiremock::Mock; use wiremock::MockServer; @@ -1250,6 +1252,55 @@ async fn thread_start_emits_mcp_server_status_updated_notifications() -> Result< Ok(()) } +#[tokio::test] +async fn thread_start_does_not_wait_for_optional_http_mcp_auth_discovery() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let listener = TcpListener::bind("127.0.0.1:0").await?; + let mcp_addr = listener.local_addr()?; + let (connection_started_tx, connection_started_rx) = oneshot::channel(); + let blackhole_server = tokio::spawn(async move { + let Ok((connection, _)) = listener.accept().await else { + return; + }; + let _ = connection_started_tx.send(()); + let _connection = connection; + std::future::pending::<()>().await; + }); + + let codex_home = TempDir::new()?; + create_config_toml_with_optional_http_mcp( + codex_home.path(), + &server.uri(), + &format!("http://{mcp_addr}/mcp"), + )?; + + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .build() + .await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let req_id = mcp + .send_thread_start_request_with_auto_env(ThreadStartParams::default()) + .await?; + + timeout(DEFAULT_READ_TIMEOUT, connection_started_rx) + .await + .context("optional HTTP MCP never attempted a connection")??; + let response = timeout( + std::time::Duration::from_secs(3), + mcp.read_stream_until_response_message(RequestId::Integer(req_id)), + ) + .await + .context("thread/start waited for optional HTTP MCP auth discovery"); + blackhole_server.abort(); + let response: JSONRPCResponse = response??; + let response: ThreadStartResponse = to_response(response)?; + + assert!(!response.thread.id.is_empty()); + Ok(()) +} + #[tokio::test] async fn thread_start_surfaces_cloud_config_bundle_load_errors() -> Result<()> { let server = MockServer::start().await; @@ -1735,6 +1786,37 @@ stream_max_retries = 0 ) } +fn create_config_toml_with_optional_http_mcp( + codex_home: &Path, + server_uri: &str, + mcp_uri: &str, +) -> std::io::Result<()> { + let config_toml = codex_home.join("config.toml"); + std::fs::write( + config_toml, + format!( + r#" +model = "mock-model" +approval_policy = "never" +sandbox_mode = "read-only" + +model_provider = "mock_provider" + +[model_providers.mock_provider] +name = "Mock provider for test" +base_url = "{server_uri}/v1" +wire_api = "responses" +request_max_retries = 0 +stream_max_retries = 0 + +[mcp_servers.optional_http] +url = "{mcp_uri}" +startup_timeout_sec = 60 +"# + ), + ) +} + #[cfg(target_os = "windows")] fn broken_mcp_transport_toml() -> &'static str { r#"command = "cmd" diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index b8e58e7ae9..fbd5e59d87 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -13,7 +13,6 @@ use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; -use crate::McpAuthStatusEntry; use crate::codex_apps::prepare_openai_file_params_for_model; use crate::elicitation::ElicitationRequestManager; use crate::elicitation::ElicitationRequestRouter; @@ -41,6 +40,7 @@ use async_channel::Sender; use codex_api::SharedAuthProvider; use codex_config::Constrained; use codex_config::McpServerAuth; +use codex_config::McpServerConfig; use codex_config::McpServerTransportConfig; use codex_config::types::AuthKeyringBackendKind; use codex_config::types::OAuthCredentialsStoreMode; @@ -63,6 +63,7 @@ use codex_protocol::protocol::McpStartupUpdateEvent; use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::McpAuthState; use codex_rmcp_client::McpLoginRequirement; +use codex_rmcp_client::determine_streamable_http_auth_status_from_credentials; use rmcp::model::ElicitationCapability; use rmcp::model::ListResourceTemplatesResult; use rmcp::model::ListResourcesResult; @@ -127,7 +128,6 @@ impl McpConnectionManager { mcp_servers: &HashMap, store_mode: OAuthCredentialsStoreMode, keyring_backend_kind: AuthKeyringBackendKind, - auth_entries: HashMap, approval_policy: &Constrained, submit_id: String, tx_event: Sender, @@ -189,18 +189,20 @@ impl McpConnectionManager { }, ) .await; - let configured_config = server.configured_config(); + let configured_config = server.configured_config().cloned(); // For built-in Codex Apps, `CODEX_CONNECTORS_TOKEN` is a debug // override: it supplies runtime auth but bypasses the shared tools // cache. let uses_env_bearer_token = - configured_config.is_some_and(|config| match &config.transport { - McpServerTransportConfig::StreamableHttp { - bearer_token_env_var, - .. - } => bearer_token_env_var.is_some(), - McpServerTransportConfig::Stdio { .. } => false, - }); + configured_config + .as_ref() + .is_some_and(|config| match &config.transport { + McpServerTransportConfig::StreamableHttp { + bearer_token_env_var, + .. + } => bearer_token_env_var.is_some(), + McpServerTransportConfig::Stdio { .. } => false, + }); let shares_codex_apps_tools_cache = should_share_codex_apps_tools_cache(&server_name, uses_env_bearer_token); let codex_apps_tools_cache_context = shares_codex_apps_tools_cache.then(|| { @@ -226,6 +228,7 @@ impl McpConnectionManager { } else { chatgpt_auth_provider_for_server(&server, chatgpt_auth_provider) }; + let has_runtime_auth = runtime_auth_provider.is_some(); let async_managed_client = AsyncManagedClient::new( server_name.clone(), startup_submit_id.clone(), @@ -245,20 +248,55 @@ impl McpConnectionManager { clients.insert(server_name.clone(), async_managed_client.clone()); let tx_event = tx_event.clone(); let submit_id = startup_submit_id.clone(); - let auth_entry = auth_entries.get(&server_name).cloned(); join_set.spawn(async move { let mut outcome = async_managed_client.client().await; if cancel_token.is_cancelled() { outcome = Err(StartupOutcomeError::Cancelled); } + let auth_state = match &outcome { + Err(error) if error.is_authentication_required() && !has_runtime_auth => { + configured_config.as_ref().and_then(|config| { + let McpServerTransportConfig::StreamableHttp { + url, + bearer_token_env_var, + http_headers, + env_http_headers, + } = &config.transport + else { + return None; + }; + match determine_streamable_http_auth_status_from_credentials( + &server_name, + url, + bearer_token_env_var.as_deref(), + http_headers.clone(), + env_http_headers.clone(), + store_mode, + keyring_backend_kind, + ) { + Ok(auth_state) => auth_state, + Err(error) => { + warn!( + "failed to read stored auth status for MCP server `{server_name}`: {error:?}" + ); + None + } + } + }) + } + Ok(_) | Err(_) => None, + }; + if cancel_token.is_cancelled() { + outcome = Err(StartupOutcomeError::Cancelled); + } let status = match &outcome { Ok(_) => McpStartupStatus::Ready, Err(StartupOutcomeError::Cancelled) => McpStartupStatus::Cancelled, Err(error) => { - let reason = mcp_startup_failure_reason(auth_entry.as_ref(), error); + let reason = mcp_startup_failure_reason(auth_state, error); let error_str = mcp_init_error_display( server_name.as_str(), - auth_entry.as_ref(), + configured_config.as_ref(), error, ); McpStartupStatus::Failed { @@ -960,14 +998,14 @@ async fn emit_update( } fn mcp_startup_failure_reason( - entry: Option<&McpAuthStatusEntry>, + auth_state: Option, error: &StartupOutcomeError, ) -> Option { if !error.is_authentication_required() { return None; } - match entry.map(|entry| entry.auth_state) { + match auth_state { Some(McpAuthState::LoggedOut(McpLoginRequirement::Reauthentication)) => { Some(McpStartupFailureReason::ReauthenticationRequired) } @@ -983,7 +1021,7 @@ fn mcp_startup_failure_reason( fn mcp_init_error_display( server_name: &str, - entry: Option<&McpAuthStatusEntry>, + config: Option<&McpServerConfig>, err: &StartupOutcomeError, ) -> String { if let Some(McpServerTransportConfig::StreamableHttp { @@ -991,7 +1029,7 @@ fn mcp_init_error_display( bearer_token_env_var, http_headers, .. - }) = entry.and_then(|entry| entry.config.as_ref().map(|config| &config.transport)) + }) = config.map(|config| &config.transport) && url == "https://api.githubcopilot.com/mcp/" && bearer_token_env_var.is_none() && http_headers.as_ref().map(HashMap::is_empty).unwrap_or(true) @@ -1004,18 +1042,10 @@ fn mcp_init_error_display( "The {server_name} MCP server is not logged in. Run `codex mcp login {server_name}`." ) } else if is_mcp_client_startup_timeout_error(err) { - let startup_timeout_secs = match entry { - Some(entry) => match entry - .config - .as_ref() - .and_then(|config| config.startup_timeout_sec) - { - Some(timeout) => timeout, - None => DEFAULT_STARTUP_TIMEOUT, - }, - None => DEFAULT_STARTUP_TIMEOUT, - } - .as_secs(); + let startup_timeout_secs = config + .and_then(|config| config.startup_timeout_sec) + .unwrap_or(DEFAULT_STARTUP_TIMEOUT) + .as_secs(); format!( "MCP client for `{server_name}` timed out after {startup_timeout_secs} seconds. Add or adjust `startup_timeout_sec` in your config.toml:\n[mcp_servers.{server_name}]\nstartup_timeout_sec = XX" ) diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 1d330a29b1..dece950259 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -1492,7 +1492,6 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() { &mcp_servers, OAuthCredentialsStoreMode::default(), AuthKeyringBackendKind::default(), - HashMap::new(), &approval_policy, String::new(), tx_event, @@ -1570,35 +1569,32 @@ fn elicitation_capability_advertises_url_support_when_enabled() { #[test] fn mcp_init_error_display_prompts_for_github_pat() { let server_name = "github"; - let entry = McpAuthStatusEntry { - config: Some(McpServerConfig { - auth: Default::default(), - transport: McpServerTransportConfig::StreamableHttp { - url: "https://api.githubcopilot.com/mcp/".to_string(), - bearer_token_env_var: None, - http_headers: None, - env_http_headers: None, - }, - environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), - enabled: true, - required: false, - supports_parallel_tool_calls: false, - disabled_reason: None, - startup_timeout_sec: None, - tool_timeout_sec: None, - default_tools_approval_mode: None, - enabled_tools: None, - disabled_tools: None, - scopes: None, - oauth: None, - oauth_resource: None, - tools: HashMap::new(), - }), - auth_state: McpAuthState::Unsupported, + let config = McpServerConfig { + auth: Default::default(), + transport: McpServerTransportConfig::StreamableHttp { + url: "https://api.githubcopilot.com/mcp/".to_string(), + bearer_token_env_var: None, + http_headers: None, + env_http_headers: None, + }, + environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), + enabled: true, + required: false, + supports_parallel_tool_calls: false, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + default_tools_approval_mode: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + oauth: None, + oauth_resource: None, + tools: HashMap::new(), }; let err: StartupOutcomeError = anyhow::anyhow!("OAuth is unsupported").into(); - let display = mcp_init_error_display(server_name, Some(&entry), &err); + let display = mcp_init_error_display(server_name, Some(&config), &err); let expected = format!( "GitHub MCP does not support OAuth. Log in by adding a personal access token (https://github.com/settings/personal-access-tokens) to your environment and config.toml:\n[mcp_servers.{server_name}]\nbearer_token_env_var = CODEX_GITHUB_PERSONAL_ACCESS_TOKEN" @@ -1612,7 +1608,7 @@ fn mcp_init_error_display_prompts_for_login_when_auth_required() { let server_name = "example"; let err: StartupOutcomeError = anyhow::anyhow!("Auth required for server").into(); - let display = mcp_init_error_display(server_name, /*entry*/ None, &err); + let display = mcp_init_error_display(server_name, /*config*/ None, &err); let expected = format!( "The {server_name} MCP server is not logged in. Run `codex mcp login {server_name}`." @@ -1648,17 +1644,13 @@ fn mcp_startup_failure_reason_requires_existing_oauth_and_auth_failure() { (Some(McpAuthState::OAuth), true, None), (None, true, None), ] { - let entry = auth_state.map(|auth_state| McpAuthStatusEntry { - config: None, - auth_state, - }); let error = StartupOutcomeError::Failed { error: "startup failed".to_string(), is_authentication_required, }; assert_eq!( - mcp_startup_failure_reason(entry.as_ref(), &error), + mcp_startup_failure_reason(auth_state, &error), expected, "auth_state={auth_state:?}, is_authentication_required={is_authentication_required}" ); @@ -1668,35 +1660,32 @@ fn mcp_startup_failure_reason_requires_existing_oauth_and_auth_failure() { #[test] fn mcp_init_error_display_reports_generic_errors() { let server_name = "custom"; - let entry = McpAuthStatusEntry { - config: Some(McpServerConfig { - auth: Default::default(), - transport: McpServerTransportConfig::StreamableHttp { - url: "https://example.com".to_string(), - bearer_token_env_var: Some("TOKEN".to_string()), - http_headers: None, - env_http_headers: None, - }, - environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), - enabled: true, - required: false, - supports_parallel_tool_calls: false, - disabled_reason: None, - startup_timeout_sec: None, - tool_timeout_sec: None, - default_tools_approval_mode: None, - enabled_tools: None, - disabled_tools: None, - scopes: None, - oauth: None, - oauth_resource: None, - tools: HashMap::new(), - }), - auth_state: McpAuthState::Unsupported, + let config = McpServerConfig { + auth: Default::default(), + transport: McpServerTransportConfig::StreamableHttp { + url: "https://example.com".to_string(), + bearer_token_env_var: Some("TOKEN".to_string()), + http_headers: None, + env_http_headers: None, + }, + environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), + enabled: true, + required: false, + supports_parallel_tool_calls: false, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + default_tools_approval_mode: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + oauth: None, + oauth_resource: None, + tools: HashMap::new(), }; let err: StartupOutcomeError = anyhow::anyhow!("boom").into(); - let display = mcp_init_error_display(server_name, Some(&entry), &err); + let display = mcp_init_error_display(server_name, Some(&config), &err); let expected = format!("MCP client for `{server_name}` failed to start: {err:#}"); @@ -1712,7 +1701,7 @@ fn mcp_init_error_display_includes_startup_timeout_hint() { ] { let err: StartupOutcomeError = anyhow::anyhow!(error).into(); - let display = mcp_init_error_display(server_name, /*entry*/ None, &err); + let display = mcp_init_error_display(server_name, /*config*/ None, &err); assert_eq!( "MCP client for `slow` timed out after 30 seconds. Add or adjust `startup_timeout_sec` in your config.toml:\n[mcp_servers.slow]\nstartup_timeout_sec = XX", diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index ad2245e029..345f425faa 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -310,14 +310,6 @@ pub async fn read_mcp_resource( ) -> anyhow::Result { let mut mcp_servers = effective_mcp_servers(config, auth); mcp_servers.retain(|name, _| name == server); - let auth_statuses = compute_auth_statuses( - mcp_servers.iter(), - config.mcp_oauth_credentials_store_mode, - config.auth_keyring_backend_kind, - auth, - &runtime_context, - ) - .await; let (tx_event, rx_event) = unbounded(); drop(rx_event); let cancel_token = CancellationToken::new(); @@ -325,7 +317,6 @@ pub async fn read_mcp_resource( &mcp_servers, config.mcp_oauth_credentials_store_mode, config.auth_keyring_backend_kind, - auth_statuses, &config.approval_policy, String::new(), tx_event, @@ -404,7 +395,6 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( &mcp_servers, config.mcp_oauth_credentials_store_mode, config.auth_keyring_backend_kind, - auth_status_entries.clone(), &config.approval_policy, submit_id, tx_event, diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index bd12cf3053..de791d8bae 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -38,7 +38,6 @@ use codex_mcp::McpConnectionManager; use codex_mcp::McpRuntimeContext; use codex_mcp::ToolInfo; use codex_mcp::ToolPluginProvenance; -use codex_mcp::compute_auth_statuses; use codex_mcp::effective_mcp_servers; use codex_mcp::tool_plugin_provenance; @@ -235,14 +234,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager( let runtime_context = McpRuntimeContext::new(Arc::clone(&environment_manager), config.cwd.to_path_buf()); - let auth_status_entries = compute_auth_statuses( - mcp_servers.iter(), - config.mcp_oauth_credentials_store_mode, - config.auth_keyring_backend_kind(), - auth.as_ref(), - &runtime_context, - ) - .await; let (tx_event, rx_event) = unbounded(); drop(rx_event); @@ -255,7 +246,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager( &mcp_servers, config.mcp_oauth_credentials_store_mode, config.auth_keyring_backend_kind(), - auth_status_entries, &config.permissions.approval_policy, INITIAL_SUBMIT_ID.to_owned(), tx_event, diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 3356880edc..a1d06d1546 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1439,7 +1439,6 @@ async fn host_owned_codex_apps_manager( &mcp_servers, turn_context.config.mcp_oauth_credentials_store_mode, turn_context.config.auth_keyring_backend_kind(), - HashMap::new(), &turn_context.approval_policy, turn_context.sub_id.clone(), tx_event, diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index a057267900..c215f89c7c 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -347,14 +347,6 @@ impl Session { turn_context.cwd.to_path_buf() }); let mcp_runtime_context = McpRuntimeContext::new(environment_manager, cwd); - let auth_statuses = compute_auth_statuses( - mcp_servers.iter(), - mcp_config.mcp_oauth_credentials_store_mode, - mcp_config.auth_keyring_backend_kind, - auth.as_ref(), - &mcp_runtime_context, - ) - .await; let mcp_startup_cancellation_token = { let mut guard = self.services.mcp_startup_cancellation_token.lock().await; // The previous runtime owns the old token and may still be serving an in-flight step. @@ -371,7 +363,6 @@ impl Session { &mcp_servers, mcp_config.mcp_oauth_credentials_store_mode, mcp_config.auth_keyring_backend_kind, - auth_statuses, &turn_context.approval_policy, turn_context.sub_id.clone(), self.get_tx_event(), diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index e61d59b2e2..b5093d7d01 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -332,7 +332,6 @@ use codex_core_plugins::PluginsManager; use codex_core_plugins::RecommendedPluginCandidatesInput; use codex_git_utils::get_git_repo_root; use codex_mcp::McpConfig; -use codex_mcp::compute_auth_statuses; use codex_mcp::effective_mcp_servers; use codex_otel::SessionTelemetry; use codex_otel::THREAD_STARTED_METRIC; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 4ba1503ea3..a1103eb08b 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -681,7 +681,6 @@ impl Session { .unwrap_or_else(|| session_configuration.cwd().to_path_buf()); let mcp_runtime_context = McpRuntimeContext::new(Arc::clone(&environment_manager), mcp_runtime_cwd); - let mcp_runtime_context_for_auth = mcp_runtime_context.clone(); let auth_and_mcp_fut = async move { let auth = auth_manager_clone.auth().await; let mcp_projection = mcp_manager_for_mcp @@ -696,21 +695,7 @@ impl Session { let mcp_config = &mcp_projection.config; let mcp_servers = codex_mcp::effective_mcp_servers(mcp_config, auth.as_ref()); let tool_plugin_provenance = codex_mcp::tool_plugin_provenance(mcp_config); - let auth_statuses = compute_auth_statuses( - mcp_servers.iter(), - config_for_mcp.mcp_oauth_credentials_store_mode, - config_for_mcp.auth_keyring_backend_kind(), - auth.as_ref(), - &mcp_runtime_context_for_auth, - ) - .await; - ( - auth, - mcp_projection, - mcp_servers, - auth_statuses, - tool_plugin_provenance, - ) + (auth, mcp_projection, mcp_servers, tool_plugin_provenance) } .instrument(info_span!( "session_init.auth_mcp", @@ -721,7 +706,7 @@ impl Session { let ( thread_persistence_result, state_db_ctx, - (auth, mcp_projection, mcp_servers, auth_statuses, tool_plugin_provenance), + (auth, mcp_projection, mcp_servers, tool_plugin_provenance), ) = tokio::join!(thread_persistence_fut, state_db_fut, auth_and_mcp_fut); let mut live_thread_init = @@ -1212,7 +1197,6 @@ impl Session { &mcp_servers, config.mcp_oauth_credentials_store_mode, config.auth_keyring_backend_kind(), - auth_statuses, &session_configuration.approval_policy, INITIAL_SUBMIT_ID.to_owned(), tx_event.clone(), diff --git a/codex-rs/core/tests/suite/mcp_auth_refresh.rs b/codex-rs/core/tests/suite/mcp_auth_refresh.rs index 7d98fd4110..0c05874ccd 100644 --- a/codex-rs/core/tests/suite/mcp_auth_refresh.rs +++ b/codex-rs/core/tests/suite/mcp_auth_refresh.rs @@ -89,7 +89,6 @@ async fn hosted_plugin_runtime_ps_mcp_tool_calls_use_current_auth_manager_token( &mcp_servers, OAuthCredentialsStoreMode::default(), AuthKeyringBackendKind::default(), - HashMap::new(), &approval_policy, "test".to_string(), tx_event, diff --git a/codex-rs/rmcp-client/src/auth_status.rs b/codex-rs/rmcp-client/src/auth_status.rs index 32c244af8e..b8f0e53a79 100644 --- a/codex-rs/rmcp-client/src/auth_status.rs +++ b/codex-rs/rmcp-client/src/auth_status.rs @@ -125,6 +125,32 @@ pub async fn determine_streamable_http_auth_status_with_http_client( ) } +/// Determine authentication status using only configured and stored credentials. +/// +/// Returns `None` when determining the status would require OAuth metadata discovery. +pub fn determine_streamable_http_auth_status_from_credentials( + server_name: &str, + url: &str, + bearer_token_env_var: Option<&str>, + http_headers: Option>, + env_http_headers: Option>, + store_mode: OAuthCredentialsStoreMode, + keyring_backend_kind: AuthKeyringBackendKind, +) -> Result> { + match auth_status_before_discovery( + server_name, + url, + bearer_token_env_var, + http_headers, + env_http_headers, + store_mode, + keyring_backend_kind, + )? { + AuthStatusCheck::Complete(status) => Ok(Some(status)), + AuthStatusCheck::Discover(_) => Ok(None), + } +} + fn auth_status_before_discovery( server_name: &str, url: &str, diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs index 74dd1697be..45799ef4a8 100644 --- a/codex-rs/rmcp-client/src/lib.rs +++ b/codex-rs/rmcp-client/src/lib.rs @@ -17,6 +17,7 @@ pub use auth_status::McpAuthState; pub use auth_status::McpLoginRequirement; pub use auth_status::StreamableHttpOAuthDiscovery; pub use auth_status::determine_streamable_http_auth_status; +pub use auth_status::determine_streamable_http_auth_status_from_credentials; pub use auth_status::determine_streamable_http_auth_status_with_http_client; pub use auth_status::discover_streamable_http_oauth; pub use auth_status::discover_streamable_http_oauth_with_http_client;