Codex Handbook
rmcp-client/src/rmcp_client.rs 1219 lines
use std::collections::HashMap;use std::ffi::OsString;use std::future::Future;use std::io;use std::path::PathBuf;use std::sync::Arc;use std::sync::atomic::AtomicUsize;use std::sync::atomic::Ordering;use std::time::Duration;use std::time::Instant;use anyhow::Result;use anyhow::anyhow;use codex_api::SharedAuthProvider;use codex_client::maybe_build_rustls_client_config_with_custom_ca;use codex_config::types::AuthKeyringBackendKind;use codex_config::types::McpServerEnvVar;use codex_exec_server::HttpClient;use futures::FutureExt;use futures::future::BoxFuture;use oauth2::TokenResponse;use reqwest::header::AUTHORIZATION;use reqwest::header::HeaderMap;use rmcp::model::CallToolRequestParams;use rmcp::model::CallToolResult;use rmcp::model::ClientNotification;use rmcp::model::ClientRequest;use rmcp::model::CreateElicitationRequestParams;use rmcp::model::CreateElicitationResult;use rmcp::model::CustomNotification;use rmcp::model::CustomRequest;use rmcp::model::ElicitationAction;use rmcp::model::Extensions;use rmcp::model::InitializeRequestParams;use rmcp::model::InitializeResult;use rmcp::model::ListResourceTemplatesResult;use rmcp::model::ListResourcesResult;use rmcp::model::ListToolsResult;use rmcp::model::PaginatedRequestParams;use rmcp::model::ReadResourceRequestParams;use rmcp::model::ReadResourceResult;use rmcp::model::RequestId;use rmcp::model::ServerResult;use rmcp::model::Tool;use rmcp::service::RoleClient;use rmcp::service::RunningService;use rmcp::service::{self};use rmcp::transport::StreamableHttpClientTransport;use rmcp::transport::auth::AuthClient;use rmcp::transport::auth::AuthError;use rmcp::transport::auth::OAuthState;use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;use rmcp::transport::streamable_http_client::StreamableHttpError;use serde::Deserialize;use serde::Serialize;use serde_json::Value;use tokio::sync::Mutex;use tokio::sync::Semaphore;use tokio::sync::watch;use tokio::time;use tracing::warn;use crate::elicitation_client_service::ElicitationClientService;use crate::http_client_adapter::StreamableHttpClientAdapter;use crate::http_client_adapter::StreamableHttpClientAdapterError;use crate::in_process_transport::InProcessTransportFactory;use crate::load_oauth_tokens;use crate::oauth::OAuthPersistor;use crate::oauth::StoredOAuthTokens;use crate::stdio_server_launcher::StdioServerCommand;use crate::stdio_server_launcher::StdioServerLauncher;use crate::stdio_server_launcher::StdioServerProcessHandle;use crate::stdio_server_launcher::StdioServerTransport;use crate::utils::apply_default_headers;use crate::utils::build_default_headers;use codex_config::types::OAuthCredentialsStoreMode;#[path = "streamable_http_retry.rs"]mod streamable_http_retry;use self::streamable_http_retry::HandshakeError;use self::streamable_http_retry::STREAMABLE_HTTP_RETRY_DELAYS_MS;use self::streamable_http_retry::sleep_with_retry_deadline;enum PendingTransport {    InProcess {        transport: tokio::io::DuplexStream,    },    Stdio {        transport: StdioServerTransport,    },    StreamableHttp {        transport: StreamableHttpClientTransport<StreamableHttpClientAdapter>,    },    StreamableHttpWithOAuth {        transport: StreamableHttpClientTransport<AuthClient<StreamableHttpClientAdapter>>,        oauth_persistor: OAuthPersistor,    },}enum ClientState {    Connecting {        transport: Option<PendingTransport>,    },    Ready {        service: Arc<RunningService<RoleClient, ElicitationClientService>>,        oauth: Option<OAuthPersistor>,    },    Closed,}#[derive(Clone)]enum TransportRecipe {    InProcess {        factory: Arc<dyn InProcessTransportFactory>,    },    Stdio {        command: StdioServerCommand,        launcher: Arc<dyn StdioServerLauncher>,    },    StreamableHttp {        server_name: String,        url: String,        bearer_token: Option<String>,        http_headers: Option<HashMap<String, String>>,        env_http_headers: Option<HashMap<String, String>>,        store_mode: OAuthCredentialsStoreMode,        keyring_backend_kind: AuthKeyringBackendKind,        http_client: Arc<dyn HttpClient>,        auth_provider: Option<SharedAuthProvider>,    },}#[derive(Clone)]struct InitializeContext {    timeout: Option<Duration>,    client_service: ElicitationClientService,}#[derive(Clone)]pub(crate) struct ElicitationPauseState {    active_count: Arc<AtomicUsize>,    paused: watch::Sender<bool>,}impl ElicitationPauseState {    fn new() -> Self {        let (paused, _rx) = watch::channel(false);        Self {            active_count: Arc::new(AtomicUsize::new(0)),            paused,        }    }    pub(crate) fn enter(&self) -> ElicitationPauseGuard {        if self.active_count.fetch_add(1, Ordering::AcqRel) == 0 {            self.paused.send_replace(true);        }        ElicitationPauseGuard {            pause_state: self.clone(),        }    }    fn subscribe(&self) -> watch::Receiver<bool> {        self.paused.subscribe()    }}pub(crate) struct ElicitationPauseGuard {    pause_state: ElicitationPauseState,}impl Drop for ElicitationPauseGuard {    fn drop(&mut self) {        if self.pause_state.active_count.fetch_sub(1, Ordering::AcqRel) == 1 {            self.pause_state.paused.send_replace(false);        }    }}async fn active_time_timeout<T, Fut>(    duration: Duration,    mut pause_state: watch::Receiver<bool>,    operation: Fut,) -> std::result::Result<T, ()>where    Fut: Future<Output = T>,{    let mut remaining = duration;    tokio::pin!(operation);    loop {        if *pause_state.borrow_and_update() {            tokio::select! {                result = &mut operation => return Ok(result),                changed = pause_state.changed() => {                    if changed.is_err() {                        return time::timeout(remaining, operation).await.map_err(|_| ());                    }                    let _paused = *pause_state.borrow_and_update();                }            }            continue;        }        let active_start = Instant::now();        tokio::select! {            result = &mut operation => return Ok(result),            _ = time::sleep(remaining) => {                return Err(());            }            changed = pause_state.changed() => {                if changed.is_err() {                    return time::timeout(remaining, operation).await.map_err(|_| ());                }                if *pause_state.borrow_and_update() {                    remaining = remaining.saturating_sub(active_start.elapsed());                    if remaining.is_zero() {                        return Err(());                    }                }            }        }    }}#[derive(Debug, thiserror::Error)]enum ClientOperationError {    #[error(transparent)]    Service(#[from] rmcp::service::ServiceError),    #[error("timed out awaiting {label} after {duration:?}")]    Timeout { label: String, duration: Duration },}fn remaining_operation_timeout(    label: &str,    timeout: Option<Duration>,    deadline: Option<Instant>,) -> std::result::Result<Option<Duration>, ClientOperationError> {    let Some(deadline) = deadline else {        return Ok(None);    };    let remaining = deadline.saturating_duration_since(Instant::now());    if remaining.is_zero() {        Err(ClientOperationError::Timeout {            label: label.to_string(),            duration: timeout.unwrap_or(remaining),        })    } else {        Ok(Some(remaining))    }}pub type Elicitation = CreateElicitationRequestParams;#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct ElicitationResponse {    pub action: ElicitationAction,    pub content: Option<serde_json::Value>,    #[serde(rename = "_meta")]    pub meta: Option<serde_json::Value>,}impl From<CreateElicitationResult> for ElicitationResponse {    fn from(value: CreateElicitationResult) -> Self {        Self {            action: value.action,            content: value.content,            meta: None,        }    }}impl From<ElicitationResponse> for CreateElicitationResult {    fn from(value: ElicitationResponse) -> Self {        Self {            action: value.action,            content: value.content,            meta: None,        }    }}/// Interface for sending elicitation requests to the UI and awaiting a response.pub type SendElicitation = Box<    dyn Fn(RequestId, Elicitation) -> BoxFuture<'static, Result<ElicitationResponse>> + Send + Sync,>;pub struct ToolWithConnectorId {    pub tool: Tool,    pub connector_id: Option<String>,    pub connector_name: Option<String>,    pub connector_description: Option<String>,}pub struct ListToolsWithConnectorIdResult {    pub next_cursor: Option<String>,    pub tools: Vec<ToolWithConnectorId>,}/// MCP client implemented on top of the official `rmcp` SDK./// https://github.com/modelcontextprotocol/rust-sdkpub struct RmcpClient {    state: Mutex<ClientState>,    stdio_process: Option<StdioServerProcessHandle>,    transport_recipe: TransportRecipe,    initialize_context: Mutex<Option<InitializeContext>>,    session_recovery_lock: Semaphore,    elicitation_pause_state: ElicitationPauseState,}impl RmcpClient {    pub async fn new_in_process_client(        factory: Arc<dyn InProcessTransportFactory>,    ) -> io::Result<Self> {        let transport_recipe = TransportRecipe::InProcess { factory };        let transport = Self::create_pending_transport(&transport_recipe)            .await            .map_err(io::Error::other)?;        Ok(Self {            state: Mutex::new(ClientState::Connecting {                transport: Some(transport),            }),            stdio_process: None,            transport_recipe,            initialize_context: Mutex::new(None),            session_recovery_lock: Semaphore::new(/*permits*/ 1),            elicitation_pause_state: ElicitationPauseState::new(),        })    }    pub async fn new_stdio_client(        program: OsString,        args: Vec<OsString>,        env: Option<HashMap<OsString, OsString>>,        env_vars: &[McpServerEnvVar],        cwd: Option<PathBuf>,        launcher: Arc<dyn StdioServerLauncher>,    ) -> io::Result<Self> {        let transport_recipe = TransportRecipe::Stdio {            command: StdioServerCommand::new(program, args, env, env_vars.to_vec(), cwd),            launcher,        };        let transport = Self::create_pending_transport(&transport_recipe)            .await            .map_err(io::Error::other)?;        let stdio_process = match &transport {            PendingTransport::Stdio { transport } => Some(transport.process_handle()),            PendingTransport::InProcess { .. }            | PendingTransport::StreamableHttp { .. }            | PendingTransport::StreamableHttpWithOAuth { .. } => None,        };        Ok(Self {            state: Mutex::new(ClientState::Connecting {                transport: Some(transport),            }),            stdio_process,            transport_recipe,            initialize_context: Mutex::new(None),            session_recovery_lock: Semaphore::new(/*permits*/ 1),            elicitation_pause_state: ElicitationPauseState::new(),        })    }    #[allow(clippy::too_many_arguments)]    pub async fn new_streamable_http_client(        server_name: &str,        url: &str,        bearer_token: Option<String>,        http_headers: Option<HashMap<String, String>>,        env_http_headers: Option<HashMap<String, String>>,        store_mode: OAuthCredentialsStoreMode,        keyring_backend_kind: AuthKeyringBackendKind,        http_client: Arc<dyn HttpClient>,        auth_provider: Option<SharedAuthProvider>,    ) -> Result<Self> {        let transport_recipe = TransportRecipe::StreamableHttp {            server_name: server_name.to_string(),            url: url.to_string(),            bearer_token,            http_headers,            env_http_headers,            store_mode,            keyring_backend_kind,            http_client,            auth_provider,        };        let transport = Self::create_pending_transport(&transport_recipe).await?;        Ok(Self {            state: Mutex::new(ClientState::Connecting {                transport: Some(transport),            }),            stdio_process: None,            transport_recipe,            initialize_context: Mutex::new(None),            session_recovery_lock: Semaphore::new(/*permits*/ 1),            elicitation_pause_state: ElicitationPauseState::new(),        })    }    /// Perform the initialization handshake with the MCP server.    /// https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#initialization    pub async fn initialize(        &self,        params: InitializeRequestParams,        timeout: Option<Duration>,        send_elicitation: SendElicitation,    ) -> Result<InitializeResult> {        let client_service = ElicitationClientService::new(            params.clone(),            send_elicitation,            self.elicitation_pause_state.clone(),        );        let pending_transport = {            let mut guard = self.state.lock().await;            match &mut *guard {                ClientState::Connecting { transport } => match transport.take() {                    Some(transport) => transport,                    None => return Err(anyhow!("client already initializing")),                },                ClientState::Ready { .. } => return Err(anyhow!("client already initialized")),                ClientState::Closed => return Err(anyhow!("MCP client is shut down")),            }        };        let (service, oauth_persistor) = self            .connect_pending_transport_with_initialize_retries(                pending_transport,                client_service.clone(),                timeout,            )            .await?;        let initialize_result_rmcp = service            .peer()            .peer_info()            .ok_or_else(|| anyhow!("handshake succeeded but server info was missing"))?;        let initialize_result = initialize_result_rmcp.clone();        {            let mut initialize_context = self.initialize_context.lock().await;            *initialize_context = Some(InitializeContext {                timeout,                client_service,            });        }        {            let mut guard = self.state.lock().await;            if matches!(*guard, ClientState::Closed) {                return Err(anyhow!("MCP client is shut down"));            }            *guard = ClientState::Ready {                service,                oauth: oauth_persistor.clone(),            };        }        if let Some(runtime) = oauth_persistor            && let Err(error) = runtime.persist_if_needed().await        {            warn!("failed to persist OAuth tokens after initialize: {error}");        }        Ok(initialize_result)    }    pub async fn list_tools(        &self,        params: Option<PaginatedRequestParams>,        timeout: Option<Duration>,    ) -> Result<ListToolsResult> {        self.refresh_oauth_if_needed().await;        let result = self            .run_service_operation("tools/list", timeout, move |service| {                let params = params.clone();                async move { service.list_tools(params).await }.boxed()            })            .await?;        self.persist_oauth_tokens().await;        Ok(result)    }    pub async fn list_tools_with_connector_ids(        &self,        params: Option<PaginatedRequestParams>,        timeout: Option<Duration>,    ) -> Result<ListToolsWithConnectorIdResult> {        self.refresh_oauth_if_needed().await;        let result = self            .run_service_operation("tools/list", timeout, move |service| {                let params = params.clone();                async move { service.list_tools(params).await }.boxed()            })            .await?;        let tools = result            .tools            .into_iter()            .map(|tool| {                let meta = tool.meta.as_ref();                let connector_id = Self::meta_string(meta, "connector_id");                let connector_name = Self::meta_string(meta, "connector_name")                    .or_else(|| Self::meta_string(meta, "connector_display_name"));                let connector_description = Self::meta_string(meta, "connector_description")                    .or_else(|| Self::meta_string(meta, "connectorDescription"));                Ok(ToolWithConnectorId {                    tool,                    connector_id,                    connector_name,                    connector_description,                })            })            .collect::<Result<Vec<_>>>()?;        self.persist_oauth_tokens().await;        Ok(ListToolsWithConnectorIdResult {            next_cursor: result.next_cursor,            tools,        })    }    fn meta_string(meta: Option<&rmcp::model::Meta>, key: &str) -> Option<String> {        meta.and_then(|meta| meta.get(key))            .and_then(Value::as_str)            .map(str::trim)            .filter(|value| !value.is_empty())            .map(str::to_string)    }    pub async fn list_resources(        &self,        params: Option<PaginatedRequestParams>,        timeout: Option<Duration>,    ) -> Result<ListResourcesResult> {        self.refresh_oauth_if_needed().await;        let result = self            .run_service_operation("resources/list", timeout, move |service| {                let params = params.clone();                async move { service.list_resources(params).await }.boxed()            })            .await?;        self.persist_oauth_tokens().await;        Ok(result)    }    pub async fn list_resource_templates(        &self,        params: Option<PaginatedRequestParams>,        timeout: Option<Duration>,    ) -> Result<ListResourceTemplatesResult> {        self.refresh_oauth_if_needed().await;        let result = self            .run_service_operation("resources/templates/list", timeout, move |service| {                let params = params.clone();                async move { service.list_resource_templates(params).await }.boxed()            })            .await?;        self.persist_oauth_tokens().await;        Ok(result)    }    pub async fn read_resource(        &self,        params: ReadResourceRequestParams,        timeout: Option<Duration>,    ) -> Result<ReadResourceResult> {        self.refresh_oauth_if_needed().await;        let result = self            .run_service_operation("resources/read", timeout, move |service| {                let params = params.clone();                async move { service.read_resource(params).await }.boxed()            })            .await?;        self.persist_oauth_tokens().await;        Ok(result)    }    pub async fn call_tool(        &self,        name: String,        arguments: Option<serde_json::Value>,        meta: Option<serde_json::Value>,        timeout: Option<Duration>,    ) -> Result<CallToolResult> {        self.refresh_oauth_if_needed().await;        let arguments = match arguments {            Some(Value::Object(map)) => Some(map),            Some(other) => {                return Err(anyhow!(                    "MCP tool arguments must be a JSON object, got {other}"                ));            }            None => None,        };        let meta = match meta {            Some(Value::Object(map)) => Some(rmcp::model::Meta(map)),            Some(other) => {                return Err(anyhow!(                    "MCP tool request _meta must be a JSON object, got {other}"                ));            }            None => None,        };        let mut rmcp_params = CallToolRequestParams::new(name);        rmcp_params.arguments = arguments;        let result = self            .run_service_operation("tools/call", timeout, move |service| {                let rmcp_params = rmcp_params.clone();                let meta = meta.clone();                async move {                    let mut options = rmcp::service::PeerRequestOptions::no_options();                    options.meta = meta;                    let result = service                        .peer()                        .send_request_with_option(                            ClientRequest::CallToolRequest(rmcp::model::CallToolRequest::new(                                rmcp_params,                            )),                            options,                        )                        .await?                        .await_response()                        .await?;                    match result {                        ServerResult::CallToolResult(result) => Ok(result),                        _ => Err(rmcp::service::ServiceError::UnexpectedResponse),                    }                }                .boxed()            })            .await?;        self.persist_oauth_tokens().await;        Ok(result)    }    pub async fn send_custom_notification(        &self,        method: &str,        params: Option<serde_json::Value>,    ) -> Result<()> {        self.refresh_oauth_if_needed().await;        self.run_service_operation(            "notifications/custom",            /*timeout*/ None,            move |service| {                let params = params.clone();                async move {                    service                        .send_notification(ClientNotification::CustomNotification(                            CustomNotification {                                method: method.to_string(),                                params,                                extensions: Extensions::new(),                            },                        ))                        .await                }                .boxed()            },        )        .await?;        self.persist_oauth_tokens().await;        Ok(())    }    pub async fn send_custom_request(        &self,        method: &str,        params: Option<serde_json::Value>,    ) -> Result<ServerResult> {        self.refresh_oauth_if_needed().await;        let response = self            .run_service_operation("requests/custom", /*timeout*/ None, move |service| {                let params = params.clone();                async move {                    service                        .send_request(ClientRequest::CustomRequest(CustomRequest::new(                            method, params,                        )))                        .await                }                .boxed()            })            .await?;        self.persist_oauth_tokens().await;        Ok(response)    }    async fn service(&self) -> Result<Arc<RunningService<RoleClient, ElicitationClientService>>> {        let guard = self.state.lock().await;        match &*guard {            ClientState::Ready { service, .. } => Ok(Arc::clone(service)),            ClientState::Connecting { .. } => Err(anyhow!("MCP client not initialized")),            ClientState::Closed => Err(anyhow!("MCP client is shut down")),        }    }    async fn oauth_persistor(&self) -> Option<OAuthPersistor> {        let guard = self.state.lock().await;        match &*guard {            ClientState::Ready {                oauth: Some(runtime),                ..            } => Some(runtime.clone()),            _ => None,        }    }    /// Stop the MCP transport and any stdio server process owned by this client.    pub async fn shutdown(&self) {        let previous_state = {            let mut guard = self.state.lock().await;            std::mem::replace(&mut *guard, ClientState::Closed)        };        if let Some(process) = &self.stdio_process            && let Err(error) = process.terminate().await        {            warn!("failed to terminate MCP stdio server process: {error}");        }        drop(previous_state);    }    /// This should be called after every tool call so that if a given tool call triggered    /// a refresh of the OAuth tokens, they are persisted.    async fn persist_oauth_tokens(&self) {        if let Some(runtime) = self.oauth_persistor().await            && let Err(error) = runtime.persist_if_needed().await        {            warn!("failed to persist OAuth tokens: {error}");        }    }    async fn refresh_oauth_if_needed(&self) {        if let Some(runtime) = self.oauth_persistor().await            && let Err(error) = runtime.refresh_if_needed().await        {            warn!("failed to refresh OAuth tokens: {error}");        }    }    async fn create_pending_transport(        transport_recipe: &TransportRecipe,    ) -> Result<PendingTransport> {        match transport_recipe {            TransportRecipe::InProcess { factory } => {                let transport = factory.open().await?;                Ok(PendingTransport::InProcess { transport })            }            TransportRecipe::Stdio { command, launcher } => {                let transport = launcher.launch(command.clone()).await?;                Ok(PendingTransport::Stdio { transport })            }            TransportRecipe::StreamableHttp {                server_name,                url,                bearer_token,                http_headers,                env_http_headers,                store_mode,                keyring_backend_kind,                http_client,                auth_provider,            } => {                let default_headers =                    build_default_headers(http_headers.clone(), env_http_headers.clone())?;                let initial_oauth_tokens = if bearer_token.is_none()                    && auth_provider.is_none()                    && !default_headers.contains_key(AUTHORIZATION)                {                    match load_oauth_tokens(server_name, url, *store_mode, *keyring_backend_kind) {                        Ok(tokens) => tokens,                        Err(err) => {                            warn!("failed to read tokens for server `{server_name}`: {err}");                            None                        }                    }                } else {                    None                };                if let Some(initial_tokens) = initial_oauth_tokens.clone() {                    match create_oauth_transport_and_runtime(                        server_name,                        url,                        initial_tokens.clone(),                        *store_mode,                        *keyring_backend_kind,                        default_headers.clone(),                        Arc::clone(http_client),                    )                    .await                    {                        Ok((transport, oauth_persistor)) => {                            Ok(PendingTransport::StreamableHttpWithOAuth {                                transport,                                oauth_persistor,                            })                        }                        Err(err)                            if err.downcast_ref::<AuthError>().is_some_and(|auth_err| {                                matches!(auth_err, AuthError::NoAuthorizationSupport)                            }) =>                        {                            let access_token = initial_tokens                                .token_response                                .0                                .access_token()                                .secret()                                .to_string();                            warn!(                                "OAuth metadata discovery is unavailable for MCP server `{server_name}`; falling back to stored bearer token authentication"                            );                            let http_config =                                StreamableHttpClientTransportConfig::with_uri(url.clone())                                    .auth_header(access_token);                            let transport = StreamableHttpClientTransport::with_client(                                StreamableHttpClientAdapter::new(                                    Arc::clone(http_client),                                    default_headers,                                    /*auth_provider*/ None,                                ),                                http_config,                            );                            Ok(PendingTransport::StreamableHttp { transport })                        }                        Err(err) => Err(err),                    }                } else {                    let mut http_config =                        StreamableHttpClientTransportConfig::with_uri(url.clone());                    if let Some(bearer_token) = bearer_token.clone() {                        http_config = http_config.auth_header(bearer_token);                    }                    let transport = StreamableHttpClientTransport::with_client(                        StreamableHttpClientAdapter::new(                            Arc::clone(http_client),                            default_headers,                            auth_provider.clone(),                        ),                        http_config,                    );                    Ok(PendingTransport::StreamableHttp { transport })                }            }        }    }    async fn connect_pending_transport(        pending_transport: PendingTransport,        client_service: ElicitationClientService,        timeout: Option<Duration>,    ) -> Result<(        Arc<RunningService<RoleClient, ElicitationClientService>>,        Option<OAuthPersistor>,    )> {        let (transport, oauth_persistor) = match pending_transport {            PendingTransport::InProcess { transport } => (                service::serve_client(client_service, transport).boxed(),                None,            ),            PendingTransport::Stdio { transport } => (                service::serve_client(client_service, transport).boxed(),                None,            ),            PendingTransport::StreamableHttp { transport } => (                service::serve_client(client_service, transport).boxed(),                None,            ),            PendingTransport::StreamableHttpWithOAuth {                transport,                oauth_persistor,            } => (                service::serve_client(client_service, transport).boxed(),                Some(oauth_persistor),            ),        };        let service_result = match timeout {            Some(duration) => match time::timeout(duration, transport).await {                Ok(result) => {                    result.map_err(|source| anyhow::Error::from(HandshakeError { source }))                }                Err(_elapsed) => Err(anyhow!(                    "timed out handshaking with MCP server after {duration:?}"                )),            },            None => transport                .await                .map_err(|source| anyhow::Error::from(HandshakeError { source })),        };        let service = match service_result {            Ok(service) => service,            Err(error) => {                if let Some(runtime) = oauth_persistor.as_ref()                    && let Err(persist_error) = runtime.persist_if_needed().await                {                    warn!(                        "failed to persist OAuth tokens after failed initialize: {persist_error}"                    );                }                return Err(error);            }        };        Ok((Arc::new(service), oauth_persistor))    }    async fn run_service_operation<T, F, Fut>(        &self,        label: &str,        timeout: Option<Duration>,        operation: F,    ) -> Result<T>    where        F: Fn(Arc<RunningService<RoleClient, ElicitationClientService>>) -> Fut,        Fut: std::future::Future<Output = std::result::Result<T, rmcp::service::ServiceError>>,    {        let service = self.service().await?;        match Self::run_service_operation_with_transient_retries(            Arc::clone(&service),            label,            timeout,            self.elicitation_pause_state.clone(),            &operation,        )        .await        {            Ok(result) => Ok(result),            Err(error) if Self::is_session_expired_404(&error) => {                self.reinitialize_after_session_expiry(&service).await?;                let recovered_service = self.service().await?;                Self::run_service_operation_with_transient_retries(                    recovered_service,                    label,                    timeout,                    self.elicitation_pause_state.clone(),                    &operation,                )                .await                .map_err(Into::into)            }            Err(error) => Err(error.into()),        }    }    async fn run_service_operation_with_transient_retries<T, F, Fut>(        service: Arc<RunningService<RoleClient, ElicitationClientService>>,        label: &str,        timeout: Option<Duration>,        pause_state: ElicitationPauseState,        operation: &F,    ) -> std::result::Result<T, ClientOperationError>    where        F: Fn(Arc<RunningService<RoleClient, ElicitationClientService>>) -> Fut,        Fut: std::future::Future<Output = std::result::Result<T, rmcp::service::ServiceError>>,    {        let retry_deadline = timeout.map(|duration| Instant::now() + duration);        for (attempt, retry_delay_ms) in STREAMABLE_HTTP_RETRY_DELAYS_MS            .iter()            .copied()            .map(Some)            .chain(std::iter::once(None))            .enumerate()        {            let attempt_timeout = remaining_operation_timeout(label, timeout, retry_deadline)?;            match Self::run_service_operation_once(                Arc::clone(&service),                label,                attempt_timeout,                pause_state.clone(),                operation,            )            .await            {                Ok(result) => return Ok(result),                Err(error) if Self::is_retryable_tools_list_error(label, &error) => {                    let Some(retry_delay_ms) = retry_delay_ms else {                        return Err(error);                    };                    let delay = Duration::from_millis(retry_delay_ms);                    warn!(                        attempt = attempt + 1,                        max_attempts = STREAMABLE_HTTP_RETRY_DELAYS_MS.len() + 1,                        delay_ms = delay.as_millis(),                        error = %error,                        "streamable HTTP MCP tools/list failed with a retryable error; retrying"                    );                    if !sleep_with_retry_deadline(delay, retry_deadline).await {                        return Err(ClientOperationError::Timeout {                            label: label.to_string(),                            duration: timeout.unwrap_or(delay),                        });                    }                }                Err(error) => return Err(error),            }        }        unreachable!("service operation retry loop should return on success or final error")    }    async fn run_service_operation_once<T, F, Fut>(        service: Arc<RunningService<RoleClient, ElicitationClientService>>,        label: &str,        timeout: Option<Duration>,        pause_state: ElicitationPauseState,        operation: &F,    ) -> std::result::Result<T, ClientOperationError>    where        F: Fn(Arc<RunningService<RoleClient, ElicitationClientService>>) -> Fut,        Fut: std::future::Future<Output = std::result::Result<T, rmcp::service::ServiceError>>,    {        match timeout {            Some(duration) => {                active_time_timeout(duration, pause_state.subscribe(), operation(service))                    .await                    .map_err(|_| ClientOperationError::Timeout {                        label: label.to_string(),                        duration,                    })?                    .map_err(ClientOperationError::from)            }            None => operation(service).await.map_err(ClientOperationError::from),        }    }    fn is_retryable_tools_list_error(label: &str, error: &ClientOperationError) -> bool {        if label != "tools/list" {            return false;        }        let ClientOperationError::Service(rmcp::service::ServiceError::TransportSend(error)) =            error        else {            return false;        };        error            .error            .downcast_ref::<StreamableHttpError<StreamableHttpClientAdapterError>>()            .is_some_and(Self::is_retryable_streamable_http_error)    }    fn is_session_expired_404(error: &ClientOperationError) -> bool {        let ClientOperationError::Service(rmcp::service::ServiceError::TransportSend(error)) =            error        else {            return false;        };        error            .error            .downcast_ref::<StreamableHttpError<StreamableHttpClientAdapterError>>()            .is_some_and(|error| {                matches!(                    error,                    StreamableHttpError::Client(                        StreamableHttpClientAdapterError::SessionExpired404                    )                )            })    }    async fn reinitialize_after_session_expiry(        &self,        failed_service: &Arc<RunningService<RoleClient, ElicitationClientService>>,    ) -> Result<()> {        let _recovery_guard = self            .session_recovery_lock            .acquire()            .await            .map_err(|_| anyhow!("MCP client recovery semaphore closed"))?;        {            let guard = self.state.lock().await;            match &*guard {                ClientState::Ready { service, .. } if !Arc::ptr_eq(service, failed_service) => {                    return Ok(());                }                ClientState::Ready { .. } => {}                ClientState::Connecting { .. } => {                    return Err(anyhow!("MCP client not initialized"));                }                ClientState::Closed => {                    return Err(anyhow!("MCP client is shut down"));                }            }        }        let initialize_context = self            .initialize_context            .lock()            .await            .clone()            .ok_or_else(|| anyhow!("MCP client cannot recover before initialize succeeds"))?;        let pending_transport = Self::create_pending_transport(&self.transport_recipe).await?;        let (service, oauth_persistor) = self            .connect_pending_transport_with_initialize_retries(                pending_transport,                initialize_context.client_service,                initialize_context.timeout,            )            .await?;        {            let mut guard = self.state.lock().await;            if matches!(*guard, ClientState::Closed) {                return Err(anyhow!("MCP client is shut down"));            }            *guard = ClientState::Ready {                service,                oauth: oauth_persistor.clone(),            };        }        if let Some(runtime) = oauth_persistor            && let Err(error) = runtime.persist_if_needed().await        {            warn!("failed to persist OAuth tokens after session recovery: {error}");        }        Ok(())    }}async fn create_oauth_transport_and_runtime(    server_name: &str,    url: &str,    initial_tokens: StoredOAuthTokens,    credentials_store: OAuthCredentialsStoreMode,    keyring_backend_kind: AuthKeyringBackendKind,    default_headers: HeaderMap,    http_client: Arc<dyn HttpClient>,) -> Result<(    StreamableHttpClientTransport<AuthClient<StreamableHttpClientAdapter>>,    OAuthPersistor,)> {    let mut builder = apply_default_headers(reqwest::Client::builder(), &default_headers);    if let Some(tls_config) = maybe_build_rustls_client_config_with_custom_ca()? {        builder = builder.tls_backend_preconfigured(tls_config.as_ref().clone());    }    let oauth_metadata_client = builder.build()?;    // TODO(aibrahim): teach OAuth bootstrap and refresh to use the same    // shared HTTP client abstraction instead of always creating the local    // reqwest metadata client here.    let mut oauth_state =        OAuthState::new(url.to_string(), Some(oauth_metadata_client.clone())).await?;    oauth_state        .set_credentials(            &initial_tokens.client_id,            initial_tokens.token_response.0.clone(),        )        .await?;    let manager = match oauth_state {        OAuthState::Authorized(manager) => manager,        OAuthState::Unauthorized(manager) => manager,        _ => {            return Err(anyhow!("unexpected OAuth state during client setup"));        }    };    let auth_client = AuthClient::new(        StreamableHttpClientAdapter::new(http_client, default_headers, /*auth_provider*/ None),        manager,    );    let auth_manager = auth_client.auth_manager.clone();    let transport = StreamableHttpClientTransport::with_client(        auth_client,        StreamableHttpClientTransportConfig::with_uri(url.to_string()),    );    let runtime = OAuthPersistor::new(        server_name.to_string(),        url.to_string(),        auth_manager,        credentials_store,        keyring_backend_kind,        Some(initial_tokens),    );    Ok((transport, runtime))}#[cfg(test)]mod tests {    use std::time::Duration;    use pretty_assertions::assert_eq;    use tokio::time;    use super::*;    #[tokio::test]    async fn active_time_timeout_pauses_while_elicitation_is_pending() {        let pause_state = ElicitationPauseState::new();        let pause = pause_state.enter();        tokio::spawn(async move {            time::sleep(Duration::from_millis(75)).await;            drop(pause);        });        let result =            active_time_timeout(Duration::from_millis(50), pause_state.subscribe(), async {                time::sleep(Duration::from_millis(90)).await;                "done"            })            .await;        assert_eq!(Ok("done"), result);    }}