Skip to main content

mcp_utils/server/
mrtr.rs

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
27/// Deserialize one keyed response from a complete MRTR response batch.
28pub 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}