From bc90844a636ada790bb85438fb079d484b6288c6 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Tue, 4 Nov 2025 10:25:04 -0800 Subject: [PATCH 1/3] fix: refresh expired MCP OAuth tokens Repro: - Set up an HTTP MCP server with OAuth expiring tokens like Datadog - Use it for a bit until the token expires - Start a new codex session Expected results: - The token gets transparently refreshed and the MCP server continues to work Actual results: - You start getting 401s and need to do `codex mcp logout` and log back in. --- codex-rs/rmcp-client/src/rmcp_client.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index bc1980f1f5..0e23eb4ca2 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -9,6 +9,7 @@ use std::time::Duration; use anyhow::Result; use anyhow::anyhow; use futures::FutureExt; +use oauth2::TokenResponse; use mcp_types::CallToolRequestParams; use mcp_types::CallToolResult; use mcp_types::InitializeRequestParams; @@ -31,6 +32,7 @@ use rmcp::service::RunningService; use rmcp::service::{self}; use rmcp::transport::StreamableHttpClientTransport; use rmcp::transport::auth::AuthClient; +use rmcp::transport::auth::OAuthTokenResponse; use rmcp::transport::auth::OAuthState; use rmcp::transport::child_process::TokioChildProcess; use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig; @@ -55,6 +57,8 @@ use crate::utils::convert_to_rmcp; use crate::utils::create_env_for_mcp_server; use crate::utils::run_with_timeout; +const REFRESH_SKEW_SECS: u64 = 60; + enum PendingTransport { ChildProcess(TokioChildProcess), StreamableHttp { @@ -397,6 +401,13 @@ async fn create_oauth_transport_and_runtime( let auth_client = AuthClient::new(http_client, manager); let auth_manager = auth_client.auth_manager.clone(); + // If the stored token is expired or about to expire, refresh before the handshake. + if should_refresh_initial_token(&initial_tokens.token_response.0) { + if let Err(err) = auth_manager.lock().await.refresh_token().await { + warn!("failed to refresh OAuth token before handshake: {err}"); + } + } + let transport = StreamableHttpClientTransport::with_client( auth_client, StreamableHttpClientTransportConfig::with_uri(url.to_string()), @@ -412,3 +423,10 @@ async fn create_oauth_transport_and_runtime( Ok((transport, runtime)) } + +fn should_refresh_initial_token(token: &OAuthTokenResponse) -> bool { + match token.expires_in() { + Some(duration) => duration.as_secs() <= REFRESH_SKEW_SECS, + None => token.refresh_token().is_some(), + } +} From 24535c59224798a369ab0b564e5c896bb9db4859 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Tue, 4 Nov 2025 11:01:00 -0800 Subject: [PATCH 2/3] Implement proper fix: also set the token expiration time when it's expired MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old load path in codex-rs/rmcp-client/src/oauth.rs only set expires_in when the stored expiry was in the future: // codex-rs/rmcp-client/src/oauth.rs:369-374 (original) if let Some(expires_at) = entry.expires_at && let Some(seconds) = expires_in_from_timestamp(expires_at) { let duration = Duration::from_secs(seconds); token_response.set_expires_in(Some(&duration)); } And expires_in_from_timestamp returned None for expired tokens: // codex-rs/rmcp-client/src/oauth.rs:444-453 (original) if expires_at <= now_ms { None } else { Some((expires_at - now_ms) / 1000) } So when the stored token was already expired, set_expires_in was never called, RMCP saw “no expiry,” and it didn’t auto-refresh on handshake. --- codex-rs/rmcp-client/src/oauth.rs | 5 ++--- codex-rs/rmcp-client/src/rmcp_client.rs | 17 ----------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/codex-rs/rmcp-client/src/oauth.rs b/codex-rs/rmcp-client/src/oauth.rs index bd6833fca4..af01cdfa88 100644 --- a/codex-rs/rmcp-client/src/oauth.rs +++ b/codex-rs/rmcp-client/src/oauth.rs @@ -366,9 +366,8 @@ fn load_oauth_tokens_from_file(server_name: &str, url: &str) -> Result bool { - match token.expires_in() { - Some(duration) => duration.as_secs() <= REFRESH_SKEW_SECS, - None => token.refresh_token().is_some(), - } -} From 214ac14b608369ad740f51b60f7fce42cb3f0a68 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Tue, 4 Nov 2025 11:13:48 -0800 Subject: [PATCH 3/3] Remove import --- codex-rs/rmcp-client/src/rmcp_client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index 01f8437972..bc1980f1f5 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -31,7 +31,6 @@ use rmcp::service::RunningService; use rmcp::service::{self}; use rmcp::transport::StreamableHttpClientTransport; use rmcp::transport::auth::AuthClient; -use rmcp::transport::auth::OAuthTokenResponse; use rmcp::transport::auth::OAuthState; use rmcp::transport::child_process::TokioChildProcess; use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;