Skip to main content

protocol_droid/application/client/
inputs.rs

1//! Answering a server's request for input (SEP-2322, `2026-07-28`).
2//!
3//! A server cannot send a request of its own at this revision, so a call that
4//! needs the user answers with an `InputRequiredResult`: `resultType:
5//! "input_required"`, the questions in `inputRequests`, and optionally a
6//! `requestState` of its own. The client answers by *retrying the original
7//! request* with `inputResponses` keyed as the questions were, echoing
8//! `requestState` back unchanged — and sending none when the server sent none.
9//! Every retry is a new JSON-RPC request with a new id.
10//! <https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr>
11//!
12//! This works on any method the SEP names (`tools/call`, `prompts/get`,
13//! `resources/read`) and over any [`ClientTransport`], handshake or not.
14
15use core::future::Future;
16use core::pin::Pin;
17
18use serde_json::{Map, Value};
19
20use super::{ClientError, ClientTransport};
21use crate::domain::ir::JsonObject;
22
23/// Answers one input request.
24///
25/// Given the request's key, its method (`elicitation/create`,
26/// `sampling/createMessage` or `roots/list`) and its params, returns the answer
27/// — an `ElicitResult`, `CreateMessageResult` or `ListRootsResult` — or `None`
28/// when it cannot. Any closure `Fn(String, String, JsonObject) -> impl Future`
29/// is one, so most callers never name this trait.
30///
31/// A trait with a boxed future rather than an `AsyncFn` bound because an
32/// `AsyncFn`'s future cannot be required to be `Send` on stable Rust, and a
33/// call that cannot be moved onto a `tokio::spawn` is not usable by the
34/// callers who need it most.
35///
36/// # Examples
37///
38/// ```
39/// use protocol_droid::application::client::inputs::InputResolver;
40/// use protocol_droid::domain::ir::JsonObject;
41///
42/// fn takes(_: &impl InputResolver) {}
43///
44/// takes(&|_key: String, method: String, _params: JsonObject| async move {
45///     (method == "roots/list").then(|| JsonObject::new(r#"{"roots":[]}"#).unwrap_or_default())
46/// });
47/// ```
48pub trait InputResolver: Send + Sync {
49    /// Answers the request, or declines with `None`.
50    fn resolve<'a>(
51        &'a self,
52        key: &'a str,
53        method: &'a str,
54        params: &'a JsonObject,
55    ) -> Pin<Box<dyn Future<Output = Option<JsonObject>> + Send + 'a>>;
56}
57
58impl<F, Fut> InputResolver for F
59where
60    F: Fn(String, String, JsonObject) -> Fut + Send + Sync,
61    Fut: Future<Output = Option<JsonObject>> + Send + 'static,
62{
63    fn resolve<'a>(
64        &'a self,
65        key: &'a str,
66        method: &'a str,
67        params: &'a JsonObject,
68    ) -> Pin<Box<dyn Future<Output = Option<JsonObject>> + Send + 'a>> {
69        Box::pin(self(key.to_owned(), method.to_owned(), params.clone()))
70    }
71}
72
73/// How many times a call may be retried with fresh input before it fails.
74///
75/// A server that keeps asking is either broken or hostile, and a client that
76/// answered forever would be a way to make it prompt a person without end.
77pub const MAX_INPUT_ROUNDS: u32 = 8;
78
79/// What a client answers an `InputRequiredResult` with.
80#[derive(Debug, Clone, Default, PartialEq, Eq)]
81pub struct InputAnswers {
82    /// The `inputResponses`, keyed as the server's `inputRequests` were.
83    pub responses: Map<String, Value>,
84    /// The server's `requestState`, to echo unchanged; `None` when it sent none.
85    pub state: Option<String>,
86}
87
88impl InputAnswers {
89    /// `params` with these answers added: the original call, retried.
90    #[must_use]
91    pub fn applied_to(&self, params: &JsonObject) -> JsonObject {
92        let mut attempt: Map<String, Value> = match serde_json::from_str(params.as_str()) {
93            Ok(Value::Object(object)) => object,
94            _ => Map::new(),
95        };
96        attempt.insert(
97            "inputResponses".to_owned(),
98            Value::Object(self.responses.clone()),
99        );
100        if let Some(state) = &self.state {
101            attempt.insert("requestState".to_owned(), Value::from(state.clone()));
102        }
103        JsonObject::from_value(Value::Object(attempt)).unwrap_or_default()
104    }
105}
106
107/// Answers `result` if it is an interim `InputRequiredResult`.
108///
109/// `Ok(None)` for a complete result — including one with no `resultType` at
110/// all, which a server older than the field never wrote and which must be read
111/// as complete and not retried. This is the step [`call_resolving`] repeats;
112/// it is public for a caller that has to do something between the question
113/// and the retry.
114///
115/// # Errors
116///
117/// [`ClientError::InputUnresolved`] when `resolve` cannot answer a request.
118pub async fn answer_input_required(
119    result: &JsonObject,
120    resolve: &(impl InputResolver + ?Sized),
121) -> Result<Option<InputAnswers>, ClientError> {
122    let parsed: Value = serde_json::from_str(result.as_str()).unwrap_or_default();
123    if parsed.get("resultType").and_then(Value::as_str) != Some("input_required") {
124        return Ok(None);
125    }
126    let state = parsed
127        .get("requestState")
128        .and_then(Value::as_str)
129        .map(ToOwned::to_owned);
130    let requests = parsed
131        .get("inputRequests")
132        .and_then(Value::as_object)
133        .cloned()
134        .unwrap_or_default();
135    let mut responses = Map::new();
136    for (key, request) in requests {
137        let asked = request
138            .get("method")
139            .and_then(Value::as_str)
140            .unwrap_or_default()
141            .to_owned();
142        let asked_params =
143            JsonObject::from_value(request.get("params").cloned().unwrap_or_default())
144                .unwrap_or_default();
145        let Some(answer) = resolve.resolve(&key, &asked, &asked_params).await else {
146            return Err(ClientError::InputUnresolved { method: asked });
147        };
148        responses.insert(
149            key,
150            serde_json::from_str(answer.as_str()).unwrap_or_default(),
151        );
152    }
153    Ok(Some(InputAnswers { responses, state }))
154}
155
156/// Sends `method` with `params`, answering input requests through `resolve`.
157///
158/// `resolve` is given the request's key, its method (`elicitation/create`,
159/// `sampling/createMessage` or `roots/list`) and its params, and returns the
160/// answer — an `ElicitResult`, `CreateMessageResult` or `ListRootsResult` — or
161/// `None` when it cannot.
162///
163/// # Errors
164///
165/// [`ClientError::Transport`] as for any call, [`ClientError::InputUnresolved`]
166/// when `resolve` returns `None` (the call is abandoned, never answered on the
167/// caller's behalf), and [`ClientError::TooManyRounds`] past
168/// [`MAX_INPUT_ROUNDS`].
169pub async fn call_resolving(
170    transport: &(impl ClientTransport + ?Sized),
171    method: &str,
172    params: &JsonObject,
173    resolve: &(impl InputResolver + ?Sized),
174) -> Result<JsonObject, ClientError> {
175    let mut attempt = params.clone();
176    for _ in 0..MAX_INPUT_ROUNDS {
177        let result = transport.send(method, &attempt).await?;
178        let Some(answers) = answer_input_required(&result, resolve).await? else {
179            return Ok(result);
180        };
181        // Each retry starts from the original params, not the last attempt:
182        // an answer from round one must not ride along into round three.
183        attempt = answers.applied_to(params);
184    }
185    Err(ClientError::TooManyRounds(MAX_INPUT_ROUNDS))
186}