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}