Skip to main content

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;