#[cfg(feature = "server")]
pub(crate) mod state;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::types::elicitation::ElicitRequestParams;
use crate::types::root::ListRootsRequestParams;
use crate::types::sampling::CreateMessageRequestParams;
use crate::types::{IntoResponse, RequestId, Response};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputRequiredResult {
#[serde(rename = "resultType")]
pub result_type: InputRequiredTag,
#[serde(rename = "inputRequests", skip_serializing_if = "Option::is_none")]
pub input_requests: Option<InputRequests>,
#[serde(rename = "requestState", skip_serializing_if = "Option::is_none")]
pub request_state: Option<String>,
}
pub type InputRequests = HashMap<String, InputRequest>;
pub type InputResponses = HashMap<String, serde_json::Value>;
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "method", content = "params")]
pub enum InputRequest {
#[serde(rename = "elicitation/create")]
Elicitation(ElicitRequestParams),
#[serde(rename = "sampling/createMessage")]
#[deprecated(
note = "sampling is deprecated in MCP 2026-07-28; it returns as an MRTR input-request kind only for migration"
)]
Sampling(Box<CreateMessageRequestParams>),
#[serde(rename = "roots/list")]
#[deprecated(
note = "roots are deprecated in MCP 2026-07-28; they return as an MRTR input-request kind only for migration"
)]
Roots(Box<ListRootsRequestParams>),
}
impl<'de> Deserialize<'de> for InputRequest {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use crate::types::{elicitation, root, sampling};
use serde::de::Error as DeError;
#[derive(Deserialize)]
struct Envelope {
method: String,
#[serde(default)]
params: Option<serde_json::Value>,
}
let envelope = Envelope::deserialize(deserializer)?;
let params = envelope
.params
.filter(|params| !params.is_null())
.unwrap_or_else(|| serde_json::Value::Object(Default::default()));
fn parse<T: serde::de::DeserializeOwned, E: DeError>(
value: serde_json::Value,
) -> Result<T, E> {
serde_json::from_value(value).map_err(E::custom)
}
#[allow(deprecated)]
match envelope.method.as_str() {
elicitation::commands::CREATE => parse(params).map(Self::Elicitation),
sampling::commands::CREATE => parse(params).map(Self::Sampling),
root::commands::LIST => parse(params).map(Self::Roots),
unknown => Err(D::Error::custom(format!(
"unknown MRTR input request method `{unknown}`"
))),
}
}
}
impl InputRequest {
pub fn method(&self) -> &'static str {
#[allow(deprecated)]
match self {
Self::Elicitation(_) => crate::types::elicitation::commands::CREATE,
Self::Sampling(_) => crate::types::sampling::commands::CREATE,
Self::Roots(_) => crate::types::root::commands::LIST,
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct ClientMrtrCapabilities {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub elicitation: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub sampling: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub roots: bool,
}
#[cfg(feature = "server")]
impl ClientMrtrCapabilities {
pub(crate) fn allows(&self, request: &InputRequest) -> bool {
#[allow(deprecated)]
match request {
InputRequest::Elicitation(_) => self.elicitation,
InputRequest::Sampling(_) => self.sampling,
InputRequest::Roots(_) => self.roots,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum InputRequiredTag {
#[serde(rename = "input_required")]
InputRequired,
}
#[cfg(feature = "server")]
impl InputRequiredResult {
pub(crate) fn single(key: String, request: InputRequest, state: String) -> Self {
let mut input_requests = HashMap::with_capacity(1);
input_requests.insert(key, request);
Self {
result_type: InputRequiredTag::InputRequired,
input_requests: Some(input_requests),
request_state: Some(state),
}
}
}
impl IntoResponse for InputRequiredResult {
#[inline]
fn into_response(self, req_id: RequestId) -> Response {
match serde_json::to_value(self) {
Ok(v) => Response::success(req_id, v),
Err(err) => Response::error(req_id, err.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn input_required_result_roundtrips_with_tag_and_envelope() {
let json = r#"{
"resultType": "input_required",
"inputRequests": {
"ask_name": {
"method": "elicitation/create",
"params": { "Form": {
"message": "Your name?",
"mode": null,
"requestedSchema": { "type": "object", "properties": {}, "required": null }
}}
}
},
"requestState": "abc.def"
}"#;
let parsed: InputRequiredResult = serde_json::from_str(json).unwrap();
assert_eq!(parsed.request_state.as_deref(), Some("abc.def"));
assert!(
parsed
.input_requests
.as_ref()
.expect("requests")
.contains_key("ask_name")
);
let back = serde_json::to_value(&parsed).unwrap();
assert_eq!(back["resultType"], serde_json::json!("input_required"));
assert_eq!(
back["inputRequests"]["ask_name"]["method"],
serde_json::json!("elicitation/create")
);
}
#[test]
fn every_input_kind_roundtrips_as_a_method_params_envelope() {
#[allow(deprecated)]
let cases = [
(
InputRequest::Elicitation(ElicitRequestParams::form("Your name?").into()),
"elicitation/create",
),
(
InputRequest::Sampling(Box::default()),
"sampling/createMessage",
),
(InputRequest::Roots(Box::default()), "roots/list"),
];
for (request, method) in cases {
assert_eq!(request.method(), method);
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["method"], serde_json::json!(method));
assert!(
json.get("params").is_some(),
"the envelope must carry `params` for {method}: {json}"
);
let back: InputRequest = serde_json::from_value(json).unwrap();
assert_eq!(back.method(), method, "kind must survive the round trip");
}
}
#[test]
fn a_roots_envelope_decodes_with_or_without_params() {
for json in [
serde_json::json!({ "method": "roots/list", "params": {} }),
serde_json::json!({ "method": "roots/list" }),
serde_json::json!({ "method": "roots/list", "params": null }),
] {
let parsed: InputRequest = serde_json::from_value(json.clone())
.unwrap_or_else(|err| panic!("{json} must decode: {err}"));
assert_eq!(parsed.method(), "roots/list");
}
}
#[test]
fn a_paramless_envelope_still_fails_for_kinds_that_need_params() {
for method in ["elicitation/create", "sampling/createMessage"] {
let json = serde_json::json!({ "method": method });
assert!(
serde_json::from_value::<InputRequest>(json).is_err(),
"{method} must not decode without params"
);
}
}
#[test]
fn an_unknown_input_kind_is_rejected_by_name() {
let json = serde_json::json!({ "method": "sorcery/summon", "params": {} });
let err = serde_json::from_value::<InputRequest>(json).unwrap_err();
assert!(
err.to_string().contains("sorcery/summon"),
"the error must name the unknown method, got: {err}"
);
}
#[cfg(feature = "server")]
#[test]
fn capabilities_gate_each_kind_independently() {
#[allow(deprecated)]
let sampling = InputRequest::Sampling(Box::default());
let elicitation = InputRequest::Elicitation(ElicitRequestParams::form("m").into());
let only_elicitation = ClientMrtrCapabilities {
elicitation: true,
..Default::default()
};
assert!(only_elicitation.allows(&elicitation));
assert!(
!only_elicitation.allows(&sampling),
"a client that only does elicitation must not be asked to sample"
);
let all = ClientMrtrCapabilities {
elicitation: true,
sampling: true,
roots: true,
};
assert!(all.allows(&sampling));
}
#[test]
fn capabilities_decode_from_an_older_peer() {
let caps: ClientMrtrCapabilities =
serde_json::from_value(serde_json::json!({ "elicitation": true })).unwrap();
assert!(caps.elicitation);
assert!(!caps.sampling);
assert!(!caps.roots);
let json = serde_json::to_value(ClientMrtrCapabilities::default()).unwrap();
assert_eq!(json, serde_json::json!({}));
}
}