use std::future::Future;
use std::time::Duration;
use rmcp::RoleServer;
use rmcp::handler::server::tool::IntoCallToolResult;
use rmcp::model::{CallToolResponse, CallToolResult, ContentBlock, ErrorData as McpError};
use rmcp::service::Peer;
use tokio::task::JoinError;
use tokio_util::sync::CancellationToken;
pub(super) const PEER_CLOSE_POLL: Duration = Duration::from_millis(250);
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub(super) enum CallStop {
#[error("request cancelled by the client or its session ended")]
Cancelled,
#[error("mcpls is shutting down")]
ServerShutdown,
#[error("connection closed")]
ConnectionClosed,
}
impl CallStop {
fn of_request_cancel(stop: &CancellationToken) -> Self {
if stop.is_cancelled() {
Self::ServerShutdown
} else {
Self::Cancelled
}
}
pub(super) fn into_response(self) -> CallToolResponse {
CallToolResult::error(vec![ContentBlock::text(self.to_string())]).into()
}
}
#[derive(Debug)]
pub(super) enum ToolFailure {
Protocol(McpError),
Stopped(CallStop),
}
impl IntoCallToolResult for ToolFailure {
fn into_call_tool_result(self) -> Result<CallToolResponse, McpError> {
match self {
Self::Protocol(error) => Err(error),
Self::Stopped(stop) => Ok(stop.into_response()),
}
}
}
pub(super) fn detached_result<T>(joined: Result<T, JoinError>) -> Result<T, ToolFailure> {
joined.map_err(|_| ToolFailure::Stopped(CallStop::ServerShutdown))
}
pub(super) async fn peer_closed(peer: &Peer<RoleServer>) {
while !peer.is_transport_closed() {
tokio::time::sleep(PEER_CLOSE_POLL).await;
}
}
pub(super) async fn run_cancellable<T>(
request: &CancellationToken,
stop: &CancellationToken,
closed: impl Future<Output = ()>,
handler: impl Future<Output = T>,
) -> Result<T, CallStop> {
tokio::select! {
biased;
() = request.cancelled() => Err(CallStop::of_request_cancel(stop)),
() = closed => Err(CallStop::ConnectionClosed),
output = handler => Ok(output),
}
}
#[cfg(test)]
mod wire_tests;
#[cfg(test)]
mod tests {
use std::assert_matches;
use std::future::pending;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use super::*;
struct Polled(Arc<AtomicBool>);
impl Polled {
async fn never_finishes(self) {
self.0.store(true, Ordering::SeqCst);
pending::<()>().await;
}
}
#[tokio::test]
async fn a_finished_handler_returns_its_output() {
let token = CancellationToken::new();
let outcome =
run_cancellable(&token, &CancellationToken::new(), pending(), async { 7 }).await;
assert_eq!(outcome, Ok(7));
}
#[tokio::test]
async fn a_token_cancelled_up_front_never_polls_the_handler() {
let token = CancellationToken::new();
token.cancel();
let polled = Arc::new(AtomicBool::new(false));
let outcome = run_cancellable(
&token,
&CancellationToken::new(),
pending(),
Polled(Arc::clone(&polled)).never_finishes(),
)
.await;
assert_matches!(outcome, Err(CallStop::Cancelled));
assert!(!polled.load(Ordering::SeqCst));
}
#[tokio::test]
async fn a_closed_connection_stops_the_handler() {
let token = CancellationToken::new();
let polled = Arc::new(AtomicBool::new(false));
let outcome = run_cancellable(
&token,
&CancellationToken::new(),
std::future::ready(()),
Polled(Arc::clone(&polled)).never_finishes(),
)
.await;
assert_matches!(outcome, Err(CallStop::ConnectionClosed));
assert!(!polled.load(Ordering::SeqCst));
}
#[tokio::test]
async fn a_token_fired_mid_flight_drops_the_handler() {
struct DropFlag(Arc<AtomicBool>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
let token = CancellationToken::new();
let dropped = Arc::new(AtomicBool::new(false));
let handler = {
let flag = DropFlag(Arc::clone(&dropped));
async move {
let _flag = flag;
pending::<()>().await;
}
};
let canceller = token.clone();
tokio::spawn(async move {
tokio::task::yield_now().await;
canceller.cancel();
});
let outcome = run_cancellable(&token, &CancellationToken::new(), pending(), handler).await;
assert_matches!(outcome, Err(CallStop::Cancelled));
assert!(dropped.load(Ordering::SeqCst));
}
#[tokio::test]
async fn a_request_token_fired_after_the_stop_token_reports_shutdown() {
let (token, stop) = (CancellationToken::new(), CancellationToken::new());
stop.cancel();
token.cancel();
let outcome = run_cancellable(&token, &stop, pending(), pending::<()>()).await;
assert_matches!(outcome, Err(CallStop::ServerShutdown));
}
#[tokio::test]
async fn a_task_cancelled_by_runtime_teardown_reads_as_shutdown() {
let task = tokio::spawn(pending::<()>());
task.abort();
let joined = task.await;
assert_matches!(
detached_result(joined),
Err(ToolFailure::Stopped(CallStop::ServerShutdown))
);
assert_matches!(detached_result(Ok::<_, JoinError>(1)), Ok(1));
}
#[test]
fn a_stopped_call_is_delivered_as_a_tool_error_result() {
let response = ToolFailure::Stopped(CallStop::ServerShutdown)
.into_call_tool_result()
.unwrap();
let CallToolResponse::Complete(result) = response else {
panic!("expected a complete result");
};
assert_eq!(result.is_error, Some(true));
assert!(
ToolFailure::Protocol(McpError::internal_error("x", None))
.into_call_tool_result()
.is_err()
);
}
}