use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use dashmap::DashMap;
use serde_json::Value;
use tokio::sync::{RwLock, mpsc, oneshot};
use tokio_util::sync::CancellationToken;
use turbomcp_core::error::{ErrorKind, McpError, McpResult};
use turbomcp_types::{ClientCapabilities, ProtocolVersion};
use crate::context::{McpSession, SessionFuture};
const MAX_PENDING_REQUESTS: usize = 64;
const COMMAND_BUFFER: usize = 32;
pub(crate) const SHUTDOWN_GRACE: std::time::Duration = std::time::Duration::from_secs(10);
#[derive(Debug)]
pub(crate) enum SessionCommand {
Request {
id: Value,
method: String,
params: Value,
response_tx: oneshot::Sender<McpResult<Value>>,
},
Notify { method: String, params: Value },
}
#[derive(Debug, Clone)]
pub(crate) struct ConnectionSession {
commands: mpsc::Sender<SessionCommand>,
client_capabilities: Arc<RwLock<Option<ClientCapabilities>>>,
protocol_version: Arc<RwLock<Option<ProtocolVersion>>>,
next_request_id: Arc<AtomicU64>,
session_id: Arc<str>,
}
impl ConnectionSession {
pub(crate) fn new() -> (Self, mpsc::Receiver<SessionCommand>) {
let (commands, receiver) = mpsc::channel(COMMAND_BUFFER);
let session = Self {
commands,
client_capabilities: Arc::new(RwLock::new(None)),
protocol_version: Arc::new(RwLock::new(None)),
next_request_id: Arc::new(AtomicU64::new(1)),
session_id: uuid::Uuid::new_v4().to_string().into(),
};
(session, receiver)
}
pub(crate) fn id(&self) -> &str {
&self.session_id
}
pub(crate) async fn set_initialized(
&self,
client_capabilities: ClientCapabilities,
protocol_version: ProtocolVersion,
) {
*self.client_capabilities.write().await = Some(client_capabilities);
*self.protocol_version.write().await = Some(protocol_version);
}
}
impl McpSession for ConnectionSession {
fn client_capabilities<'a>(&'a self) -> SessionFuture<'a, Option<ClientCapabilities>> {
Box::pin(async move { Ok(self.client_capabilities.read().await.clone()) })
}
fn protocol_version<'a>(&'a self) -> SessionFuture<'a, Option<ProtocolVersion>> {
Box::pin(async move { Ok(self.protocol_version.read().await.clone()) })
}
fn call<'a>(&'a self, method: &'a str, params: Value) -> SessionFuture<'a, Value> {
Box::pin(async move {
let id = Value::String(format!(
"s-{}",
self.next_request_id.fetch_add(1, Ordering::Relaxed)
));
let (response_tx, response_rx) = oneshot::channel();
self.commands
.send(SessionCommand::Request {
id: id.clone(),
method: method.to_string(),
params,
response_tx,
})
.await
.map_err(|_| McpError::internal("Session closed"))?;
let mut abandon = CancelOnAbandon {
commands: self.commands.clone(),
id: Some(id),
reason: "server abandoned the request",
};
match tokio::time::timeout(super::SERVER_REQUEST_TIMEOUT, response_rx).await {
Ok(Ok(result)) => {
abandon.disarm();
result
}
Ok(Err(_)) => {
abandon.disarm();
Err(McpError::internal("Response channel closed"))
}
Err(_) => {
abandon.reason = "server request timed out";
Err(McpError::timeout(format!(
"client did not answer {method} within {:?}",
super::SERVER_REQUEST_TIMEOUT
)))
}
}
})
}
fn notify<'a>(&'a self, method: &'a str, params: Value) -> SessionFuture<'a, ()> {
Box::pin(async move {
self.commands
.send(SessionCommand::Notify {
method: method.to_string(),
params,
})
.await
.map_err(|_| McpError::internal("Session closed"))?;
Ok(())
})
}
}
struct CancelOnAbandon {
commands: mpsc::Sender<SessionCommand>,
id: Option<Value>,
reason: &'static str,
}
impl CancelOnAbandon {
fn disarm(&mut self) {
self.id = None;
}
}
impl Drop for CancelOnAbandon {
fn drop(&mut self) {
if let Some(id) = self.id.take() {
let _ = self.commands.try_send(SessionCommand::Notify {
method: "notifications/cancelled".to_string(),
params: serde_json::json!({ "requestId": id, "reason": self.reason }),
});
}
}
}
#[derive(Debug, Default)]
pub(crate) struct OutboundRequests {
pending: HashMap<Value, oneshot::Sender<McpResult<Value>>>,
}
impl OutboundRequests {
pub(crate) fn frame(&mut self, command: SessionCommand) -> Option<Value> {
match command {
SessionCommand::Request {
id,
method,
params,
response_tx,
} => self.begin(id, &method, params, response_tx),
SessionCommand::Notify { method, params } => Some(notification_frame(&method, params)),
}
}
fn begin(
&mut self,
id: Value,
method: &str,
params: Value,
response_tx: oneshot::Sender<McpResult<Value>>,
) -> Option<Value> {
self.pending.retain(|_, waiting| !waiting.is_closed());
if self.pending.len() >= MAX_PENDING_REQUESTS {
tracing::error!(
count = self.pending.len(),
"Too many pending server-to-client requests"
);
let _ = response_tx.send(Err(McpError::internal(
"Too many pending server-to-client requests",
)));
return None;
}
let frame = request_frame(&id, method, params);
self.pending.insert(id, response_tx);
Some(frame)
}
pub(crate) fn resolve(&mut self, message: &Value) -> bool {
let Some(id) = message.get("id") else {
return false;
};
if message.get("result").is_none() && message.get("error").is_none() {
return false;
}
let Some(waiting) = self.pending.remove(id) else {
tracing::warn!(id = %id, "Received response for unknown request ID");
return true;
};
let outcome = match message.get("error") {
Some(error) => Err(error_from_response(error)),
None => Ok(message.get("result").cloned().unwrap_or(Value::Null)),
};
let _ = waiting.send(outcome);
true
}
pub(crate) fn close(&mut self) {
if !self.pending.is_empty() {
tracing::warn!(
count = self.pending.len(),
"Abandoning pending server-to-client requests on connection close"
);
}
for (_, waiting) in self.pending.drain() {
let _ = waiting.send(Err(McpError::internal("Session closed")));
}
}
}
fn error_from_response(error: &Value) -> McpError {
match serde_json::from_value::<turbomcp_core::jsonrpc::JsonRpcError>(error.clone()) {
Ok(e) => {
let mut mapped = McpError::new(ErrorKind::from_i32(e.code), e.message);
if let Some(data) = e.data {
mapped = mapped.with_data(data);
}
mapped
}
Err(_) => McpError::internal("Failed to parse error response"),
}
}
fn request_frame(id: &Value, method: &str, params: Value) -> Value {
let mut frame = serde_json::json!({ "jsonrpc": "2.0", "id": id, "method": method });
if !params.is_null() {
frame["params"] = params;
}
frame
}
fn notification_frame(method: &str, params: Value) -> Value {
let mut frame = serde_json::json!({ "jsonrpc": "2.0", "method": method });
if !params.is_null() {
frame["params"] = params;
}
frame
}
pub(crate) fn readable_id(message: &Value) -> Option<Value> {
message
.get("id")
.filter(|id| id.is_string() || id.is_i64() || id.is_u64())
.cloned()
}
pub(crate) struct ConnectionCleanup {
pub(crate) handlers: Arc<DashMap<String, CancellationToken>>,
pub(crate) session_id: Arc<str>,
}
impl ConnectionCleanup {
pub(crate) fn new(
handlers: &Arc<DashMap<String, CancellationToken>>,
session: &ConnectionSession,
) -> Self {
Self {
handlers: Arc::clone(handlers),
session_id: Arc::clone(&session.session_id),
}
}
}
impl Drop for ConnectionCleanup {
fn drop(&mut self) {
for entry in self.handlers.iter() {
entry.value().cancel();
}
turbomcp_core::context::clear_session_log_state(&self.session_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn request(id: &str) -> (SessionCommand, oneshot::Receiver<McpResult<Value>>) {
let (response_tx, response_rx) = oneshot::channel();
(
SessionCommand::Request {
id: Value::String(id.into()),
method: "sampling/createMessage".into(),
params: serde_json::json!({}),
response_tx,
},
response_rx,
)
}
#[test]
fn abandoned_requests_do_not_hold_their_slots() {
let mut outbound = OutboundRequests::default();
for n in 0..MAX_PENDING_REQUESTS * 2 {
let (command, response_rx) = request(&format!("s-{n}"));
assert!(outbound.frame(command).is_some(), "request {n} refused");
drop(response_rx);
}
}
#[test]
fn requests_still_awaited_are_capped() {
let mut outbound = OutboundRequests::default();
let mut waiting = Vec::new();
for n in 0..MAX_PENDING_REQUESTS {
let (command, response_rx) = request(&format!("s-{n}"));
assert!(outbound.frame(command).is_some());
waiting.push(response_rx);
}
let (command, mut refused) = request("s-over");
assert!(outbound.frame(command).is_none());
assert!(refused.try_recv().expect("answered").is_err());
}
#[test]
fn an_error_answer_keeps_its_data() {
let mut outbound = OutboundRequests::default();
let (command, mut response_rx) = request("s-1");
outbound.frame(command);
assert!(outbound.resolve(&serde_json::json!({
"jsonrpc": "2.0",
"id": "s-1",
"error": { "code": -1, "message": "User rejected", "data": { "why": "no" } }
})));
let error = response_rx
.try_recv()
.expect("answered")
.expect_err("error");
assert_eq!(error.data(), Some(&serde_json::json!({ "why": "no" })));
}
#[test]
fn a_notification_without_params_omits_the_member() {
let frame = notification_frame("notifications/tools/list_changed", Value::Null);
assert!(frame.get("params").is_none(), "{frame}");
}
#[tokio::test]
async fn an_abandoned_call_sends_a_cancellation() {
let (session, mut commands) = ConnectionSession::new();
let call = tokio::spawn(async move {
let _ = session
.call("elicitation/create", serde_json::json!({}))
.await;
});
let Some(SessionCommand::Request { id, .. }) = commands.recv().await else {
panic!("expected the request first");
};
call.abort();
let _ = call.await;
let Some(SessionCommand::Notify { method, params }) = commands.recv().await else {
panic!("expected a cancellation");
};
assert_eq!(method, "notifications/cancelled");
assert_eq!(params["requestId"], id);
}
}