use rmcp::ErrorData as McpError;
use rmcp::model::{ElicitRequestParams, ElicitationAction, ElicitationSchema};
use rmcp::service::{
ElicitationError, ElicitationMode, ElicitationSafe, Peer, RoleServer, ServiceError,
};
fn interoperable_schema<T>() -> Result<ElicitationSchema, ElicitationError>
where
T: ElicitationSafe,
{
let mut schema = ElicitationSchema::from_type::<T>().map_err(|error| {
ElicitationError::Service(ServiceError::McpError(McpError::invalid_params(
format!(
"Invalid schema for type {}: {error}",
std::any::type_name::<T>()
),
None,
)))
})?;
schema.title = None;
schema.description = None;
Ok(schema)
}
pub(super) async fn elicit<T>(
peer: &Peer<RoleServer>,
message: impl Into<String>,
) -> Result<Option<T>, ElicitationError>
where
T: ElicitationSafe + for<'de> serde::Deserialize<'de>,
{
if !peer
.supported_elicitation_modes()
.contains(&ElicitationMode::Form)
{
return Err(ElicitationError::CapabilityNotSupported);
}
let response = peer
.create_elicitation(ElicitRequestParams::FormElicitationParams {
meta: None,
message: message.into(),
requested_schema: interoperable_schema::<T>()?,
})
.await?;
match response.action {
ElicitationAction::Accept => match response.content {
Some(value) => serde_json::from_value(value.clone())
.map(Some)
.map_err(|error| ElicitationError::ParseError { error, data: value }),
None => Err(ElicitationError::NoContent),
},
ElicitationAction::Decline => Err(ElicitationError::UserDeclined),
_ => Err(ElicitationError::UserCancelled),
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use rmcp::schemars::{self, JsonSchema};
use serde::Deserialize;
use super::*;
#[derive(Deserialize, JsonSchema)]
struct DocumentedForm {
allow: bool,
}
rmcp::elicit_safe!(DocumentedForm);
#[test]
fn strips_annotations_rejected_by_strict_clients() {
let derived = ElicitationSchema::from_type::<DocumentedForm>()
.expect("documented form should produce an elicitation schema");
assert!(derived.title.is_some());
let schema = interoperable_schema::<DocumentedForm>()
.expect("interoperable form schema should build");
assert!(schema.title.is_none());
assert!(schema.description.is_none());
let wire = serde_json::to_value(schema).expect("schema should serialize");
let keys: BTreeSet<_> = wire
.as_object()
.expect("elicitation schema should be an object")
.keys()
.map(String::as_str)
.collect();
let allowed = BTreeSet::from(["$schema", "properties", "required", "type"]);
assert!(
keys.is_subset(&allowed),
"strict clients reject these top-level keys: {:?}",
keys.difference(&allowed).collect::<Vec<_>>()
);
for required in ["properties", "required", "type"] {
assert!(
keys.contains(required),
"schema omitted required key {required}"
);
}
assert_eq!(wire["properties"]["allow"]["type"], "boolean");
}
#[test]
fn typed_form_remains_elicitation_safe() {
let schema = interoperable_schema::<DocumentedForm>();
assert!(schema.is_ok(), "typed form must remain valid: {schema:?}");
}
}