protocol_droid/application/mcp/input_required.rs
1//! SEP-2322 multi round-trip requests, introduced at **2026-07-28**.
2//!
3//! # What replaced server-initiated requests
4//!
5//! Through `2025-11-25` a tool that needed the user asked the client directly:
6//! it sent `elicitation/create` down the response stream and waited for the
7//! answer to come back on a separate request. `2026-07-28` has no
8//! server-to-client requests at all — the schema's `ServerRequest` union is
9//! gone — because a stateless server has nowhere to wait. Instead the tool
10//! *answers* with an `InputRequiredResult` naming what it needs, and the client
11//! retries the original request carrying `inputResponses` (and the server's
12//! `requestState`, if it sent one).
13//!
14//! So the shape a handler writes is a result, not a call:
15//!
16//! - round one returns [`InputRequired`], built here;
17//! - round two reads the answers off its
18//! [`RequestContext`](crate::application::server::RequestContext).
19//!
20//! # Why `requestState` is sealed by the library
21//!
22//! `requestState` is the server's own memory, held by the client between
23//! rounds. A client that edits it can make a server believe it already asked a
24//! question, or already got an answer, it never did. The SEP's scenario for
25//! this is explicit: a tampered state is rejected. Sealing it here, under a key
26//! the client never sees, means every handler gets that rejection without
27//! writing it — the dispatcher refuses a state that does not open before the
28//! handler runs.
29//!
30//! Sealed, not encrypted: the state is integrity-protected, and its content is
31//! visible to the client. A handler that needs a secret in it must not put one
32//! there; this is the same contract the SEP gives `requestState` itself.
33
34use hmac::{Hmac, KeyInit, Mac};
35use sha2::Sha256;
36
37use crate::domain::ir::JsonObject;
38
39/// The value `resultType` carries on an interim result.
40pub const INPUT_REQUIRED: &str = "input_required";
41
42/// The key a server seals `requestState` under.
43///
44/// Thirty-two bytes, the HMAC-SHA256 block-size recommendation's lower bound,
45/// supplied by the caller rather than generated here: this layer may not draw
46/// from a random source itself. The transports fill one from the OS CSPRNG
47/// when the server was built without one.
48#[derive(Clone)]
49pub struct StateKey([u8; 32]);
50
51impl StateKey {
52 /// A key from raw bytes.
53 ///
54 /// For a deployment running several replicas behind one endpoint, every
55 /// replica must be given the *same* key, or a retry routed to a different
56 /// replica will find its state refused as tampered.
57 #[must_use]
58 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
59 Self(bytes)
60 }
61
62 /// Seals `state` so that any change to it is detectable.
63 ///
64 /// The format is `<hex mac>.<state>`: the MAC is fixed-width, so the first
65 /// `.` always ends it and a state containing dots of its own needs no
66 /// escaping.
67 #[must_use]
68 pub fn seal(&self, state: &str) -> Option<String> {
69 let mut mac = self.hmac()?;
70 mac.update(state.as_bytes());
71 let mac = hex(&mac.finalize().into_bytes());
72 Some(format!("{mac}.{state}"))
73 }
74
75 /// The state a sealed value carries, or `None` if it was altered.
76 ///
77 /// The comparison is the MAC crate's own, which is constant-time; a
78 /// byte-by-byte `==` would tell an attacker how many leading bytes of a
79 /// forged MAC were right.
80 #[must_use]
81 pub fn open<'a>(&self, sealed: &'a str) -> Option<&'a str> {
82 let (mac, state) = sealed.split_once('.')?;
83 let expected = unhex(mac)?;
84 let mut verifier = self.hmac()?;
85 verifier.update(state.as_bytes());
86 verifier.verify_slice(&expected).ok()?;
87 Some(state)
88 }
89
90 /// The keyed MAC. HMAC accepts a key of any length, so this is `None`
91 /// only if that stops being true of the crate behind it.
92 fn hmac(&self) -> Option<Hmac<Sha256>> {
93 <Hmac<Sha256> as KeyInit>::new_from_slice(&self.0).ok()
94 }
95}
96
97impl core::fmt::Debug for StateKey {
98 /// Never prints the key.
99 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
100 f.write_str("StateKey(<redacted>)")
101 }
102}
103
104fn hex(bytes: &[u8]) -> String {
105 use core::fmt::Write as _;
106 bytes
107 .iter()
108 .fold(String::with_capacity(bytes.len() * 2), |mut out, byte| {
109 let _ = write!(out, "{byte:02x}");
110 out
111 })
112}
113
114fn unhex(text: &str) -> Option<Vec<u8>> {
115 if !text.len().is_multiple_of(2) {
116 return None;
117 }
118 text.as_bytes()
119 .chunks(2)
120 .map(|pair| {
121 let pair = core::str::from_utf8(pair).ok()?;
122 u8::from_str_radix(pair, 16).ok()
123 })
124 .collect()
125}
126
127/// An interim result: what the server needs before it can finish.
128///
129/// # Examples
130///
131/// ```
132/// use protocol_droid::application::mcp::input_required::InputRequired;
133/// use protocol_droid::domain::ir::JsonObject;
134///
135/// let ask = JsonObject::new(
136/// r#"{"message":"Name?","requestedSchema":{"type":"object","properties":{"name":{"type":"string"}}}}"#,
137/// )?;
138/// let result = InputRequired::new().elicit("user_name", &ask).into_result();
139/// assert!(result.as_str().contains(r#""resultType":"input_required""#));
140/// # Ok::<(), Box<dyn std::error::Error>>(())
141/// ```
142#[derive(Debug, Clone, Default)]
143pub struct InputRequired {
144 requests: serde_json::Map<String, serde_json::Value>,
145 state: Option<String>,
146}
147
148impl InputRequired {
149 /// An interim result asking for nothing yet.
150 #[must_use]
151 pub fn new() -> Self {
152 Self::default()
153 }
154
155 /// Asks the client's user for input, under `key`.
156 #[must_use]
157 pub fn elicit(self, key: impl Into<String>, params: &JsonObject) -> Self {
158 self.request(key, "elicitation/create", params)
159 }
160
161 /// Asks the client to sample from a model, under `key`.
162 #[must_use]
163 pub fn sample(self, key: impl Into<String>, params: &JsonObject) -> Self {
164 self.request(key, "sampling/createMessage", params)
165 }
166
167 /// Asks the client for its roots, under `key`.
168 #[must_use]
169 pub fn list_roots(self, key: impl Into<String>) -> Self {
170 self.request(key, "roots/list", &JsonObject::default())
171 }
172
173 /// Carries a sealed `requestState` the client will echo on its retry.
174 ///
175 /// Takes the *sealed* form — see
176 /// [`RequestContext::seal_state`](crate::application::server::RequestContext::seal_state).
177 #[must_use]
178 pub fn with_state(mut self, sealed: impl Into<String>) -> Self {
179 self.state = Some(sealed.into());
180 self
181 }
182
183 fn request(mut self, key: impl Into<String>, method: &str, params: &JsonObject) -> Self {
184 let params = serde_json::from_str(params.as_str())
185 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
186 self.requests.insert(
187 key.into(),
188 serde_json::json!({ "method": method, "params": params }),
189 );
190 self
191 }
192
193 /// The result document.
194 ///
195 /// `resultType` is written here rather than left to the dispatcher's
196 /// envelope, because it is what makes this an interim result at all: the
197 /// envelope stamps `"complete"` on anything that does not already say
198 /// otherwise.
199 #[must_use]
200 pub fn into_result(self) -> JsonObject {
201 let mut result = serde_json::Map::new();
202 result.insert("resultType".to_owned(), INPUT_REQUIRED.into());
203 if !self.requests.is_empty() {
204 result.insert(
205 "inputRequests".to_owned(),
206 serde_json::Value::Object(self.requests),
207 );
208 }
209 if let Some(state) = self.state {
210 result.insert("requestState".to_owned(), state.into());
211 }
212 JsonObject::from_value(serde_json::Value::Object(result)).unwrap_or_default()
213 }
214}
215
216/// What a retry carried back: the client's answers and the server's state.
217///
218/// Built by the dispatcher, once the state has been opened, and handed to the
219/// handler through its context. A first round has neither.
220#[derive(Debug, Clone, Default)]
221pub struct InputRound {
222 pub(crate) responses: Option<serde_json::Map<String, serde_json::Value>>,
223 pub(crate) state: Option<String>,
224 pub(crate) key: Option<StateKey>,
225}
226
227impl InputRound {
228 /// The client's answer under `key`, if it sent one.
229 #[must_use]
230 pub fn response(&self, key: &str) -> Option<JsonObject> {
231 let value = self.responses.as_ref()?.get(key)?;
232 JsonObject::from_value(value.clone()).ok()
233 }
234
235 /// Whether this is a retry carrying any answers at all.
236 #[must_use]
237 pub const fn has_responses(&self) -> bool {
238 self.responses.is_some()
239 }
240
241 /// The state the server sealed on the previous round, already verified.
242 #[must_use]
243 pub fn state(&self) -> Option<&str> {
244 self.state.as_deref()
245 }
246
247 /// Seals `state` for the next round, or `None` without a key.
248 #[must_use]
249 pub fn seal(&self, state: &str) -> Option<String> {
250 self.key.as_ref().and_then(|key| key.seal(state))
251 }
252}
253
254/// Whether a result is an interim one.
255#[must_use]
256pub fn is_input_required(result: &serde_json::Map<String, serde_json::Value>) -> bool {
257 result.get("resultType").and_then(serde_json::Value::as_str) == Some(INPUT_REQUIRED)
258}
259
260/// The client capability each input request needs, by method.
261///
262/// The three the schema's `InputRequest` union admits; anything else is not an
263/// input request at all and needs nothing, because it will not be sent.
264#[must_use]
265pub fn capability_for(method: &str) -> Option<&'static str> {
266 match method {
267 "sampling/createMessage" => Some("sampling"),
268 "elicitation/create" => Some("elicitation"),
269 "roots/list" => Some("roots"),
270 _ => None,
271 }
272}
273
274#[cfg(test)]
275pub mod test_keys;
276#[cfg(test)]
277mod tests;