use rmcp::{
RoleClient, ServiceExt,
model::{ClientInfo, InitializeRequestParams},
service::RunningService,
transport::{
AuthClient, AuthorizationManager, CredentialStore, StreamableHttpClientTransport,
auth::{OAuthClientConfig, OAuthState},
streamable_http_client::StreamableHttpClientTransportConfig,
},
};
use tracing::{debug, info};
use crate::{
auth_handler,
cred_store::FullCredStore,
result::{ClientConnectError, MapAuthToConnectError},
};
use super::{Client, Connected, Disconnected};
const MCP_SERVER_URL: &str = "https://mcp.montrose.io";
impl Client<Disconnected> {
pub async fn connect(self) -> Result<Client<Connected>, ClientConnectError> {
let auth_mgr = self.authenticate().await?;
let mut mcp_client_res = self.init_mcp_client(auth_mgr).await;
if let Err(rmcp::service::ClientInitializeError::TransportError {
error: dyn_transport_err,
context: _,
}) = &mcp_client_res
{
debug!("Transport error: {dyn_transport_err:#?}");
if Self::is_auth_required_error(dyn_transport_err) {
info!("Authentication required error encountered");
info!("Starting new authorization flow");
let auth_mgr = self.authenticate_new_auth().await?;
mcp_client_res = self.init_mcp_client(auth_mgr).await;
}
}
let mcp_client = mcp_client_res.map_err(|e| ClientConnectError::ConnectionError {
msg: "Failed to connect to MCP server".to_string(),
source: Some(e.into()),
})?;
info!("Successfully connected to MCP server");
Ok(Client {
client_name: self.client_name,
auth_handler: self.auth_handler,
cred_store: self.cred_store,
state: Connected { client: mcp_client },
})
}
fn is_auth_required_error(dyn_transport_err: &rmcp::transport::DynamicTransportError) -> bool {
let http_error = dyn_transport_err
.error
.downcast_ref::<rmcp::transport::streamable_http_client::StreamableHttpError<
reqwest::Error,
>>();
matches!(
http_error,
Some(
rmcp::transport::streamable_http_client::StreamableHttpError::Auth(
rmcp::transport::AuthError::AuthorizationRequired,
) | rmcp::transport::streamable_http_client::StreamableHttpError::AuthRequired(
rmcp::transport::streamable_http_client::AuthRequiredError { .. }
)
)
)
}
async fn init_mcp_client(
&self,
auth_mgr: AuthorizationManager,
) -> Result<
RunningService<RoleClient, InitializeRequestParams>,
rmcp::service::ClientInitializeError,
> {
let auth_client = AuthClient::new(reqwest::Client::default(), auth_mgr);
let transport = StreamableHttpClientTransport::with_client(
auth_client,
StreamableHttpClientTransportConfig::with_uri(MCP_SERVER_URL),
);
let client_service = ClientInfo::default();
client_service.serve(transport).await
}
async fn authenticate(&self) -> Result<AuthorizationManager, ClientConnectError> {
debug!("Using MCP server URL: {}", MCP_SERVER_URL);
info!("Establishing authorized connection to MCP server...");
let mut auth_mgr = AuthorizationManager::new(MCP_SERVER_URL)
.await
.map_err(|e| ClientConnectError::AuthError {
msg: "Failed to initialize authorization manager".to_string(),
source: Some(e.into()),
})?;
auth_mgr.set_credential_store(self.cred_store.clone());
let initialized = Self::init_from_store_with_secret(
&mut auth_mgr,
self.cred_store.clone(),
self.auth_handler.redirect_uri(),
)
.await?;
if initialized {
info!("Initialized authorization manager from credential store");
return Ok(auth_mgr);
}
info!(
"No usable credentials found in the credential store. Starting new authorization flow.",
);
self.authenticate_new_auth().await
}
async fn init_from_store_with_secret(
auth_mgr: &mut AuthorizationManager,
cred_store: impl FullCredStore,
redirect_uri: impl Into<String>,
) -> Result<bool, ClientConnectError> {
let creds = cred_store
.load()
.await
.to_connect_err("Error while loading credentials from credential store")?;
let client_secret = cred_store
.load_client_secret()
.await
.to_connect_err("Error while loading client secret from credential store")?;
let (Some(creds), Some(client_secret)) = (creds, client_secret) else {
return Ok(false);
};
let oauth_config = OAuthClientConfig::new(creds.client_id, redirect_uri)
.with_scopes(creds.granted_scopes)
.with_client_secret(client_secret);
let metadata = auth_mgr
.discover_metadata()
.await
.to_connect_err("Failed to discover authorization server metadata")?;
auth_mgr.set_metadata(metadata);
auth_mgr
.configure_client(oauth_config)
.to_connect_err("Failed to configure authorization manager with client credentials")?;
Ok(true)
}
async fn authenticate_new_auth(&self) -> Result<AuthorizationManager, ClientConnectError> {
let wanted_scopes = &["mcp"];
debug!("Requesting scopes: {:?}", wanted_scopes);
let redirect_uri = self.auth_handler.redirect_uri();
debug!("Using redirect URI: {}", redirect_uri);
let (mut oauth_state, client_secret) = Self::start_authorization_with_secret(
wanted_scopes,
redirect_uri,
&self.client_name,
self.cred_store.clone(),
)
.await
.map_err(|e| ClientConnectError::AuthError {
msg: "Failed to start authorization".to_string(),
source: Some(e.into()),
})?;
let auth_url = oauth_state.get_authorization_url().await.map_err(|e| {
ClientConnectError::AuthError {
msg: "Failed to get authorization URL".to_string(),
source: Some(e.into()),
}
})?;
debug!("Auth URL: {}", auth_url);
info!("Waiting for authorization code...");
let auth_handler::AuthGrant {
code: auth_code,
state: csrf_token,
} = self.auth_handler.authenticate(&auth_url).await?;
info!("Received authorization code: {}", auth_code);
info!("Exchanging authorization code for access token...");
oauth_state
.handle_callback(&auth_code, &csrf_token)
.await
.to_connect_err("Failed to handle authorization callback")?;
self.cred_store
.save_client_secret(&client_secret.unwrap())
.await
.unwrap();
info!("Authorization successful! Access token obtained.");
let auth_mgr = oauth_state.into_authorization_manager().ok_or_else(|| {
ClientConnectError::AuthError {
msg: "Failed to convert OAuth state into authorization manager".to_string(),
source: None,
}
})?;
Ok(auth_mgr)
}
async fn start_authorization_with_secret<S: CredentialStore + FullCredStore + 'static>(
scopes: &[&str],
redirect_uri: &str,
client_name: &str,
cred_store: S,
) -> Result<(OAuthState, Option<String>), rmcp::transport::AuthError> {
let mut auth_mgr = AuthorizationManager::new(MCP_SERVER_URL).await?;
auth_mgr.set_credential_store(cred_store);
debug!("start discovery");
let metadata = auth_mgr.discover_metadata().await?;
auth_mgr.set_metadata(metadata);
let _ = auth_mgr.select_scopes(None, scopes);
debug!("start session");
let oauth_config = auth_mgr
.register_client(client_name, redirect_uri, scopes)
.await?;
let auth_url = auth_mgr.get_authorization_url(scopes).await?;
let session = rmcp::transport::AuthorizationSession::for_scope_upgrade(
auth_mgr,
auth_url,
redirect_uri,
);
let oauth_state = OAuthState::Session(session);
Ok((oauth_state, oauth_config.client_secret))
}
}