use rmcp::ErrorData as McpError;
use rmcp::model::{CallToolRequestParams, CallToolResponse, CreateTaskResult};
use rmcp::service::{RequestContext, RoleServer};
use rmcp::task_manager::{TaskExit, TaskOptions};
use super::BasemindServer;
tokio::task_local! {
pub(super) static CURRENT_TASK: rmcp::task_manager::TaskContext;
}
pub(super) const CANCEL_AWARE_READS: &[&str] = &[
"code:grep",
"code:semantic",
"graph:map",
"graph:calls",
"memory:documents",
"agents:wait",
];
pub(super) fn is_cancel_aware_read(name: &str, arguments: Option<&serde_json::Map<String, serde_json::Value>>) -> bool {
let Some(mode) = arguments
.and_then(|args| args.get("mode"))
.and_then(serde_json::Value::as_str)
else {
return false;
};
CANCEL_AWARE_READS.iter().any(|read| {
read.split_once(':')
.is_some_and(|(tool, read_mode)| tool == name && read_mode == mode)
})
}
const REQUEST_CANCELLED_CODE: i32 = -32800;
pub(super) async fn run_until_cancelled<T>(
cancelled: impl std::future::Future<Output = ()>,
work: impl std::future::Future<Output = Result<T, McpError>>,
) -> Result<T, McpError> {
tokio::select! {
biased;
() = cancelled => Err(McpError::new(
rmcp::model::ErrorCode(REQUEST_CANCELLED_CODE),
"request_cancelled",
None,
)),
result = work => result,
}
}
pub(super) const SLOW_CALLS: &[&str] = &[
"admin:rescan",
"graph:map",
"code:semantic",
#[cfg(feature = "documents")]
"memory:documents",
#[cfg(feature = "crawl")]
"web:scrape",
#[cfg(feature = "crawl")]
"web:crawl",
#[cfg(feature = "crawl")]
"web:map",
];
pub(super) fn is_slow_tool(name: &str, arguments: Option<&serde_json::Map<String, serde_json::Value>>) -> bool {
if SLOW_CALLS.contains(&name) {
return true;
}
let Some(mode) = arguments
.and_then(|args| args.get("mode"))
.and_then(serde_json::Value::as_str)
else {
return false;
};
SLOW_CALLS.iter().any(|slow| {
slow.split_once(':')
.is_some_and(|(tool, slow_mode)| tool == name && slow_mode == mode)
})
}
fn settle_cancelled<T>(work: tokio::task::JoinHandle<T>, tool_ct: &tokio_util::sync::CancellationToken, abort: bool) {
tool_ct.cancel();
if abort {
work.abort();
}
}
pub(super) fn spawn_slow_tool(
server: &BasemindServer,
request: CallToolRequestParams,
mut context: RequestContext<RoleServer>,
admission: super::admission::Admission,
) -> CreateTaskResult {
let server = server.clone();
let tool_ct = context.ct.child_token();
context.ct = tool_ct.clone();
let cancel_aware = super::tasks::is_cancel_aware_read(&request.name, request.arguments.as_ref());
let manager = server.tasks.clone();
let task = manager.spawn(TaskOptions::new(), move |ctx| {
Box::pin(async move {
let task_ctx = ctx.clone();
let mut work = tokio::spawn(async move {
let _admission = admission;
let tcc = rmcp::handler::server::tool::ToolCallContext::new(&server, request, context);
CURRENT_TASK.scope(task_ctx, server.tool_router.call(tcc)).await
});
let outcome = tokio::select! {
biased;
() = ctx.cancelled() => {
settle_cancelled(work, &tool_ct, cancel_aware);
return Err(TaskExit::Cancelled);
}
joined = &mut work => joined,
};
match outcome {
Ok(Ok(CallToolResponse::Complete(result))) => Ok(result),
Ok(Ok(_)) => Err(TaskExit::Error(McpError::internal_error(
"tool returned a non-terminal response inside a task",
None,
))),
Ok(Err(error)) => Err(TaskExit::Error(error)),
Err(join_error) => Err(TaskExit::Error(McpError::internal_error(
format!("slow tool task failed to complete: {join_error}"),
None,
))),
}
})
});
CreateTaskResult::new(task)
}
#[cfg(test)]
mod tests {
use super::*;
fn args(json: serde_json::Value) -> serde_json::Map<String, serde_json::Value> {
json.as_object().expect("object").clone()
}
#[test]
fn should_offload_the_slow_admin_mode_and_not_its_fast_siblings() {
assert!(is_slow_tool(
"admin",
Some(&args(serde_json::json!({ "mode": "rescan", "paths": ["src"] })))
));
assert!(!is_slow_tool(
"admin",
Some(&args(serde_json::json!({ "mode": "status" })))
));
assert!(!is_slow_tool("admin", None));
}
#[test]
fn should_not_offload_a_tool_that_is_not_listed() {
assert!(!is_slow_tool("outline", None));
}
#[cfg(feature = "crawl")]
#[test]
fn should_offload_a_consolidated_domain_only_for_the_modes_that_are_slow() {
assert!(is_slow_tool("web", Some(&args(serde_json::json!({ "mode": "crawl" })))));
assert!(is_slow_tool(
"web",
Some(&args(serde_json::json!({ "mode": "scrape" })))
));
}
#[cfg(feature = "crawl")]
#[test]
fn should_not_match_a_mode_across_domains() {
assert!(!is_slow_tool(
"memory",
Some(&args(serde_json::json!({ "mode": "map" })))
));
}
#[cfg(feature = "crawl")]
#[test]
fn should_not_offload_a_domain_call_with_an_absent_or_unknown_mode() {
assert!(!is_slow_tool("web", None));
assert!(!is_slow_tool(
"web",
Some(&args(serde_json::json!({ "mode": "sniff" })))
));
assert!(!is_slow_tool("web", Some(&args(serde_json::json!({ "mode": 7 })))));
}
#[test]
fn should_race_cancellation_only_for_the_pure_read_modes() {
for (tool, mode) in [
("code", "grep"),
("code", "semantic"),
("graph", "map"),
("graph", "calls"),
("memory", "documents"),
] {
assert!(is_cancel_aware_read(
tool,
Some(&args(serde_json::json!({ "mode": mode })))
));
}
for (tool, mode) in [
("admin", "rescan"),
("memory", "put"),
("memory", "accept"),
("code", "outline"),
("web", "crawl"),
] {
assert!(!is_cancel_aware_read(
tool,
Some(&args(serde_json::json!({ "mode": mode })))
));
}
assert!(!is_cancel_aware_read("code", None));
}
#[tokio::test]
async fn cancelling_a_read_returns_promptly_and_releases_the_permit() {
use super::super::admission::{HeavyAdmission, WorkClass};
let admission = HeavyAdmission::new(1, std::time::Duration::from_millis(50));
let permit = admission.admit(WorkClass::Heavy).await.expect("permit");
let (cancel, cancelled) = tokio::sync::oneshot::channel::<()>();
let work = async move {
let _held = permit;
std::future::pending::<Result<(), McpError>>().await
};
let call = tokio::spawn(run_until_cancelled(
async {
let _ = cancelled.await;
},
work,
));
assert!(
admission.admit(WorkClass::Heavy).await.is_err(),
"permit is held while the read runs"
);
cancel.send(()).expect("send cancel");
let started = std::time::Instant::now();
let error = tokio::time::timeout(std::time::Duration::from_secs(2), call)
.await
.expect("cancelled call returns promptly")
.expect("join")
.expect_err("cancelled call is an error");
assert!(started.elapsed() < std::time::Duration::from_secs(1));
assert_eq!(error.code, rmcp::model::ErrorCode(REQUEST_CANCELLED_CODE));
admission
.admit(WorkClass::Heavy)
.await
.expect("permit released after cancel");
}
struct LinkGuard(std::sync::Arc<std::sync::atomic::AtomicBool>);
impl Drop for LinkGuard {
fn drop(&mut self) {
self.0.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
#[tokio::test]
async fn cancelling_a_task_releases_a_cancel_aware_reads_link() {
let released = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let tool_ct = tokio_util::sync::CancellationToken::new();
let guard = LinkGuard(released.clone());
let work = tokio::spawn(async move {
let _link = guard;
std::future::pending::<()>().await;
});
settle_cancelled(work, &tool_ct, true);
assert!(tool_ct.is_cancelled());
tokio::time::timeout(std::time::Duration::from_secs(2), async {
while !released.load(std::sync::atomic::Ordering::SeqCst) {
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
})
.await
.expect("link released promptly after task cancel");
}
#[tokio::test]
async fn cancelling_a_task_detaches_a_mutating_tool() {
let released = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let tool_ct = tokio_util::sync::CancellationToken::new();
let guard = LinkGuard(released.clone());
let work = tokio::spawn(async move {
let _link = guard;
std::future::pending::<()>().await;
});
settle_cancelled(work, &tool_ct, false);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(!released.load(std::sync::atomic::Ordering::SeqCst));
}
#[tokio::test]
async fn an_uncancelled_read_returns_its_result() {
let value = run_until_cancelled(std::future::pending::<()>(), async { Ok::<_, McpError>(7) })
.await
.expect("result");
assert_eq!(value, 7);
}
}