use std::sync::Arc;
use rho_providers::credentials::CredentialStore as RhoCredentialStore;
use rmcp::transport::auth::{AuthError, CredentialStore as RmcpCredentialStore, StoredCredentials};
const ACCOUNT_PREFIX: &str = "mcp-oauth";
pub(crate) fn account_name(identity: &str) -> String {
format!("{ACCOUNT_PREFIX}:{identity}")
}
#[derive(Clone)]
pub(crate) struct McpOAuthCredentialStore {
account: String,
store: Arc<dyn RhoCredentialStore>,
}
impl McpOAuthCredentialStore {
pub(crate) fn new(identity: &str, store: Arc<dyn RhoCredentialStore>) -> Self {
Self {
account: account_name(identity),
store,
}
}
fn read(&self) -> Result<Option<StoredCredentials>, AuthError> {
let raw = self
.store
.get_secret(&self.account)
.map_err(internal_error)?;
let Some(raw) = raw else {
return Ok(None);
};
match serde_json::from_str::<StoredCredentials>(&raw) {
Ok(stored) => Ok(Some(stored)),
Err(error) => {
tracing::warn!(
account = %self.account,
error = %error,
"stored MCP OAuth credentials could not be read; re-authorization is required"
);
Ok(None)
}
}
}
fn write(&self, credentials: &StoredCredentials) -> Result<(), AuthError> {
let raw = serde_json::to_string(credentials).map_err(internal_error)?;
self.store
.set_secret(&self.account, &raw)
.map_err(internal_error)
}
fn remove(&self) -> Result<(), AuthError> {
self.store
.delete_secret(&self.account)
.map(|_| ())
.map_err(internal_error)
}
async fn off_runtime<T, F>(&self, operation: F) -> Result<T, AuthError>
where
T: Send + 'static,
F: FnOnce(&Self) -> Result<T, AuthError> + Send + 'static,
{
let store = self.clone();
tokio::task::spawn_blocking(move || operation(&store))
.await
.map_err(internal_error)?
}
}
fn internal_error(error: impl std::fmt::Display) -> AuthError {
AuthError::InternalError(error.to_string())
}
#[async_trait::async_trait]
impl RmcpCredentialStore for McpOAuthCredentialStore {
async fn load(&self) -> Result<Option<StoredCredentials>, AuthError> {
self.off_runtime(Self::read).await
}
async fn save(&self, credentials: StoredCredentials) -> Result<(), AuthError> {
self.off_runtime(move |store| store.write(&credentials))
.await
}
async fn clear(&self) -> Result<(), AuthError> {
self.off_runtime(Self::remove).await
}
}
#[cfg(test)]
#[path = "store_tests.rs"]
mod tests;