use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use tower_mcp::client::McpClient;
pub type Connector = Box<
dyn Fn() -> Pin<Box<dyn Future<Output = Result<McpClient, tower_mcp::Error>> + Send>>
+ Send
+ Sync,
>;
pub struct Session {
client: RwLock<Arc<McpClient>>,
connector: Option<Connector>,
reconnecting: tokio::sync::Mutex<()>,
generation: AtomicU64,
generation_tx: tokio::sync::watch::Sender<u64>,
}
impl Session {
pub fn new(client: McpClient, connector: Option<Connector>) -> Self {
let (generation_tx, _) = tokio::sync::watch::channel(0);
Self {
client: RwLock::new(Arc::new(client)),
connector,
reconnecting: tokio::sync::Mutex::new(()),
generation: AtomicU64::new(0),
generation_tx,
}
}
pub fn client(&self) -> Arc<McpClient> {
self.client.read().unwrap().clone()
}
pub fn can_reconnect(&self) -> bool {
self.connector.is_some()
}
pub fn generation(&self) -> u64 {
self.generation.load(Ordering::Acquire)
}
pub fn subscribe_generation(&self) -> tokio::sync::watch::Receiver<u64> {
self.generation_tx.subscribe()
}
pub async fn shutdown(self) -> Result<(), tower_mcp::Error> {
let client = self
.client
.into_inner()
.expect("session client lock poisoned");
let client = Arc::try_unwrap(client).map_err(|_| {
tower_mcp::Error::Transport(
"cannot shut down an MCP session while its client is still in use".to_string(),
)
})?;
client.shutdown().await
}
pub async fn reconnect(&self, seen: u64) -> Result<(), tower_mcp::Error> {
let Some(connector) = &self.connector else {
return Err(tower_mcp::Error::Transport(
"this transport cannot be reconnected".to_string(),
));
};
let _guard = self.reconnecting.lock().await;
if self.generation() != seen {
return Ok(());
}
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
let fresh = connector().await?;
*self.client.write().unwrap() = Arc::new(fresh);
let generation = self.generation.fetch_add(1, Ordering::AcqRel) + 1;
self.generation_tx.send_replace(generation);
Ok(())
}
}
pub fn is_not_initialized(e: &tower_mcp::Error) -> bool {
matches!(
e,
tower_mcp::Error::JsonRpc(j)
if j.code == -32600 && j.message.contains("notifications/initialized")
)
}
pub fn is_session_lost(e: &tower_mcp::Error) -> bool {
if matches!(e, tower_mcp::Error::SessionExpired) || is_not_initialized(e) {
return true;
}
match e {
tower_mcp::Error::Transport(msg) => {
msg.contains("Transport closed")
|| msg.contains("Connection closed")
|| msg.contains("HTTP 410")
|| msg.contains("HTTP 502")
|| msg.contains("HTTP 503")
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn jsonrpc(code: i32, message: &str) -> tower_mcp::Error {
tower_mcp::Error::JsonRpc(tower_mcp::error::JsonRpcError {
code,
message: message.to_string(),
data: None,
})
}
#[test]
fn session_loss_covers_the_documented_conditions() {
assert!(is_session_lost(&tower_mcp::Error::SessionExpired));
assert!(is_session_lost(&jsonrpc(
-32600,
"Client must send notifications/initialized before making requests"
)));
assert!(is_session_lost(&tower_mcp::Error::Transport(
"Transport closed".into()
)));
assert!(is_session_lost(&tower_mcp::Error::Transport(
"Connection closed".into()
)));
for status in ["HTTP 410 Gone", "HTTP 502 Bad Gateway", "HTTP 503"] {
assert!(
is_session_lost(&tower_mcp::Error::Transport(format!(
"{status} from server: "
))),
"{status} should count as session loss"
);
}
}
#[test]
fn session_loss_does_not_swallow_real_errors() {
assert!(!is_session_lost(&tower_mcp::Error::Transport(
"HTTP 401 Unauthorized from server: bad token".into()
)));
assert!(!is_session_lost(&tower_mcp::Error::Transport(
"HTTP 404 from http://x/mcp: MCP endpoint not found".into()
)));
assert!(!is_session_lost(&jsonrpc(-32602, "Invalid params")));
assert!(!is_session_lost(&tower_mcp::Error::tool("boom")));
}
#[test]
fn detects_not_initialized_startup_error() {
assert!(is_not_initialized(&jsonrpc(
-32600,
"Client must send notifications/initialized before making requests"
)));
}
#[test]
fn does_not_match_unrelated_errors() {
assert!(!is_not_initialized(&jsonrpc(
-32600,
"some other invalid request"
)));
assert!(!is_not_initialized(&jsonrpc(
-32602,
"notifications/initialized"
)));
assert!(!is_not_initialized(&tower_mcp::Error::Transport(
"boom".into()
)));
}
}