use crate::transport::http::ServiceUrl;
use once_cell::sync::OnceCell;
use std::sync::Arc;
use std::sync::RwLock;
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
pub(super) struct McpSession {
#[cfg(not(feature = "legacy-spec"))]
peer_mode: crate::shared::PeerMode,
initialized: Notify,
sse_ready: Notify,
url: Arc<str>,
session_id: OnceCell<uuid::Uuid>,
last_event_id: RwLock<Option<String>>,
retry_ms: std::sync::atomic::AtomicU64,
cancellation_token: CancellationToken,
#[cfg(not(feature = "legacy-spec"))]
streams: dashmap::DashMap<crate::types::RequestId, CancellationToken>,
}
const UNSTATED_RETRY: u64 = u64::MAX;
impl McpSession {
pub(super) fn new(
url: ServiceUrl,
token: CancellationToken,
#[cfg(not(feature = "legacy-spec"))] peer_mode: crate::shared::PeerMode,
) -> Self {
Self {
#[cfg(not(feature = "legacy-spec"))]
peer_mode,
initialized: Notify::new(),
sse_ready: Notify::new(),
session_id: OnceCell::new(),
last_event_id: RwLock::new(None),
retry_ms: std::sync::atomic::AtomicU64::new(UNSTATED_RETRY),
cancellation_token: token,
url: Arc::from(url.to_url()),
#[cfg(not(feature = "legacy-spec"))]
streams: dashmap::DashMap::new(),
}
}
#[cfg(not(feature = "legacy-spec"))]
pub(super) fn track_stream(&self, id: crate::types::RequestId) -> CancellationToken {
let token = CancellationToken::new();
self.streams.insert(id, token.clone());
token
}
#[cfg(not(feature = "legacy-spec"))]
pub(super) fn untrack_stream(&self, id: &crate::types::RequestId) {
self.streams.remove(id);
}
#[cfg(not(feature = "legacy-spec"))]
pub(super) fn abort_stream(&self, id: &crate::types::RequestId) -> bool {
match self.streams.remove(id) {
Some((_, token)) => {
token.cancel();
true
}
None => false,
}
}
#[cfg(not(feature = "legacy-spec"))]
pub(super) fn is_legacy(&self) -> bool {
self.peer_mode.is_legacy()
}
pub(super) fn url(&self) -> &str {
&self.url
}
pub(super) fn cancellation_token(&self) -> CancellationToken {
self.cancellation_token.clone()
}
pub(super) fn has_session_id(&self) -> bool {
self.session_id.get().is_some()
}
pub(super) fn session_id(&self) -> Option<&uuid::Uuid> {
self.session_id.get()
}
pub(super) fn set_session_id(&self, id: uuid::Uuid) {
if let Err(_err) = self.session_id.set(id) {
#[cfg(feature = "tracing")]
tracing::info!("MCP Session Id already set");
}
}
pub(super) fn last_event_id(&self) -> Option<String> {
self.last_event_id.read().ok().and_then(|g| g.clone())
}
pub(super) fn set_last_event_id(&self, id: String) {
if let Ok(mut guard) = self.last_event_id.write() {
*guard = Some(id);
}
}
pub(super) fn set_retry(&self, ms: u64) {
self.retry_ms
.store(ms, std::sync::atomic::Ordering::Relaxed);
}
pub(super) fn retry_delay(&self, default: std::time::Duration) -> std::time::Duration {
match self.retry_ms.load(std::sync::atomic::Ordering::Relaxed) {
UNSTATED_RETRY => default,
ms => std::time::Duration::from_millis(ms),
}
}
#[inline]
pub(super) fn notify_session_initialized(&self) {
self.initialized.notify_one();
}
#[inline]
pub(super) fn notify_sse_initialized(&self) {
self.sse_ready.notify_one();
}
#[inline]
pub(super) async fn initialized(&self) {
self.initialized.notified().await;
}
#[inline]
pub(super) async fn sse_ready(&self) {
self.sse_ready.notified().await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::http::HttpProto;
use std::sync::Arc;
use tokio::time::{Duration, timeout};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
fn create_session() -> McpSession {
let url = ServiceUrl {
proto: HttpProto::Http,
addr: "localhost".to_string(),
endpoint: "init".to_string(),
};
let token = CancellationToken::new();
McpSession::new(
url,
token,
#[cfg(not(feature = "legacy-spec"))]
Default::default(),
)
}
#[tokio::test]
async fn it_has_url() {
let session = create_session();
assert_eq!(session.url(), "http://localhostinit");
}
#[tokio::test]
async fn it_has_cancellable_and_synced_cancellation_token() {
let session = create_session();
let token = session.cancellation_token();
token.cancel();
assert!(token.is_cancelled());
}
#[tokio::test]
async fn it_sets_and_gets_session_id() {
let session = create_session();
let id = Uuid::new_v4();
assert!(!session.has_session_id());
assert!(session.session_id().is_none());
session.set_session_id(id);
assert!(session.has_session_id());
assert_eq!(session.session_id(), Some(&id));
}
#[test]
fn it_returns_none_last_event_id_by_default() {
let session = create_session();
assert!(session.last_event_id().is_none());
}
#[test]
fn it_sets_and_gets_last_event_id() {
let session = create_session();
session.set_last_event_id("abc-123".to_string());
assert_eq!(session.last_event_id(), Some("abc-123".to_string()));
}
#[test]
fn it_overwrites_last_event_id_on_each_set() {
let session = create_session();
session.set_last_event_id("first".to_string());
session.set_last_event_id("second".to_string());
assert_eq!(session.last_event_id(), Some("second".to_string()));
}
#[test]
fn the_reconnect_delay_is_the_servers_to_state() {
let default = std::time::Duration::from_secs(3);
let session = create_session();
assert_eq!(
session.retry_delay(default),
default,
"a server that stated nothing gets the default"
);
session.set_retry(500);
assert_eq!(
session.retry_delay(default),
std::time::Duration::from_millis(500)
);
session.set_retry(0);
assert_eq!(
session.retry_delay(default),
std::time::Duration::ZERO,
"a server asking for an immediate reconnect must get one"
);
session.set_retry(1200);
assert_eq!(
session.retry_delay(default),
std::time::Duration::from_millis(1200),
"the latest statement wins"
);
}
#[tokio::test]
async fn it_guarantees_session_id_cannot_be_overwritten() {
let session = create_session();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
session.set_session_id(id1);
session.set_session_id(id2);
assert_eq!(session.session_id(), Some(&id1));
assert_ne!(session.session_id(), Some(&id2));
}
#[tokio::test]
async fn it_notifies_and_initialized() {
let session = Arc::new(create_session());
let handle = tokio::spawn({
let session = session.clone();
async move {
session.initialized().await;
}
});
tokio::time::sleep(Duration::from_millis(10)).await;
session.notify_session_initialized();
assert!(timeout(Duration::from_secs(1), handle).await.is_ok());
}
}