mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
## Why An OAuth-backed Streamable HTTP MCP server can fail startup when its stored credentials are rejected. If a client then completes OAuth sign-in and replaces those credentials, the failed server needs to become available without restarting the Codex session. ## What changed - Track the credential store and OAuth tokens used for each MCP connection, and detect replacements after authentication failures. - Refresh affected MCP servers before the next turn and include their recovered tools in the model request. - Classify HTTP authentication challenges and rejected refresh tokens as requiring reauthentication, with sign-in guidance appropriate to local and client-managed environments. ## Testing - Cover recovery from expired credentials through an externally written replacement token. - Cover authentication-error classification, startup messaging, and `Auto` store migration from file credentials to the keyring. GitOrigin-RevId: 9b090089d4e7a44ffc182f4e25da20a18e97b70f
59 lines
2.2 KiB
Rust
59 lines
2.2 KiB
Rust
use anyhow::Error;
|
|
use rmcp::service::ClientInitializeError;
|
|
use rmcp::service::ServiceError;
|
|
use rmcp::transport::DynamicTransportError;
|
|
use rmcp::transport::auth::AuthError;
|
|
use rmcp::transport::streamable_http_client::StreamableHttpError;
|
|
|
|
use crate::http_client_adapter::StreamableHttpClientAdapterError;
|
|
use crate::rmcp_client::ClientOperationError;
|
|
|
|
/// Returns whether an RMCP client error indicates that authentication is required.
|
|
///
|
|
/// This does not distinguish first-time login from reauthentication.
|
|
/// Streamable HTTP initialization errors are stored inside RMCP's dynamic
|
|
/// transport error, which is not part of the standard error source chain.
|
|
pub fn is_authentication_required_error(error: &Error) -> bool {
|
|
error.chain().any(|source| {
|
|
source
|
|
.downcast_ref::<AuthError>()
|
|
.is_some_and(auth_error_requires_authentication)
|
|
|| source
|
|
.downcast_ref::<ClientInitializeError>()
|
|
.is_some_and(|error| {
|
|
matches!(
|
|
error,
|
|
ClientInitializeError::TransportError { error, .. }
|
|
if transport_error_requires_authentication(error)
|
|
)
|
|
})
|
|
|| source
|
|
.downcast_ref::<ClientOperationError>()
|
|
.is_some_and(|error| {
|
|
matches!(
|
|
error,
|
|
ClientOperationError::Service(ServiceError::TransportSend(error))
|
|
if transport_error_requires_authentication(error)
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
fn transport_error_requires_authentication(error: &DynamicTransportError) -> bool {
|
|
error
|
|
.error
|
|
.downcast_ref::<StreamableHttpError<StreamableHttpClientAdapterError>>()
|
|
.is_some_and(|error| match error {
|
|
StreamableHttpError::AuthRequired(_) => true,
|
|
StreamableHttpError::Auth(auth_error) => auth_error_requires_authentication(auth_error),
|
|
_ => false,
|
|
})
|
|
}
|
|
|
|
fn auth_error_requires_authentication(error: &AuthError) -> bool {
|
|
matches!(
|
|
error,
|
|
AuthError::AuthorizationRequired | AuthError::TokenExpired
|
|
)
|
|
}
|