use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use rho_sdk::{CancellationToken, Error, HostInputRequest, HostInputResponse};
use tokio::sync::{mpsc, oneshot};
const NESTED_ATTRIBUTION_GRACE: Duration = Duration::from_secs(5);
pub(crate) struct McpUserQuestion {
pub(crate) request: HostInputRequest,
pub(crate) reply: oneshot::Sender<Result<HostInputResponse, Error>>,
}
const QUESTION_QUEUE_CAPACITY: usize = 4;
#[derive(Clone, Debug)]
pub(crate) struct McpCaller {
questions: mpsc::Sender<McpUserQuestion>,
cancellation: CancellationToken,
}
impl McpCaller {
pub(crate) async fn ask(&self, request: HostInputRequest) -> Result<HostInputResponse, Error> {
let (reply, answer) = oneshot::channel();
self.questions
.send(McpUserQuestion { request, reply })
.await
.map_err(|_| Error::Interrupted {
message: "the MCP tool call stopped accepting questions".into(),
})?;
answer.await.map_err(|_| Error::Interrupted {
message: "the MCP tool call ended before the question was answered".into(),
})?
}
pub(crate) fn cancellation(&self) -> &CancellationToken {
&self.cancellation
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct McpInFlightCalls {
state: Arc<Mutex<State>>,
}
#[derive(Debug, Default)]
struct State {
next_key: u64,
callers: BTreeMap<u64, McpCaller>,
last_release_at: Option<Instant>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum McpRouteError {
NoCallInFlight,
AmbiguousCall { in_flight: usize },
AttributionUncertain,
}
impl McpRouteError {
pub(crate) fn reason(self) -> String {
match self {
Self::NoCallInFlight => {
"Rho has no MCP tool call in flight to attribute this request to".into()
}
Self::AmbiguousCall { in_flight } => format!(
"Rho has {in_flight} MCP tool calls in flight on this server and cannot tell which one this request belongs to"
),
Self::AttributionUncertain => {
"Rho cannot safely attribute this request: a previous MCP tool call on this server ended recently and the protocol does not identify which call nested requests belong to".into()
}
}
}
}
impl McpInFlightCalls {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn register(&self) -> (McpCallRegistration, mpsc::Receiver<McpUserQuestion>) {
let cancellation = CancellationToken::new();
let (questions, receiver) = mpsc::channel(QUESTION_QUEUE_CAPACITY);
let mut state = self.lock();
let key = state.next_key;
state.next_key += 1;
state.callers.insert(
key,
McpCaller {
questions,
cancellation: cancellation.clone(),
},
);
drop(state);
(
McpCallRegistration {
key,
cancellation,
calls: self.clone(),
},
receiver,
)
}
pub(crate) fn sole_caller(&self) -> Result<McpCaller, McpRouteError> {
let state = self.lock();
let recent_release = state
.last_release_at
.is_some_and(|released| released.elapsed() < NESTED_ATTRIBUTION_GRACE);
let mut running = state.callers.values();
match (running.next(), running.next()) {
(Some(_caller), None) if recent_release => {
Err(McpRouteError::AttributionUncertain)
}
(Some(caller), None) => Ok(caller.clone()),
(None, _) => Err(McpRouteError::NoCallInFlight),
(Some(_), Some(_)) => Err(McpRouteError::AmbiguousCall {
in_flight: state.callers.len(),
}),
}
}
fn release(&self, key: u64, cancellation: &CancellationToken) {
cancellation.cancel();
let mut state = self.lock();
state.callers.remove(&key);
state.last_release_at = Some(Instant::now());
}
#[cfg(test)]
pub(crate) fn set_last_release_at_for_test(&self, at: Instant) {
self.lock().last_release_at = Some(at);
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(|error| error.into_inner())
}
}
pub(crate) struct McpCallRegistration {
key: u64,
cancellation: CancellationToken,
calls: McpInFlightCalls,
}
impl Drop for McpCallRegistration {
fn drop(&mut self) {
self.calls.release(self.key, &self.cancellation);
}
}
#[cfg(test)]
#[path = "inflight_tests.rs"]
mod tests;