use std::collections::HashMap;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use futures::Stream;
use parking_lot::{Mutex, MutexGuard};
use rmcp::model::{ClientJsonRpcMessage, ServerJsonRpcMessage};
use rmcp::transport::streamable_http_server::session::{
EventStore, RestoreOutcome, ServerSseMessage, SessionId, SessionManager,
};
use thiserror::Error;
pub(crate) trait Clock: std::fmt::Debug + Send + Sync + 'static {
fn elapsed(&self) -> Duration;
}
#[derive(Debug)]
pub(crate) struct MonotonicClock(Instant);
impl MonotonicClock {
pub(crate) fn start() -> Self {
Self(Instant::now())
}
}
impl Clock for MonotonicClock {
fn elapsed(&self) -> Duration {
self.0.elapsed()
}
}
#[derive(Debug)]
struct SessionCounters {
in_flight: AtomicU32,
awaiting_initialize: AtomicBool,
closing: AtomicBool,
last_tick: AtomicU64,
last_touched_nanos: AtomicU64,
}
#[derive(Debug)]
struct ActivityClock {
tick: AtomicU64,
clock: Arc<dyn Clock>,
}
impl ActivityClock {
fn now_nanos(&self) -> u64 {
u64::try_from(self.clock.elapsed().as_nanos()).unwrap_or(u64::MAX)
}
fn touch(&self, counters: &SessionCounters) {
counters
.last_tick
.store(self.tick.fetch_add(1, Ordering::Relaxed), Ordering::Relaxed);
counters
.last_touched_nanos
.store(self.now_nanos(), Ordering::Relaxed);
}
fn new_session(&self) -> Arc<SessionCounters> {
let counters = SessionCounters {
in_flight: AtomicU32::new(0),
awaiting_initialize: AtomicBool::new(true),
closing: AtomicBool::new(false),
last_tick: AtomicU64::new(0),
last_touched_nanos: AtomicU64::new(0),
};
self.touch(&counters);
Arc::new(counters)
}
fn is_evictable(&self, counters: &SessionCounters, policy: EvictionPolicy) -> bool {
if counters.in_flight.load(Ordering::Relaxed) != 0
|| counters.closing.load(Ordering::Relaxed)
{
return false;
}
if counters.awaiting_initialize.load(Ordering::Relaxed) {
return policy
.init_timeout
.is_some_and(|abandoned_after| self.quiet_for(counters, abandoned_after));
}
self.quiet_for(counters, policy.min_idle)
}
fn quiet_for(&self, counters: &SessionCounters, at_least: Duration) -> bool {
let quiet_nanos = self
.now_nanos()
.saturating_sub(counters.last_touched_nanos.load(Ordering::Relaxed));
u128::from(quiet_nanos) >= at_least.as_nanos()
}
}
#[derive(Debug, Clone, Copy)]
pub struct EvictionPolicy {
pub min_idle: Duration,
pub init_timeout: Option<Duration>,
}
struct ClosingMark(Arc<SessionCounters>);
impl Drop for ClosingMark {
fn drop(&mut self) {
self.0.closing.store(false, Ordering::Relaxed);
}
}
struct ActivityGuard {
counters: Arc<SessionCounters>,
clock: Arc<ActivityClock>,
}
impl ActivityGuard {
fn begin(counters: Arc<SessionCounters>, clock: Arc<ActivityClock>) -> Self {
counters.in_flight.fetch_add(1, Ordering::Relaxed);
Self { counters, clock }
}
}
impl Drop for ActivityGuard {
fn drop(&mut self) {
self.clock.touch(&self.counters);
self.counters.in_flight.fetch_sub(1, Ordering::Relaxed);
}
}
struct GuardedStream<S> {
inner: Pin<Box<S>>,
_guard: Option<ActivityGuard>,
}
impl<S: Stream> Stream for GuardedStream<S> {
type Item = S::Item;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.get_mut().inner.as_mut().poll_next(cx)
}
}
#[derive(Debug)]
pub struct BoundedSessionManager<SM> {
inner: SM,
max_sessions: usize,
policy: EvictionPolicy,
admission: tokio::sync::Mutex<()>,
live: Mutex<HashMap<SessionId, Arc<SessionCounters>>>,
activity: Arc<ActivityClock>,
}
impl<SM> BoundedSessionManager<SM> {
pub fn new(inner: SM, max_sessions: usize, policy: EvictionPolicy) -> Self {
Self::with_clock(
inner,
max_sessions,
policy,
Arc::new(MonotonicClock::start()),
)
}
pub(crate) fn with_clock(
inner: SM,
max_sessions: usize,
policy: EvictionPolicy,
clock: Arc<dyn Clock>,
) -> Self {
Self {
inner,
max_sessions,
policy,
admission: tokio::sync::Mutex::new(()),
live: Mutex::new(HashMap::new()),
activity: Arc::new(ActivityClock {
tick: AtomicU64::new(0),
clock,
}),
}
}
fn live(&self) -> MutexGuard<'_, HashMap<SessionId, Arc<SessionCounters>>> {
self.live.lock()
}
fn is_full(&self) -> bool {
self.live().len() >= self.max_sessions
}
#[cfg(test)]
pub(crate) fn live_count(&self) -> usize {
self.live().len()
}
#[cfg(test)]
pub(crate) fn is_live(&self, id: &SessionId) -> bool {
self.live().contains_key(id)
}
fn activity_guard<E>(
&self,
id: &SessionId,
) -> Result<Option<ActivityGuard>, BoundedSessionManagerError<E>> {
let live = self.live();
let Some(counters) = live.get(id) else {
return Ok(None);
};
if counters.closing.load(Ordering::Relaxed) {
return Err(BoundedSessionManagerError::SessionClosing);
}
Ok(Some(ActivityGuard::begin(
Arc::clone(counters),
Arc::clone(&self.activity),
)))
}
fn check_in(&self, id: &SessionId) -> bool {
let live = self.live();
match live.get(id) {
Some(counters) if counters.closing.load(Ordering::Relaxed) => false,
Some(counters) => {
self.activity.touch(counters);
true
}
None => true,
}
}
}
#[derive(Debug, Error)]
#[non_exhaustive] pub enum BoundedSessionManagerError<E> {
#[error(
"too many concurrent MCP sessions (max {max_sessions}) and none has been \
idle for {min_idle_secs} s, so none can be evicted; retry in a moment"
)]
TooManySessions {
max_sessions: usize,
min_idle_secs: u64,
},
#[error(
"this MCP session is being closed to admit another client; initialize a \
new session"
)]
SessionClosing,
#[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 _admission = self.admission.lock().await;
if self.is_full() {
self.evict_one_idle().await?;
}
let (id, transport) = self.inner.create_session().await?;
self.live().insert(id.clone(), self.activity.new_session());
Ok((id, transport))
}
async fn initialize_session(
&self,
id: &SessionId,
message: ClientJsonRpcMessage,
) -> Result<ServerJsonRpcMessage, Self::Error> {
let guard = self.activity_guard(id)?;
if let Some(guard) = &guard {
guard
.counters
.awaiting_initialize
.store(false, Ordering::Relaxed);
}
let result = self.inner.initialize_session(id, message).await;
drop(guard);
result.map_err(Into::into)
}
async fn has_session(&self, id: &SessionId) -> Result<bool, Self::Error> {
if !self.check_in(id) {
return Ok(false);
}
self.inner.has_session(id).await.map_err(Into::into)
}
async fn close_session(&self, id: &SessionId) -> Result<(), Self::Error> {
let _admission = self.admission.lock().await;
let result = self.inner.close_session(id).await;
if result.is_ok() {
self.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> {
let guard = self.activity_guard(id)?;
let stream = self.inner.create_stream(id, message).await?;
Ok(GuardedStream {
inner: Box::pin(stream),
_guard: guard,
})
}
async fn accept_message(
&self,
id: &SessionId,
message: ClientJsonRpcMessage,
) -> Result<(), Self::Error> {
let _guard = self.activity_guard(id)?;
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> {
let guard = self.activity_guard(id)?;
let stream = self.inner.create_standalone_stream(id).await?;
Ok(GuardedStream {
inner: Box::pin(stream),
_guard: guard,
})
}
async fn resume(
&self,
id: &SessionId,
last_event_id: String,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + Sync + 'static, Self::Error> {
let guard = self.activity_guard(id)?;
let stream = self.inner.resume(id, last_event_id).await?;
Ok(GuardedStream {
inner: Box::pin(stream),
_guard: guard,
})
}
async fn restore_session(
&self,
id: SessionId,
) -> Result<RestoreOutcome<Self::Transport>, Self::Error> {
let _admission = self.admission.lock().await;
let outcome = self.inner.restore_session(id.clone()).await?;
if !matches!(outcome, RestoreOutcome::Restored(_)) {
return Ok(outcome);
}
if self.is_full() {
if let Err(refusal) = self.evict_one_idle().await {
drop(outcome);
self.inner.close_session(&id).await?;
return Err(refusal);
}
}
self.live().insert(id, self.activity.new_session());
Ok(outcome)
}
fn event_store(&self) -> Option<Arc<dyn EventStore>> {
self.inner.event_store()
}
}
impl<SM> BoundedSessionManager<SM>
where
SM: SessionManager,
{
async fn evict_one_idle(&self) -> Result<(), BoundedSessionManagerError<SM::Error>> {
let (victim, _closing) = {
let live = self.live();
let (id, counters) = live
.iter()
.filter(|(_, counters)| self.activity.is_evictable(counters, self.policy))
.min_by_key(|(_, counters)| counters.last_tick.load(Ordering::Relaxed))
.ok_or(BoundedSessionManagerError::TooManySessions {
max_sessions: self.max_sessions,
min_idle_secs: self.policy.min_idle.as_secs(),
})?;
counters.closing.store(true, Ordering::Relaxed);
(id.clone(), ClosingMark(Arc::clone(counters)))
};
self.inner.close_session(&victim).await?;
self.live().remove(&victim);
tracing::info!(
session = %victim,
max_sessions = self.max_sessions,
min_idle_secs = self.policy.min_idle.as_secs(),
"evicted idle MCP session to admit a new one"
);
Ok(())
}
}
#[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;