From afcc95b4310b46210e42ad6b12f537c2ee066736 Mon Sep 17 00:00:00 2001 From: jif Date: Mon, 10 Aug 2026 17:51:36 +0000 Subject: [PATCH] Speed up MCP OAuth credential reads (#37860) ## Why Refreshing MCP connection identities should not stall the async executor while another process holds the OAuth credential-store lock. ## What changed - Probe file and secrets credential stores without waiting during runtime refreshes, retaining the last known credentials when the store is contended. - Reuse unchanged authentication failures while credentials are temporarily unavailable instead of restarting the connection. - Run blocking credential loads on a blocking worker during MCP client construction. - Allow OAuth fallback when a configured authorization header is missing, empty, or invalid. GitOrigin-RevId: 38dad5606da9a1ceb30fe945000ec9151a5058fa --- codex-rs/codex-mcp/src/connection_manager.rs | 89 ++++++++++++++++-- .../codex-mcp/src/connection_manager_tests.rs | 5 + codex-rs/codex-mcp/src/server.rs | 59 +++++++++--- codex-rs/rmcp-client/src/oauth.rs | 93 ++++++++++++++++++- .../rmcp-client/src/oauth/resolved_store.rs | 62 +++++++++++++ codex-rs/rmcp-client/src/oauth/store_lock.rs | 21 ++++- codex-rs/rmcp-client/src/rmcp_client.rs | 44 +++++---- 7 files changed, 329 insertions(+), 44 deletions(-) diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index 6880484b95..2f93f1a59a 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -64,6 +64,7 @@ use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::McpStartupCompleteEvent; use codex_protocol::protocol::McpStartupFailure; +use codex_protocol::protocol::McpStartupFailureReason; use codex_protocol::protocol::McpStartupStatus; use codex_protocol::protocol::McpStartupUpdateEvent; use codex_rmcp_client::determine_streamable_http_auth_status_from_credentials; @@ -98,6 +99,16 @@ impl McpServerConnection { if client.client.is_closed().await { return None; } + if current == desired { + if matches!(desired.oauth_credentials(), Ok(None)) + && tokio::time::timeout(Duration::ZERO, client.client.managed_oauth_credentials()) + .await + .is_ok_and(|credentials| matches!(credentials, Some(Some(_)))) + { + return None; + } + return Some(client); + } let Ok(desired_credentials) = desired.oauth_credentials() else { return Some(client); }; @@ -167,6 +178,7 @@ impl McpServerView { /// A published view over a set of running MCP server connections. pub(crate) struct McpConnectionSet { servers: HashMap, + protocol_mode: crate::McpProtocolMode, required_servers: Vec, optional_startup_deadline: OnceLock, tool_catalog_revision: Arc>, @@ -336,6 +348,9 @@ impl McpConnectionSet { .then(|| (codex_home.clone(), codex_apps_tools_cache_key.clone())), client_elicitation_capability.clone(), client_mcp_extensions.clone(), + previous + .and_then(|previous| previous.servers.get(&server_name)) + .and_then(|view| view.connection.identity.as_ref()), ); let expected_protocol_mode = match &configured_config.transport { McpServerTransportConfig::StreamableHttp { .. } => Some(protocol_mode), @@ -361,14 +376,31 @@ impl McpConnectionSet { reusable_previous.and_then(|previous| previous.servers.get(&server_name)) { let connection = Arc::clone(&previous_view.connection); - if connection - .reusable_client(&connection_identity) - .await - .is_some_and(|client| { - previous_view.catalog_item_limit == catalog_item_limit - && expected_protocol_mode - .is_some_and(|expected| client.client.protocol_mode() == expected) - }) + let unchanged_auth_failure = if connection.identity.as_ref() + == Some(&connection_identity) + && connection_identity.oauth_store_was_contended + && reusable_previous + .is_some_and(|previous| previous.protocol_mode == protocol_mode) + && connection.client.startup_complete.load(Ordering::Acquire) + { + connection + .client() + .await + .err() + .filter(StartupOutcomeError::is_authentication_required) + } else { + None + }; + if unchanged_auth_failure.is_some() + || connection + .reusable_client(&connection_identity) + .await + .is_some_and(|client| { + previous_view.catalog_item_limit == catalog_item_limit + && expected_protocol_mode.is_some_and(|expected| { + client.client.protocol_mode() == expected + }) + }) { servers.insert( server_name.clone(), @@ -380,7 +412,44 @@ impl McpConnectionSet { catalog_item_limit, }, ); - reused_ready.push(server_name); + if let Some(error) = unchanged_auth_failure { + let status = McpStartupStatus::Failed { + error: mcp_init_error_display( + &server_name, + Some(&configured_config), + &error, + ), + reason: connection_identity + .oauth_credentials() + .ok() + .flatten() + .map(|_| McpStartupFailureReason::ReauthenticationRequired), + }; + let tx_event = tx_event.clone(); + let submit_id = startup_submit_id.clone(); + let publication_gate = publication_gate.clone(); + join_set.spawn(async move { + if !publication_gate.wait().await { + return (server_name, Err(StartupOutcomeError::Cancelled)); + } + if let Some(tx_event) = tx_event.as_ref() { + for status in [McpStartupStatus::Starting, status] { + let _ = emit_update( + submit_id.as_str(), + tx_event, + McpStartupUpdateEvent { + server: server_name.clone(), + status, + }, + ) + .await; + } + } + (server_name, Err(error)) + }); + } else { + reused_ready.push(server_name); + } continue; } } @@ -567,6 +636,7 @@ impl McpConnectionSet { } let manager = Self { servers, + protocol_mode, required_servers, optional_startup_deadline: OnceLock::new(), tool_catalog_revision: Arc::new(RwLock::new(0)), @@ -625,6 +695,7 @@ impl McpConnectionSet { pub fn empty(prefix_mcp_tool_names: bool) -> Self { Self { servers: HashMap::new(), + protocol_mode: crate::McpProtocolMode::Legacy, required_servers: Vec::new(), optional_startup_deadline: OnceLock::new(), tool_catalog_revision: Arc::new(RwLock::new(0)), diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index a2e2a7c4a1..c4add7f930 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -88,6 +88,7 @@ impl McpConnectionSet { ) -> Self { Self { servers: HashMap::new(), + protocol_mode: crate::McpProtocolMode::Legacy, required_servers: Vec::new(), optional_startup_deadline: OnceLock::new(), tool_catalog_revision: Arc::new(RwLock::new(0)), @@ -3034,6 +3035,7 @@ async fn executor_owned_chatgpt_mcp_accepts_only_safe_explicit_authorization() - /*codex_apps_cache_identity*/ None, ElicitationCapability::default(), ClientMcpExtensions::default(), + /*previous_identity*/ None, ) }; let direct_keyring_identity = connection_identity(AuthKeyringBackendKind::Direct); @@ -3486,6 +3488,7 @@ fn reusable_server_identity( /*codex_apps_cache_identity*/ None, ElicitationCapability::default(), ClientMcpExtensions::default(), + /*previous_identity*/ None, ) } @@ -4021,6 +4024,7 @@ async fn connection_identity_distinguishes_accounts_with_the_same_token() -> any /*codex_apps_cache_identity*/ None, ElicitationCapability::default(), ClientMcpExtensions::default(), + /*previous_identity*/ None, ) }; @@ -4072,6 +4076,7 @@ async fn connection_identity_distinguishes_agent_account_runtime_and_task() -> a /*codex_apps_cache_identity*/ None, ElicitationCapability::default(), ClientMcpExtensions::default(), + /*previous_identity*/ None, ) }; let previous_identity = connection_identity(&previous_auth); diff --git a/codex-rs/codex-mcp/src/server.rs b/codex-rs/codex-mcp/src/server.rs index b788051d2c..b74ea03adb 100644 --- a/codex-rs/codex-mcp/src/server.rs +++ b/codex-rs/codex-mcp/src/server.rs @@ -17,8 +17,6 @@ use codex_login::CodexAuth; use codex_protocol::mcp::ClientMcpExtensions; use codex_rmcp_client::StoredOAuthCredentialSnapshot; use codex_rmcp_client::StoredOAuthTokens; -use codex_rmcp_client::stored_oauth_credential_snapshot; -use codex_rmcp_client::stored_oauth_credentials; use rmcp::model::ElicitationCapability; use tracing::warn; @@ -99,6 +97,7 @@ pub(crate) struct McpServerConnectionIdentity { environment_id: String, oauth_store: Option<(OAuthCredentialsStoreMode, AuthKeyringBackendKind)>, oauth_credentials: Result, String>, + pub(crate) oauth_store_was_contended: bool, resolved_environment: Result>, String>, local_stdio_fallback_cwd: Option, referenced_environment_variables: Vec<(String, Option)>, @@ -124,8 +123,14 @@ impl McpServerConnectionIdentity { codex_apps_cache_identity: Option<(PathBuf, ConnectorRuntimeContextKey)>, client_elicitation_capability: ElicitationCapability, client_mcp_extensions: ClientMcpExtensions, + previous_identity: Option<&Self>, ) -> Self { let config = server.config(); + let valid_http_header_value = |value: &str| { + value + .bytes() + .all(|byte| byte == b'\t' || (byte >= b' ' && byte != 0x7f)) + }; let stored_oauth_url = if runtime_auth_provider.is_none() && (!matches!(config.auth, McpServerAuth::ChatGpt) || config.is_local_environment()) { @@ -133,20 +138,44 @@ impl McpServerConnectionIdentity { McpServerTransportConfig::StreamableHttp { url, bearer_token_env_var: None, - .. - } => Some(url), - McpServerTransportConfig::StreamableHttp { - bearer_token_env_var: Some(_), - .. + http_headers, + env_http_headers, + } if !http_headers.as_ref().is_some_and(|headers| { + headers.iter().any(|(name, value)| { + name.eq_ignore_ascii_case("authorization") && valid_http_header_value(value) + }) + }) && !env_http_headers.as_ref().is_some_and(|headers| { + headers.iter().any(|(name, env_var)| { + name.eq_ignore_ascii_case("authorization") + && std::env::var(env_var).is_ok_and(|value| { + !value.trim().is_empty() && valid_http_header_value(&value) + }) + }) + }) => + { + Some(url) } + McpServerTransportConfig::StreamableHttp { .. } | McpServerTransportConfig::Stdio { .. } => None, } } else { None }; let oauth_credentials = stored_oauth_url.map_or(Ok(None), |url| { - stored_oauth_credential_snapshot( - config.oauth_credential_name(server_name).as_ref(), + let credential_name = config.oauth_credential_name(server_name); + StoredOAuthCredentialSnapshot::for_runtime_refresh( + previous_identity.and_then(|previous_identity| { + previous_identity + .oauth_credentials + .as_ref() + .ok() + .and_then(Option::as_ref) + .filter(|_| { + previous_identity.oauth_store + == Some((store_mode, keyring_backend_kind)) + }) + }), + credential_name.as_ref(), url, store_mode, keyring_backend_kind, @@ -165,6 +194,11 @@ impl McpServerConnectionIdentity { let referenced_environment_variables = referenced_environment_variables(config); let runtime_auth = runtime_auth_provider.and(auth).cloned(); let runtime_auth_token = runtime_auth.as_ref().and_then(|auth| auth.get_token().ok()); + let oauth_store_was_contended = oauth_credentials + .as_ref() + .ok() + .and_then(Option::as_ref) + .is_some_and(StoredOAuthCredentialSnapshot::store_was_contended); Self { transport: config.transport.clone(), @@ -173,6 +207,7 @@ impl McpServerConnectionIdentity { .is_some() .then_some((store_mode, keyring_backend_kind)), oauth_credentials, + oauth_store_was_contended, resolved_environment: resolved_environment.clone(), local_stdio_fallback_cwd, referenced_environment_variables, @@ -241,12 +276,14 @@ impl McpServerConnectionIdentity { store_mode, keyring_backend_kind, ), - Ok(None) | Err(_) => stored_oauth_credentials( + Ok(None) | Err(_) => StoredOAuthCredentialSnapshot::for_runtime_refresh( + /*previous*/ None, credential_name.as_ref(), url, store_mode, keyring_backend_kind, - ), + ) + .map(|snapshot| snapshot.map(|snapshot| snapshot.credentials().clone())), }; match current_credentials { diff --git a/codex-rs/rmcp-client/src/oauth.rs b/codex-rs/rmcp-client/src/oauth.rs index 6af8d95f2f..2f6238909e 100644 --- a/codex-rs/rmcp-client/src/oauth.rs +++ b/codex-rs/rmcp-client/src/oauth.rs @@ -72,6 +72,7 @@ use codex_utils_home_dir::find_codex_home; pub(crate) use self::resolved_store::ResolvedOAuthCredentialStore; pub(crate) use self::resolved_store::ResolvedOAuthTokens; pub(crate) use self::resolved_store::resolve_oauth_tokens_from_store_policy; +use self::resolved_store::try_resolve_oauth_tokens_from_store_policy; const KEYRING_SERVICE: &str = "Codex MCP Credentials"; const MCP_OAUTH_SECRET_PREFIX: &str = "MCP_OAUTH"; @@ -88,10 +89,17 @@ pub struct StoredOAuthTokens { } /// OAuth credentials paired with the concrete store selected for their client lifecycle. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct StoredOAuthCredentialSnapshot { credentials: StoredOAuthTokens, store: ResolvedOAuthCredentialStore, + store_was_contended: bool, +} + +impl PartialEq for StoredOAuthCredentialSnapshot { + fn eq(&self, other: &Self) -> bool { + self.credentials == other.credentials && self.store == other.store + } } impl StoredOAuthCredentialSnapshot { @@ -100,7 +108,49 @@ impl StoredOAuthCredentialSnapshot { &self.credentials } - /// Rereads the selected authority, allowing Auto login to migrate File credentials to keyring. + /// Returns whether this snapshot was retained because its store could not be read. + pub fn store_was_contended(&self) -> bool { + self.store_was_contended + } + + /// Refreshes a runtime snapshot without waiting or discarding its last known authority. + pub fn for_runtime_refresh( + previous: Option<&Self>, + server_name: &str, + url: &str, + store_mode: OAuthCredentialsStoreMode, + keyring_backend_kind: AuthKeyringBackendKind, + ) -> Result> { + match try_resolve_oauth_tokens_from_store_policy( + &DefaultKeyringStore, + server_name, + url, + store_mode, + keyring_backend_kind, + ) { + Ok(Some(mut resolved)) => { + resolved.tokens.token_response.0.set_expires_in(None); + Ok(Some(Self { + credentials: resolved.tokens, + store: resolved.store, + store_was_contended: false, + })) + } + Ok(None) => Ok(None), + Err(error) if oauth_store_is_contended(&error) => Ok(previous + .filter(|previous| { + previous.credentials.server_name == server_name + && previous.credentials.url == url + }) + .map(|previous| Self { + store_was_contended: true, + ..previous.clone() + })), + Err(error) => Err(error), + } + } + + /// Rereads the selected authority without waiting for a contended credential-store lock. pub fn reload( &self, server_name: &str, @@ -111,10 +161,21 @@ impl StoredOAuthCredentialSnapshot { if self.store == ResolvedOAuthCredentialStore::File && store_mode == OAuthCredentialsStoreMode::Auto { - return stored_oauth_credentials(server_name, url, store_mode, keyring_backend_kind); + return Self::for_runtime_refresh( + /*previous*/ None, + server_name, + url, + store_mode, + keyring_backend_kind, + ) + .map(|snapshot| snapshot.map(|snapshot| snapshot.credentials)); } - let credentials = self.store.load(&DefaultKeyringStore, server_name, url)?; + let credentials = match self.store.try_load(&DefaultKeyringStore, server_name, url) { + Ok(credentials) => credentials, + Err(error) if oauth_store_is_contended(&error) => return Ok(None), + Err(error) => return Err(error), + }; Ok(normalized_oauth_credentials(credentials.as_ref())) } } @@ -194,9 +255,18 @@ pub fn stored_oauth_credential_snapshot( Ok(Some(StoredOAuthCredentialSnapshot { credentials, store: resolved.store, + store_was_contended: false, })) } +fn oauth_store_is_contended(error: &Error) -> bool { + matches!( + error.downcast_ref::(), + Some(OAuthStoreLockFailure::Timeout { acquire_timeout, .. }) + if acquire_timeout.is_zero() + ) +} + fn normalized_oauth_credentials(tokens: Option<&StoredOAuthTokens>) -> Option { tokens.map(|tokens| { let mut tokens = tokens.clone(); @@ -283,6 +353,14 @@ fn load_oauth_tokens_from_secrets_keyring( url: &str, ) -> std::result::Result, OAuthKeyringLoadError> { let _store_lock = OAuthStoreLock::acquire_for_read(OAuthStore::Secrets)?; + load_oauth_tokens_from_secrets_keyring_with_lock_held(keyring_store, server_name, url) +} + +fn load_oauth_tokens_from_secrets_keyring_with_lock_held( + keyring_store: &K, + server_name: &str, + url: &str, +) -> std::result::Result, OAuthKeyringLoadError> { let codex_home = find_codex_home().map_err(anyhow::Error::from)?; let manager = SecretsManager::new_with_keyring_store_and_namespace( codex_home.to_path_buf(), @@ -682,6 +760,13 @@ struct FallbackTokenEntry { fn load_oauth_tokens_from_file(server_name: &str, url: &str) -> Result> { let _store_lock = OAuthStoreLock::acquire_for_read(OAuthStore::File)?; + load_oauth_tokens_from_file_with_lock_held(server_name, url) +} + +fn load_oauth_tokens_from_file_with_lock_held( + server_name: &str, + url: &str, +) -> Result> { let Some(store) = read_fallback_file_unlocked()? else { return Ok(None); }; diff --git a/codex-rs/rmcp-client/src/oauth/resolved_store.rs b/codex-rs/rmcp-client/src/oauth/resolved_store.rs index 6a688ea539..904f5d9670 100644 --- a/codex-rs/rmcp-client/src/oauth/resolved_store.rs +++ b/codex-rs/rmcp-client/src/oauth/resolved_store.rs @@ -8,13 +8,18 @@ use codex_keyring_store::KeyringStore; use tracing::warn; use super::OAuthKeyringLoadError; +use super::OAuthStore; +use super::OAuthStoreLock; +use super::OAuthStoreLockFailure; use super::StoredOAuthTokens; use super::compute_store_key; use super::delete_oauth_tokens_from_direct_keyring; use super::delete_oauth_tokens_from_file; use super::delete_oauth_tokens_from_secrets_keyring; use super::load_oauth_tokens_from_file; +use super::load_oauth_tokens_from_file_with_lock_held; use super::load_oauth_tokens_from_keyring; +use super::load_oauth_tokens_from_secrets_keyring_with_lock_held; use super::save_oauth_tokens_to_file; use super::save_oauth_tokens_with_keyring; @@ -57,6 +62,34 @@ impl ResolvedOAuthCredentialStore { } } + /// Reads the selected authority without waiting for its aggregate-store lock. + pub(crate) fn try_load( + self, + keyring_store: &K, + server_name: &str, + url: &str, + ) -> Result> { + match self { + Self::File => { + let _store_lock = OAuthStoreLock::try_acquire_for_read(OAuthStore::File)?; + load_oauth_tokens_from_file_with_lock_held(server_name, url) + .context("failed to probe OAuth tokens from resolved file storage") + } + Self::Keyring(AuthKeyringBackendKind::Direct) => { + self.load(keyring_store, server_name, url) + } + Self::Keyring(AuthKeyringBackendKind::Secrets) => { + let _store_lock = OAuthStoreLock::try_acquire_for_read(OAuthStore::Secrets)?; + load_oauth_tokens_from_secrets_keyring_with_lock_held( + keyring_store, + server_name, + url, + ) + .map_err(anyhow::Error::from) + } + } + } + /// Saves credentials only to this already-resolved authority. pub(crate) fn save( self, @@ -174,3 +207,32 @@ pub(crate) fn resolve_oauth_tokens_from_store_policy( + keyring_store: &K, + server_name: &str, + url: &str, + store_mode: OAuthCredentialsStoreMode, + keyring_backend_kind: AuthKeyringBackendKind, +) -> Result> { + let load = |store: ResolvedOAuthCredentialStore| { + store + .try_load(keyring_store, server_name, url) + .map(|tokens| tokens.map(|tokens| ResolvedOAuthTokens { tokens, store })) + }; + let keyring = ResolvedOAuthCredentialStore::Keyring(keyring_backend_kind); + match store_mode { + OAuthCredentialsStoreMode::File => load(ResolvedOAuthCredentialStore::File), + OAuthCredentialsStoreMode::Keyring => load(keyring), + OAuthCredentialsStoreMode::Auto => match load(keyring) { + Ok(Some(tokens)) => Ok(Some(tokens)), + Ok(None) => load(ResolvedOAuthCredentialStore::File), + Err(error) if error.downcast_ref::().is_some() => Err(error), + Err(error) => { + warn!("failed to read OAuth tokens from keyring: {error}"); + load(ResolvedOAuthCredentialStore::File) + .with_context(|| format!("failed to read OAuth tokens from keyring: {error}")) + } + }, + } +} diff --git a/codex-rs/rmcp-client/src/oauth/store_lock.rs b/codex-rs/rmcp-client/src/oauth/store_lock.rs index 0a6f3c6494..f06b9b9adc 100644 --- a/codex-rs/rmcp-client/src/oauth/store_lock.rs +++ b/codex-rs/rmcp-client/src/oauth/store_lock.rs @@ -59,15 +59,28 @@ enum OAuthStoreLockMode { impl OAuthStoreLock { pub(super) fn acquire_for_write(store: OAuthStore) -> Result { - Self::acquire_with_mode(store, OAuthStoreLockMode::Exclusive) + Self::acquire_with_timeout( + store, + STORE_LOCK_ACQUIRE_TIMEOUT, + OAuthStoreLockMode::Exclusive, + ) } pub(super) fn acquire_for_read(store: OAuthStore) -> Result { - Self::acquire_with_mode(store, OAuthStoreLockMode::Shared) + Self::acquire_with_timeout( + store, + STORE_LOCK_ACQUIRE_TIMEOUT, + OAuthStoreLockMode::Shared, + ) } - fn acquire_with_mode( + pub(super) fn try_acquire_for_read(store: OAuthStore) -> Result { + Self::acquire_with_timeout(store, Duration::ZERO, OAuthStoreLockMode::Shared) + } + + fn acquire_with_timeout( store: OAuthStore, + acquire_timeout: Duration, mode: OAuthStoreLockMode, ) -> Result { // This lock intentionally follows the existing local File/Secrets credential-store @@ -77,7 +90,7 @@ impl OAuthStoreLock { // provide its own matching lock authority instead of using this local path. let codex_home = find_codex_home() .map_err(|source| OAuthStoreLockFailure::CodexHome { store, source })?; - Self::acquire_in_with_mode(&codex_home, store, STORE_LOCK_ACQUIRE_TIMEOUT, mode) + Self::acquire_in_with_mode(&codex_home, store, acquire_timeout, mode) } fn acquire_in_with_mode( diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index ddf2a73f51..74447d0571 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -1025,19 +1025,27 @@ impl RmcpClient { && auth_provider.is_none() && !default_headers.contains_key(AUTHORIZATION) { - if let Some(store) = pinned_credential_store.get().copied() { - // Rebuilds reread the source selected during first construction. Only the - // initial construction below evaluates configured store policy. - store - .load(&DefaultKeyringStore, server_name, url)? - .map(|tokens| ResolvedOAuthTokens { tokens, store }) - } else { + let oauth_server_name = server_name.clone(); + let oauth_url = url.clone(); + let oauth_store_mode = *store_mode; + let oauth_keyring_backend_kind = *keyring_backend_kind; + let pinned_credential_store = Arc::clone(pinned_credential_store); + + tokio::task::spawn_blocking(move || -> Result> { + if let Some(store) = pinned_credential_store.get().copied() { + // Rebuilds reread the source selected during first construction. Only + // initial construction below evaluates configured store policy. + return store + .load(&DefaultKeyringStore, &oauth_server_name, &oauth_url) + .map(|tokens| tokens.map(|tokens| ResolvedOAuthTokens { tokens, store })); + } + match resolve_oauth_tokens_from_store_policy( &DefaultKeyringStore, - server_name, - url, - *store_mode, - *keyring_backend_kind, + &oauth_server_name, + &oauth_url, + oauth_store_mode, + oauth_keyring_backend_kind, ) { Ok(tokens) => { if let Some(resolved) = tokens.as_ref() { @@ -1045,18 +1053,22 @@ impl RmcpClient { // first concrete source so Auto is not reevaluated mid-client. pinned_credential_store.set(resolved.store).map_err(|_| { anyhow!( - "OAuth credential store pinned concurrently for MCP server `{server_name}`" + "OAuth credential store pinned concurrently for MCP server `{oauth_server_name}`" ) })?; } - tokens + Ok(tokens) } Err(err) => { - warn!("failed to read tokens for server `{server_name}`: {err}"); - None + warn!( + "failed to read tokens for server `{oauth_server_name}`: {err}" + ); + Ok(None) } } - } + }) + .await + .map_err(|error| anyhow!("OAuth credential loading task failed: {error}"))?? } else { None };