use std::collections::HashSet;
use futures::Stream;
use rmcp::model::{ClientJsonRpcMessage, ServerJsonRpcMessage};
use rmcp::transport::streamable_http_server::session::{
RestoreOutcome, ServerSseMessage, SessionId, SessionManager,
};
use thiserror::Error;
use tokio::sync::Mutex;
#[derive(Debug)]
pub struct BoundedSessionManager<SM> {
inner: SM,
max_sessions: usize,
live: Mutex<HashSet<SessionId>>,
}
impl<SM> BoundedSessionManager<SM> {
pub fn new(inner: SM, max_sessions: usize) -> Self {
Self {
inner,
max_sessions,
live: Mutex::new(HashSet::new()),
}
}
#[cfg(test)]
pub(crate) async fn live_count(&self) -> usize {
self.live.lock().await.len()
}
}
#[derive(Debug, Error)]
#[non_exhaustive] pub enum BoundedSessionManagerError<E> {
#[error("too many concurrent MCP sessions (max {0}); close an existing one and retry")]
TooManySessions(usize),
#[error(transparent)]
Inner(#[from] E),
}
impl<SM> SessionManager for BoundedSessionManager<SM>
where
SM: SessionManager,
{
type Error = BoundedSessionManagerError<SM::Error>;
type Transport = SM::Transport;
async fn create_session(&self) -> Result<(SessionId, Self::Transport), Self::Error> {
let mut live = self.live.lock().await;
if live.len() >= self.max_sessions {
return Err(BoundedSessionManagerError::TooManySessions(
self.max_sessions,
));
}
let (id, transport) = self.inner.create_session().await?;
live.insert(id.clone());
Ok((id, transport))
}
async fn initialize_session(
&self,
id: &SessionId,
message: ClientJsonRpcMessage,
) -> Result<ServerJsonRpcMessage, Self::Error> {
self.inner
.initialize_session(id, message)
.await
.map_err(Into::into)
}
async fn has_session(&self, id: &SessionId) -> Result<bool, Self::Error> {
self.inner.has_session(id).await.map_err(Into::into)
}
async fn close_session(&self, id: &SessionId) -> Result<(), Self::Error> {
let mut live = self.live.lock().await;
let result = self.inner.close_session(id).await;
if result.is_ok() {
live.remove(id);
}
result.map_err(Into::into)
}
async fn create_stream(
&self,
id: &SessionId,
message: ClientJsonRpcMessage,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + Sync + 'static, Self::Error> {
self.inner
.create_stream(id, message)
.await
.map_err(Into::into)
}
async fn accept_message(
&self,
id: &SessionId,
message: ClientJsonRpcMessage,
) -> Result<(), Self::Error> {
self.inner
.accept_message(id, message)
.await
.map_err(Into::into)
}
async fn create_standalone_stream(
&self,
id: &SessionId,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + Sync + 'static, Self::Error> {
self.inner
.create_standalone_stream(id)
.await
.map_err(Into::into)
}
async fn resume(
&self,
id: &SessionId,
last_event_id: String,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + Sync + 'static, Self::Error> {
self.inner
.resume(id, last_event_id)
.await
.map_err(Into::into)
}
async fn restore_session(
&self,
id: SessionId,
) -> Result<RestoreOutcome<Self::Transport>, Self::Error> {
let mut live = self.live.lock().await;
if live.len() >= self.max_sessions {
return Err(BoundedSessionManagerError::TooManySessions(
self.max_sessions,
));
}
match self.inner.restore_session(id.clone()).await {
Ok(outcome @ RestoreOutcome::Restored(_)) => {
live.insert(id);
Ok(outcome)
}
Ok(other) => Ok(other),
Err(e) => Err(e.into()),
}
}
}
#[cfg(test)]
impl<E> BoundedSessionManagerError<E> {
fn is_too_many_sessions(&self) -> bool {
matches!(self, Self::TooManySessions(_))
}
}
#[cfg(test)]
#[path = "session_limit_tests.rs"]
mod tests;