1use std::collections::{BTreeMap, BTreeSet};
21use std::sync::{Mutex, PoisonError};
22use std::time::{Duration, SystemTime, UNIX_EPOCH};
23
24use base64::Engine as _;
25use base64::engine::general_purpose::URL_SAFE_NO_PAD;
26use hmac::{Hmac, KeyInit as _, Mac as _};
27use schemars::JsonSchema;
28use serde::de::DeserializeOwned;
29use serde::{Deserialize, Serialize};
30use serde_json::{Map, Value, json};
31use sha2::{Digest as _, Sha256};
32
33use crate::context::Ctx;
34use crate::error::McpError;
35use crate::spec::{self, methods};
36use crate::tool::{ToolError, schema_for};
37
38type HmacSha256 = Hmac<Sha256>;
39
40pub const MIN_REQUEST_STATE_KEY_BYTES: usize = 32;
42
43const STATE_VERSION: u8 = 1;
45
46const SIGNATURE_CONTEXT: &[u8] = b"flymcprust/requestState/v1\0";
49
50pub(crate) struct StateCodec {
52 key: Vec<u8>,
53 ttl: Duration,
54}
55
56impl std::fmt::Debug for StateCodec {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("StateCodec")
59 .field("ttl", &self.ttl)
60 .finish_non_exhaustive()
61 }
62}
63
64#[derive(Serialize, Deserialize)]
66struct Payload {
67 v: u8,
68 m: String,
70 t: String,
72 a: String,
74 e: u64,
76 n: u32,
78 r: BTreeMap<String, Value>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 p: Option<String>,
83}
84
85impl StateCodec {
86 pub(crate) fn new(key: Vec<u8>, ttl: Duration) -> Self {
87 Self { key, ttl }
88 }
89
90 fn mac(&self, context: &[u8], data: &[u8]) -> Option<HmacSha256> {
91 let mut mac = HmacSha256::new_from_slice(&self.key).ok()?;
92 mac.update(context);
93 mac.update(data);
94 Some(mac)
95 }
96
97 pub(crate) fn seal_for<T: Serialize>(&self, context: &[u8], payload: &T) -> Option<String> {
100 let json = serde_json::to_vec(payload).ok()?;
101 let body = URL_SAFE_NO_PAD.encode(json);
102 let tag = self.mac(context, body.as_bytes())?.finalize().into_bytes();
103 Some(format!("{body}.{}", URL_SAFE_NO_PAD.encode(tag)))
104 }
105
106 pub(crate) fn open_for<T: DeserializeOwned>(&self, context: &[u8], token: &str) -> Option<T> {
109 let (body, tag) = token.rsplit_once('.')?;
110 let tag = URL_SAFE_NO_PAD.decode(tag).ok()?;
111 self.mac(context, body.as_bytes())?
112 .verify_slice(&tag)
113 .ok()?;
114 let json = URL_SAFE_NO_PAD.decode(body).ok()?;
115 serde_json::from_slice(&json).ok()
116 }
117
118 fn seal(&self, payload: &Payload) -> Result<String, McpError> {
119 self.seal_for(SIGNATURE_CONTEXT, payload)
120 .ok_or_else(McpError::internal)
121 }
122
123 fn open(&self, token: &str, round: &RoundKey<'_>) -> Result<Payload, McpError> {
124 let digest = round.args_digest();
125 let invalid = || McpError::invalid_params("Invalid or expired requestState");
126 let payload: Payload = self
127 .open_for(SIGNATURE_CONTEXT, token)
128 .ok_or_else(invalid)?;
129 if payload.v != STATE_VERSION
130 || payload.m != round.method
131 || payload.t != round.target
132 || payload.a != digest
133 || payload.p.as_deref() != round.principal
134 || payload.e < now_secs()
135 {
136 return Err(invalid());
137 }
138 Ok(payload)
139 }
140}
141
142pub(crate) fn now_secs() -> u64 {
143 SystemTime::now()
144 .duration_since(UNIX_EPOCH)
145 .map_or(0, |d| d.as_secs())
146}
147
148pub(crate) struct RoundKey<'a> {
151 pub(crate) method: &'static str,
152 pub(crate) target: &'a str,
153 pub(crate) arguments: Args<'a>,
154 pub(crate) principal: Option<&'a str>,
157}
158
159#[derive(Clone, Copy)]
161pub(crate) enum Args<'a> {
162 None,
163 Raw(&'a str),
165 Map(&'a BTreeMap<String, String>),
167}
168
169impl RoundKey<'_> {
170 fn args_digest(&self) -> String {
172 let canonical = match self.arguments {
173 Args::None => Vec::new(),
174 Args::Raw(raw) => serde_json::from_str::<Value>(raw)
175 .ok()
176 .and_then(|mut v| {
177 v.sort_all_objects();
178 serde_json::to_vec(&v).ok()
179 })
180 .unwrap_or_default(),
181 Args::Map(map) => serde_json::to_vec(map).unwrap_or_default(),
182 };
183 URL_SAFE_NO_PAD.encode(Sha256::digest(&canonical))
184 }
185}
186
187#[derive(Debug, Default)]
189pub(crate) struct Round {
190 number: u32,
192 responses: BTreeMap<String, Value>,
195 pending: Mutex<Vec<(String, Value)>>,
197 used: Mutex<BTreeSet<String>>,
200}
201
202impl Round {
203 pub(crate) fn start(
205 codec: &StateCodec,
206 key: &RoundKey<'_>,
207 input_responses: Option<Value>,
208 request_state: Option<&str>,
209 ) -> Result<Self, McpError> {
210 let (number, mut responses) = match request_state {
211 Some(token) => {
212 let payload = codec.open(token, key)?;
213 (payload.n, payload.r)
214 }
215 None => (0, BTreeMap::new()),
216 };
217 match input_responses {
218 None | Some(Value::Null) => {}
219 Some(Value::Object(map)) => {
220 for (k, v) in map {
221 responses.entry(k).or_insert(v);
222 }
223 }
224 Some(_) => {
225 return Err(McpError::invalid_params(
226 "`inputResponses` must be an object",
227 ));
228 }
229 }
230 Ok(Self {
231 number,
232 responses,
233 pending: Mutex::new(Vec::new()),
234 used: Mutex::new(BTreeSet::new()),
235 })
236 }
237
238 pub(crate) fn input_required(
240 &self,
241 codec: &StateCodec,
242 key: &RoundKey<'_>,
243 meta: Option<&spec::ResultMetaObject>,
244 ) -> Result<Value, McpError> {
245 let pending =
246 std::mem::take(&mut *self.pending.lock().unwrap_or_else(PoisonError::into_inner));
247 let expiry = now_secs().saturating_add(codec.ttl.as_secs());
248 let state = codec.seal(&Payload {
249 v: STATE_VERSION,
250 m: key.method.to_owned(),
251 t: key.target.to_owned(),
252 a: key.args_digest(),
253 e: expiry,
254 p: key.principal.map(str::to_owned),
255 n: self.number.saturating_add(1),
256 r: {
257 let used = self.used.lock().unwrap_or_else(PoisonError::into_inner);
258 self.responses
259 .iter()
260 .filter(|(k, _)| used.contains(k.as_str()))
261 .map(|(k, v)| (k.clone(), v.clone()))
262 .collect()
263 },
264 })?;
265 let mut result = Map::new();
266 result.insert("resultType".to_owned(), Value::from("input_required"));
267 if !pending.is_empty() {
268 result.insert(
269 "inputRequests".to_owned(),
270 Value::Object(pending.into_iter().collect()),
271 );
272 }
273 result.insert("requestState".to_owned(), Value::from(state));
274 if let Some(meta) = meta {
275 let meta = serde_json::to_value(meta).map_err(|_| McpError::internal())?;
276 result.insert("_meta".to_owned(), meta);
277 }
278 Ok(Value::Object(result))
279 }
280
281 fn response(&self, key: &str) -> Option<&Value> {
282 let value = self.responses.get(key)?;
283 self.used
284 .lock()
285 .unwrap_or_else(PoisonError::into_inner)
286 .insert(key.to_owned());
287 Some(value)
288 }
289
290 fn push(&self, key: &str, request: Value) {
291 let mut pending = self.pending.lock().unwrap_or_else(PoisonError::into_inner);
292 if !pending.iter().any(|(k, _)| k == key) {
293 pending.push((key.to_owned(), request));
294 }
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
300pub enum Elicitation<T> {
301 Accepted(T),
303 Declined,
305 Cancelled,
307}
308
309#[derive(Deserialize)]
310struct ElicitAnswer {
311 action: String,
312 #[serde(default)]
313 content: Option<Value>,
314}
315
316#[derive(Clone, Copy)]
318enum Needs {
319 Elicitation,
320 Sampling,
321 Roots,
322}
323
324impl Ctx {
325 #[must_use]
328 pub fn input_round(&self) -> u32 {
329 self.round().map_or(0, |r| r.number)
330 }
331
332 pub fn input_response<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, ToolError> {
340 let Some(value) = self.round().and_then(|r| r.response(key)) else {
341 return Ok(None);
342 };
343 T::deserialize(value)
344 .map(Some)
345 .map_err(|_| invalid_response(key))
346 }
347
348 pub async fn elicit<T: DeserializeOwned + JsonSchema>(
363 &self,
364 key: &str,
365 message: impl Into<String>,
366 ) -> Result<T, ToolError> {
367 match self.elicit_with_action::<T>(key, message).await? {
368 Elicitation::Accepted(value) => Ok(value),
369 Elicitation::Declined => Err(ToolError::user(
370 "The user declined to provide the requested information.",
371 )),
372 Elicitation::Cancelled => Err(ToolError::user("The user cancelled the request.")),
373 }
374 }
375
376 pub async fn elicit_with_action<T: DeserializeOwned + JsonSchema>(
383 &self,
384 key: &str,
385 message: impl Into<String>,
386 ) -> Result<Elicitation<T>, ToolError> {
387 if let Some(answer) = self.input_response::<ElicitAnswer>(key)? {
388 return match answer.action.as_str() {
389 "accept" => {
390 let content = answer.content.unwrap_or_else(|| json!({}));
391 T::deserialize(&content)
392 .map(Elicitation::Accepted)
393 .map_err(|_| invalid_response(key))
394 }
395 "decline" => Ok(Elicitation::Declined),
396 "cancel" => Ok(Elicitation::Cancelled),
397 _ => Err(invalid_response(key)),
398 };
399 }
400 let schema = elicitation_schema::<T>()?;
401 let request = json!({
402 "method": methods::ELICITATION_CREATE,
403 "params": { "message": message.into(), "requestedSchema": schema },
404 });
405 Err(self.ask(key, request, Needs::Elicitation))
406 }
407
408 pub async fn elicit_with_schema(
417 &self,
418 key: &str,
419 message: impl Into<String>,
420 requested_schema: Value,
421 ) -> Result<Elicitation<Map<String, Value>>, ToolError> {
422 if let Some(answer) = self.input_response::<ElicitAnswer>(key)? {
423 return match answer.action.as_str() {
424 "accept" => match answer.content.unwrap_or_else(|| json!({})) {
425 Value::Object(content) => Ok(Elicitation::Accepted(content)),
426 _ => Err(invalid_response(key)),
427 },
428 "decline" => Ok(Elicitation::Declined),
429 "cancel" => Ok(Elicitation::Cancelled),
430 _ => Err(invalid_response(key)),
431 };
432 }
433 let request = json!({
434 "method": methods::ELICITATION_CREATE,
435 "params": { "message": message.into(), "requestedSchema": requested_schema },
436 });
437 Err(self.ask(key, request, Needs::Elicitation))
438 }
439
440 #[deprecated(note = "Sampling is deprecated as of protocol version 2026-07-28 (SEP-2577).")]
448 #[allow(deprecated)]
449 pub async fn sample(
450 &self,
451 key: &str,
452 params: spec::CreateMessageRequestParams,
453 ) -> Result<spec::CreateMessageResult, ToolError> {
454 if let Some(answer) = self.input_response(key)? {
455 return Ok(answer);
456 }
457 let request = json!({ "method": methods::SAMPLING_CREATE_MESSAGE, "params": params });
458 Err(self.ask(key, request, Needs::Sampling))
459 }
460
461 #[deprecated(note = "Roots are deprecated as of protocol version 2026-07-28 (SEP-2577).")]
469 #[allow(deprecated)]
470 pub async fn list_roots(&self, key: &str) -> Result<spec::ListRootsResult, ToolError> {
471 if let Some(answer) = self.input_response(key)? {
472 return Ok(answer);
473 }
474 let request = json!({ "method": methods::ROOTS_LIST, "params": {} });
475 Err(self.ask(key, request, Needs::Roots))
476 }
477
478 fn ask(&self, key: &str, request: Value, needs: Needs) -> ToolError {
480 let Some(round) = self.round() else {
481 return ToolError::internal(std::io::Error::other(
482 "input requests are only available in tools/call, resources/read and prompts/get",
483 ));
484 };
485 #[allow(deprecated)]
486 let caps = self.client_capabilities();
487 #[allow(deprecated)]
488 let (supported, required) = match needs {
489 Needs::Elicitation => (
490 caps.elicitation
491 .as_ref()
492 .is_some_and(|e| e.form.is_some() || e.url.is_none()),
493 spec::ClientCapabilities {
494 elicitation: Some(spec::ClientCapabilitiesElicitation {
495 form: Some(Map::new()),
496 url: None,
497 }),
498 ..spec::ClientCapabilities::default()
499 },
500 ),
501 Needs::Sampling => (
502 caps.sampling.is_some(),
503 spec::ClientCapabilities {
504 sampling: Some(spec::ClientCapabilitiesSampling::default()),
505 ..spec::ClientCapabilities::default()
506 },
507 ),
508 Needs::Roots => (
509 caps.roots.is_some(),
510 spec::ClientCapabilities {
511 roots: Some(spec::ClientCapabilitiesRoots::default()),
512 ..spec::ClientCapabilities::default()
513 },
514 ),
515 };
516 if !supported {
517 static HINT: crate::hints::Once = crate::hints::Once::new();
518 crate::hints::warn_once(
519 &HINT,
520 format_args!(
521 "a client that did not declare the needed capability called a handler \
522 that asks it for input; it received -32021"
523 ),
524 "check ctx.client_capabilities() before asking if the handler can work \
525 without the answer",
526 );
527 return ToolError::missing_client_capability(&required);
528 }
529 round.push(key, request);
530 ToolError::input_required()
531 }
532}
533
534fn invalid_response(key: &str) -> ToolError {
535 ToolError::protocol(McpError::invalid_params(format!(
536 "Invalid input response for `{}`",
537 crate::error::truncate(key)
538 )))
539}
540
541fn elicitation_schema<T: JsonSchema>() -> Result<Value, ToolError> {
545 let mut schema = schema_for::<T>();
546 let unsupported = || {
547 ToolError::internal(std::io::Error::other(format!(
548 "`{}` cannot be used with ctx.elicit\n = help: use a struct whose fields are strings, \
549 numbers, integers, booleans or enums (optionally in Option<..>); forms cannot nest",
550 std::any::type_name::<T>()
551 )))
552 };
553 if schema.get("type").and_then(Value::as_str) != Some("object") {
554 return Err(unsupported());
555 }
556 schema.remove("additionalProperties");
557 let Some(Value::Object(properties)) = schema.get_mut("properties") else {
558 return Err(unsupported());
559 };
560 for property in properties.values_mut() {
561 let Value::Object(property) = property else {
562 return Err(unsupported());
563 };
564 if let Some(Value::Array(types)) = property.get("type") {
565 let non_null: Vec<&Value> = types
566 .iter()
567 .filter(|t| t.as_str() != Some("null"))
568 .collect();
569 if let [single] = non_null.as_slice() {
570 let single = (*single).clone();
571 property.insert("type".to_owned(), single);
572 }
573 }
574 let primitive = matches!(
575 property.get("type").and_then(Value::as_str),
576 Some("string" | "number" | "integer" | "boolean" | "array")
577 ) || property.contains_key("enum")
578 || property.contains_key("oneOf")
579 || property.contains_key("anyOf");
580 if !primitive {
581 return Err(unsupported());
582 }
583 }
584 Ok(Value::Object(schema))
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590
591 fn codec() -> StateCodec {
592 StateCodec::new(vec![7; 32], Duration::from_secs(600))
593 }
594
595 fn key(target: &str) -> RoundKey<'_> {
596 RoundKey {
597 method: "tools/call",
598 target,
599 arguments: Args::Raw(r#"{"a": 1}"#),
600 principal: None,
601 }
602 }
603
604 fn round_trip(codec: &StateCodec, round: &Round) -> String {
605 round.input_required(codec, &key("t"), None).unwrap()["requestState"]
606 .as_str()
607 .unwrap()
608 .to_owned()
609 }
610
611 #[test]
612 fn state_round_trips_and_accumulates_answers() {
613 let codec = codec();
614 let first = Round::start(
615 &codec,
616 &key("t"),
617 Some(json!({ "q1": 1, "unread": 0 })),
618 None,
619 )
620 .unwrap();
621 assert_eq!(first.response("q1"), Some(&json!(1)));
623 let token = round_trip(&codec, &first);
624 let second =
625 Round::start(&codec, &key("t"), Some(json!({ "q2": 2 })), Some(&token)).unwrap();
626 assert_eq!(second.number, 1);
627 assert_eq!(second.response("q1"), Some(&json!(1)));
628 assert_eq!(second.response("q2"), Some(&json!(2)));
629 assert_eq!(second.response("unread"), None);
631 }
632
633 #[test]
634 fn tampered_or_rebound_state_is_rejected() {
635 let codec = codec();
636 let round = Round::start(&codec, &key("t"), None, None).unwrap();
637 let token = round_trip(&codec, &round);
638 let reject = |token: &str, key: RoundKey<'_>| {
639 Round::start(&codec, &key, None, Some(token))
640 .unwrap_err()
641 .code()
642 };
643 assert_eq!(reject(&format!("{token}-TAMPERED"), key("t")), -32602);
644 assert_eq!(reject("garbage", key("t")), -32602);
645 assert_eq!(reject(&token, key("other")), -32602);
647 let mut other_args = key("t");
648 other_args.arguments = Args::Raw(r#"{"a":2}"#);
649 assert_eq!(reject(&token, other_args), -32602);
650 let mut other_user = key("t");
652 other_user.principal = Some("someone-else");
653 assert_eq!(reject(&token, other_user), -32602);
654 let other_key = StateCodec::new(vec![8; 32], Duration::from_secs(600));
655 assert!(Round::start(&other_key, &key("t"), None, Some(&token)).is_err());
656 }
657
658 #[test]
659 fn expired_state_is_rejected() {
660 let codec = StateCodec::new(vec![7; 32], Duration::ZERO);
661 let round = Round::start(&codec, &key("t"), None, None).unwrap();
662 let mut payload: Payload = {
663 let token = round_trip(&codec, &round);
664 let body = token.split('.').next().unwrap();
665 serde_json::from_slice(&URL_SAFE_NO_PAD.decode(body).unwrap()).unwrap()
666 };
667 payload.e = now_secs() - 1;
668 let token = codec.seal(&payload).unwrap();
669 assert!(Round::start(&codec, &key("t"), None, Some(&token)).is_err());
670 }
671
672 #[test]
673 fn argument_digest_ignores_whitespace() {
674 let digest = |raw| {
675 RoundKey {
676 method: "tools/call",
677 target: "t",
678 arguments: Args::Raw(raw),
679 principal: None,
680 }
681 .args_digest()
682 };
683 assert_eq!(digest(r#"{"a":1}"#), digest(r#"{ "a" : 1 }"#));
684 assert_eq!(
685 digest(r#"{"a":1,"b":{"y":2,"x":1}}"#),
686 digest(r#"{"b":{"x":1,"y":2},"a":1}"#)
687 );
688 assert_ne!(digest(r#"{"a":1}"#), digest(r#"{"a":2}"#));
689 }
690
691 #[test]
692 fn invalid_input_responses_are_rejected() {
693 let err = Round::start(&codec(), &key("t"), Some(json!(5)), None).unwrap_err();
694 assert_eq!(err.code(), -32602);
695 }
696
697 #[derive(JsonSchema)]
698 #[allow(dead_code)]
699 struct Form {
700 name: String,
701 age: Option<u32>,
702 subscribe: bool,
703 }
704
705 #[test]
706 fn elicitation_schema_is_flat() {
707 let schema = elicitation_schema::<Form>().unwrap();
708 assert_eq!(
709 schema,
710 json!({
711 "type": "object",
712 "properties": {
713 "name": { "type": "string" },
714 "age": { "type": "integer", "minimum": 0 },
715 "subscribe": { "type": "boolean" },
716 },
717 "required": ["name", "subscribe"],
718 })
719 );
720 #[derive(JsonSchema)]
721 #[allow(dead_code)]
722 struct Nested {
723 inner: Form,
724 }
725 assert!(elicitation_schema::<Nested>().is_err());
726 }
727}