use anda_core::{BoxError, CancellationToken};
use async_trait::async_trait;
use rmcp::model::{
ElicitRequestParams, ElicitResult, ElicitationCapability, FormElicitationCapability,
};
use std::{sync::Arc, time::Duration};
#[async_trait]
pub trait McpElicitationHandler: Send + Sync {
fn capabilities(&self) -> ElicitationCapability {
ElicitationCapability::new().with_form(FormElicitationCapability::default())
}
async fn elicit(
&self,
server_id: &str,
request: ElicitRequestParams,
cancellation: CancellationToken,
) -> Result<ElicitResult, BoxError>;
}
#[derive(Clone)]
pub(super) struct ElicitationDispatcher {
pub handler: Arc<dyn McpElicitationHandler>,
pub server_id: String,
pub timeout: Duration,
pub session_cancelled: CancellationToken,
}
impl ElicitationDispatcher {
pub async fn elicit(
&self,
request: ElicitRequestParams,
cancellation: &CancellationToken,
) -> Result<ElicitResult, BoxError> {
let capabilities = self.handler.capabilities();
let supported = match &request {
ElicitRequestParams::FormElicitationParams { .. } => capabilities.form.is_some(),
ElicitRequestParams::UrlElicitationParams { .. } => capabilities.url.is_some(),
_ => false,
};
if !supported {
return Err("MCP elicitation mode is not enabled".into());
}
let child = cancellation.child_token();
let _guard = child.clone().drop_guard();
tokio::select! {
biased;
_ = self.session_cancelled.cancelled() => Err("MCP session closed during elicitation".into()),
_ = cancellation.cancelled() => Err("MCP elicitation cancelled".into()),
result = tokio::time::timeout(self.timeout, self.handler.elicit(&self.server_id, request, child)) =>
result.map_err(|_| "MCP elicitation timed out")?,
}
}
}