use rho_sdk::HostInputRequest;
use rmcp::{
model::{ElicitRequestParams, ElicitResult, ElicitationAction},
ErrorData as McpError,
};
use super::{elicitation_form::ElicitationForm, inflight::McpInFlightCalls};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum McpElicitationSupport {
Available,
Unavailable,
}
impl McpElicitationSupport {
pub(crate) fn is_available(self) -> bool {
match self {
Self::Available => true,
Self::Unavailable => false,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct McpElicitationService {
identity: String,
calls: McpInFlightCalls,
support: McpElicitationSupport,
}
impl McpElicitationService {
pub(crate) fn new(
identity: impl Into<String>,
calls: McpInFlightCalls,
support: McpElicitationSupport,
) -> Self {
Self {
identity: identity.into(),
calls,
support,
}
}
pub(crate) fn is_available(&self) -> bool {
self.support.is_available()
}
pub(crate) async fn elicit(
&self,
request: ElicitRequestParams,
) -> Result<ElicitResult, McpError> {
if !self.support.is_available() {
return Ok(self.decline("this Rho run cannot show a server's question to anyone"));
}
let (message, schema) = match request {
ElicitRequestParams::FormElicitationParams {
message,
requested_schema,
..
} => (message, requested_schema),
ElicitRequestParams::UrlElicitationParams { .. } => {
return Ok(self.decline("Rho does not support URL elicitation"))
}
_ => return Ok(self.decline("Rho does not support this elicitation mode")),
};
let caller = match self.calls.sole_caller() {
Ok(caller) => caller,
Err(error) => return Ok(self.decline(error.reason())),
};
let form = match ElicitationForm::from_schema(&schema) {
Ok(form) => form,
Err(error) => return Ok(self.decline(error.reason())),
};
let title = elicitation_title(&self.identity, &message);
let host_request = match HostInputRequest::questionnaire(title, form.questions().to_vec()) {
Ok(request) => request,
Err(error) => return Ok(self.decline(error.to_string())),
};
match caller.ask(host_request).await {
Ok(response) => match form.content(&response) {
Ok(content) => {
Ok(ElicitResult::new(ElicitationAction::Accept).with_content(content))
}
Err(error) => Ok(self.decline(error.reason())),
},
Err(rho_sdk::Error::Cancelled) => Ok(ElicitResult::new(ElicitationAction::Cancel)),
Err(error) => Ok(self.decline(error.to_string())),
}
}
fn decline(&self, reason: impl AsRef<str>) -> ElicitResult {
tracing::debug!(
server = %self.identity,
reason = reason.as_ref(),
"declined an MCP elicitation request"
);
ElicitResult::new(ElicitationAction::Decline)
}
}
fn elicitation_title(identity: &str, message: &str) -> String {
let message = message.trim();
if message.is_empty() {
return format!("MCP server `{identity}` needs input");
}
format!("MCP server `{identity}`: {message}")
}
#[cfg(test)]
#[path = "elicitation_tests.rs"]
mod tests;