use rmcp::{RoleClient, model::InitializeRequestParams, service::RunningService};
#[cfg(feature = "__dev")]
use {rmcp::transport::CredentialStore, tracing::warn};
use crate::{
auth_handler::{AuthHandler, BrowserAuth},
cred_store::{FullCredStore, SharedCredStore},
error::ClientBuildError,
};
mod connected;
mod disconnected;
#[derive(Debug)]
pub struct Client<S: State = Disconnected> {
name: String,
auth_handler: Option<Box<dyn AuthHandler>>,
cred_store: SharedCredStore,
state: S,
}
mod private {
pub trait Sealed {}
}
pub trait State: private::Sealed {}
#[doc(hidden)]
#[derive(Debug)]
pub struct Disconnected;
#[doc(hidden)]
#[derive(Debug)]
pub struct Connected {
client: RunningService<RoleClient, InitializeRequestParams>,
}
impl private::Sealed for Disconnected {}
impl private::Sealed for Connected {}
impl State for Disconnected {}
impl State for Connected {}
const DEFAULT_CRED_USER: &str = "user";
#[derive(Debug)]
pub struct ClientBuilder {
client_name: String,
auth_handler: Option<Box<dyn AuthHandler>>,
interactive_auth: bool,
cred_user: Option<String>,
cred_store: Option<SharedCredStore>,
#[cfg(feature = "__dev")]
force_token_refresh: bool,
}
impl ClientBuilder {
pub fn new(client_name: impl Into<String>) -> Self {
Self {
client_name: client_name.into(),
auth_handler: None,
interactive_auth: true,
cred_user: None,
cred_store: None,
#[cfg(feature = "__dev")]
force_token_refresh: false,
}
}
#[must_use]
pub fn auth_handler(mut self, handler: impl AuthHandler + 'static) -> Self {
self.auth_handler = Some(Box::new(handler));
self
}
#[must_use]
pub fn no_auth(mut self) -> Self {
self.interactive_auth = false;
self
}
#[must_use]
pub fn cred_user(mut self, user: impl Into<String>) -> Self {
self.cred_user = Some(user.into());
self
}
#[must_use]
pub fn cred_store(mut self, cred_store: impl FullCredStore + 'static) -> Self {
self.cred_store = Some(SharedCredStore::new(cred_store));
self
}
#[cfg(feature = "__dev")]
#[must_use]
pub fn dev_force_token_refresh(mut self) -> Self {
self.force_token_refresh = true;
self
}
pub async fn build(self) -> Result<Client<Disconnected>, ClientBuildError> {
let auth_handler = if self.interactive_auth {
Some(match self.auth_handler {
Some(handler) => handler,
None => Box::new(BrowserAuth::new().await.map_err(|e| ClientBuildError {
msg: e.to_string(),
source: Some(Box::new(e)),
})?),
})
} else {
None
};
let cred_store = if let Some(store) = self.cred_store {
if self.cred_user.is_some() {
return Err(ClientBuildError {
msg: "cannot specify both a custom credential store and a credential user"
.to_string(),
source: None,
});
}
store
} else {
let cred_user = self.cred_user.unwrap_or(DEFAULT_CRED_USER.to_string());
SharedCredStore::new({
#[cfg(feature = "keyring")]
{
use crate::cred_store::KeyringCredStore;
KeyringCredStore::new(&self.client_name, &cred_user).map_err(|e| {
ClientBuildError {
msg: "failed to create keyring credential store".to_string(),
source: Some(Box::new(e)),
}
})?
}
#[cfg(not(feature = "keyring"))]
{
use crate::cred_store::PlaintextCredStore;
use etcetera::AppStrategy;
let client_dirs = etcetera::choose_app_strategy(etcetera::AppStrategyArgs {
top_level_domain: String::new(),
author: String::new(),
app_name: self.client_name.clone(),
})
.map_err(|e| ClientBuildError {
msg: e.to_string(),
source: Some(Box::new(e)),
})?;
PlaintextCredStore::new(
client_dirs
.state_dir()
.unwrap_or_else(|| client_dirs.data_dir())
.join(format!("{cred_user}_credentials.json")),
)
}
})
};
#[cfg(feature = "__dev")]
if self.force_token_refresh {
warn!("Force token refresh enabled");
let creds = cred_store.load().await.map_err(|e| ClientBuildError {
msg: "failed to load credentials for setting forced refresh".to_string(),
source: Some(Box::new(e)),
})?;
if let Some(mut creds) = creds {
use std::time::Duration;
creds
.token_response
.as_mut()
.ok_or_else(|| ClientBuildError {
msg: "no token response in stored credentials".to_string(),
source: None,
})?
.set_expires_in(Some(&Duration::ZERO));
cred_store.save(creds).await.map_err(|e| ClientBuildError {
msg: "failed to save credentials for setting forced refresh".to_string(),
source: Some(Box::new(e)),
})?;
}
}
Ok(Client {
name: self.client_name,
auth_handler,
cred_store,
state: Disconnected,
})
}
}