1use rmcp::model::{ClientCapabilities, ElicitRequestParams, ErrorData, InputRequest, InputRequests, InputResponses};
2use serde::de::DeserializeOwned;
3
4pub const ELICITATION_UNSUPPORTED: &str = "This tool needs to ask the user for input, but the connected client does not support \
5 interactive input (MCP elicitation over protocol 2026-07-28 or newer).";
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ElicitationMode {
9 Form,
10 Url,
11}
12
13pub fn input_requests_supported(capabilities: Option<&ClientCapabilities>, requests: &InputRequests) -> bool {
14 requests.values().all(|request| capabilities.is_some_and(|capabilities| supports_input(capabilities, request)))
15}
16
17pub fn elicitation_supported(capabilities: Option<&ClientCapabilities>, mode: ElicitationMode) -> bool {
18 let Some(elicitation) = capabilities.and_then(|capabilities| capabilities.elicitation.as_ref()) else {
19 return false;
20 };
21 match mode {
22 ElicitationMode::Form => elicitation.form.is_some() || elicitation.url.is_none(),
23 ElicitationMode::Url => elicitation.url.is_some(),
24 }
25}
26
27pub fn parse_response<T: DeserializeOwned>(responses: &InputResponses, key: &str) -> Result<T, ErrorData> {
29 let response = responses
30 .get(key)
31 .ok_or_else(|| ErrorData::invalid_params(format!("missing input response for '{key}'"), None))?;
32 serde_json::from_value(response.clone())
33 .map_err(|e| ErrorData::invalid_params(format!("invalid input response for '{key}': {e}"), None))
34}
35
36fn supports_input(capabilities: &ClientCapabilities, request: &InputRequest) -> bool {
37 #[allow(unreachable_patterns)]
38 match request {
39 InputRequest::CreateMessage(_) => capabilities.sampling.is_some(),
40 InputRequest::Elicitation(request) => supports_elicitation(capabilities, &request.params),
41 InputRequest::ListRoots(_) => capabilities.roots.is_some(),
42 _ => false,
43 }
44}
45
46fn supports_elicitation(capabilities: &ClientCapabilities, params: &ElicitRequestParams) -> bool {
47 #[allow(unreachable_patterns)]
48 let mode = match params {
49 ElicitRequestParams::FormElicitationParams { .. } => ElicitationMode::Form,
50 ElicitRequestParams::UrlElicitationParams { .. } => ElicitationMode::Url,
51 _ => return false,
52 };
53 elicitation_supported(Some(capabilities), mode)
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use rmcp::model::{
60 ClientCapabilities, ElicitRequest, ElicitRequestParams, ElicitationCapability, ElicitationSchema,
61 FormElicitationCapability, InputRequest, UrlElicitationCapability,
62 };
63 use serde_json::json;
64
65 #[test]
66 fn validates_capabilities_against_every_request_in_the_batch() {
67 let requests = InputRequests::from([
68 ("form".to_string(), elicitation("Form")),
69 ("url".to_string(), url_elicitation("URL")),
70 ]);
71 let mut capabilities = ClientCapabilities::default();
72 capabilities.elicitation = Some(ElicitationCapability::new().with_form(FormElicitationCapability::new()));
73
74 assert!(!input_requests_supported(Some(&capabilities), &requests));
75 }
76
77 #[test]
78 fn validates_a_mixed_elicitation_batch_when_all_capabilities_are_present() {
79 let requests = InputRequests::from([
80 ("form".to_string(), elicitation("Form")),
81 ("url".to_string(), url_elicitation("URL")),
82 ]);
83 let mut capabilities = ClientCapabilities::default();
84 capabilities.elicitation = Some(
85 ElicitationCapability::new()
86 .with_form(FormElicitationCapability::new())
87 .with_url(UrlElicitationCapability::new()),
88 );
89
90 assert!(input_requests_supported(Some(&capabilities), &requests));
91 }
92
93 #[test]
94 fn parse_response_deserializes_a_key_from_the_batch() {
95 let responses = InputResponses::from([("answer".to_string(), json!(42))]);
96
97 assert_eq!(parse_response::<u64>(&responses, "answer").unwrap(), 42);
98 }
99
100 #[test]
101 fn parse_response_rejects_a_missing_key() {
102 let error = parse_response::<u64>(&InputResponses::new(), "answer").unwrap_err();
103
104 assert_eq!(error.message, "missing input response for 'answer'");
105 }
106
107 fn elicitation(message: &str) -> InputRequest {
108 InputRequest::Elicitation(ElicitRequest::new(ElicitRequestParams::FormElicitationParams {
109 meta: None,
110 message: message.to_string(),
111 requested_schema: ElicitationSchema::builder().build().unwrap(),
112 }))
113 }
114
115 fn url_elicitation(message: &str) -> InputRequest {
116 InputRequest::Elicitation(ElicitRequest::new(ElicitRequestParams::UrlElicitationParams {
117 meta: None,
118 message: message.to_string(),
119 url: "https://example.com/input".to_string(),
120 elicitation_id: "id".to_string(),
121 }))
122 }
123}