use rmcp::{RoleClient, model::InitializeRequestParams, service::RunningService};
use crate::{
auth_handler::{AuthHandler, BrowserAuth},
cred_store::{FullCredStore, SharedCredStore, keyring_cred_store::KeyringCredStore},
result::ClientBuildError,
};
mod connected;
mod disconnected;
#[derive(Debug)]
pub struct Client<S: State = Disconnected> {
client_name: String,
auth_handler: Box<dyn AuthHandler>,
cred_store: SharedCredStore,
state: S,
}
mod private {
pub trait Sealed {}
}
pub trait State: private::Sealed {}
#[derive(Debug)]
pub struct Disconnected;
#[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>>,
cred_user: Option<String>,
cred_store: Option<SharedCredStore>,
}
impl ClientBuilder {
pub fn new(client_name: impl Into<String>) -> Self {
Self {
client_name: client_name.into(),
auth_handler: None,
cred_user: None,
cred_store: None,
}
}
pub fn auth_handler(mut self, handler: impl AuthHandler + 'static) -> Self {
self.auth_handler = Some(Box::new(handler));
self
}
pub fn cred_user(mut self, user: impl Into<String>) -> Self {
self.cred_user = Some(user.into());
self
}
pub fn cred_store(mut self, cred_store: impl FullCredStore + 'static) -> Self {
self.cred_store = Some(SharedCredStore::new(cred_store));
self
}
pub async fn build(self) -> Result<Client<Disconnected>, ClientBuildError> {
let auth_handler =
match self.auth_handler {
Some(handler) => handler,
None => Box::new(BrowserAuth::new().await.map_err(|e| {
ClientBuildError::BuildError {
msg: e.to_string(),
source: Some(Box::new(e)),
}
})?),
};
let cred_store = match self.cred_store {
Some(store) => {
if self.cred_user.is_some() {
return Err(ClientBuildError::BuildError {
msg: "Cannot specify both a custom credential store and a credential user"
.to_string(),
source: None,
});
}
store
}
None => {
let cred_user = self.cred_user.unwrap_or(DEFAULT_CRED_USER.to_string());
SharedCredStore::new({
#[cfg(feature = "keyring")]
{
KeyringCredStore::new(&self.client_name, &cred_user).map_err(|e| {
ClientBuildError::BuildError {
msg: "Failed to create keyring credential store".to_string(),
source: Some(Box::new(e)),
}
})?
}
#[cfg(not(feature = "keyring"))]
{
let client_dirs =
etcetera::choose_app_strategy(etcetera::AppStrategyArgs {
top_level_domain: "".to_string(),
author: "".to_string(),
app_name: self.client_name.clone(),
})
.map_err(|e| {
ClientBuildError::BuildError {
msg: e.to_string(),
source: Some(Box::new(e)),
}
})?;
PlaintextCredStore::new(&client_dirs)
}
})
}
};
Ok(Client {
client_name: self.client_name,
auth_handler,
cred_store,
state: Disconnected,
})
}
}