1use std::cell::RefCell;
9use std::future::Future;
10
11use serde_json::{Map as JsonMap, Value as JsonValue};
12
13use crate::value::{VmError, VmValue};
14
15#[derive(Clone, Debug)]
17pub struct McpInputRequired {
18 pub key: String,
19 pub request: JsonValue,
20 pub request_state: String,
21}
22
23#[derive(Debug)]
24struct InputContext {
25 client_capabilities: JsonValue,
26 responses: JsonMap<String, JsonValue>,
27 next_request: usize,
28}
29
30tokio::task_local! {
31 static INPUT_CONTEXT: RefCell<InputContext>;
32}
33
34pub(crate) async fn scope_input_context<F>(
35 params: &JsonValue,
36 client_capabilities: JsonValue,
37 future: F,
38) -> Result<F::Output, String>
39where
40 F: Future,
41{
42 let mut responses = match params.get("requestState") {
43 Some(JsonValue::String(state)) => serde_json::from_str::<JsonMap<String, JsonValue>>(state)
44 .map_err(|error| format!("invalid MCP requestState: {error}"))?,
45 Some(JsonValue::Null) | None => JsonMap::new(),
46 Some(_) => return Err("MCP requestState must be a string".to_string()),
47 };
48 if let Some(current) = params.get("inputResponses") {
49 let current = current
50 .as_object()
51 .ok_or_else(|| "MCP inputResponses must be an object".to_string())?;
52 responses.extend(current.clone());
53 }
54 Ok(INPUT_CONTEXT
55 .scope(
56 RefCell::new(InputContext {
57 client_capabilities,
58 responses,
59 next_request: 0,
60 }),
61 future,
62 )
63 .await)
64}
65
66pub(crate) fn request_input(
69 method: &str,
70 params: JsonValue,
71 error_prefix: &str,
72) -> Result<JsonValue, VmError> {
73 INPUT_CONTEXT
74 .try_with(|cell| {
75 let mut context = cell.borrow_mut();
76 if !supports_input(&context.client_capabilities, method, ¶ms) {
77 return Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
78 format!("{error_prefix}: the MCP client did not advertise support for {method}"),
79 ))));
80 }
81
82 let key = format!("harn-input-{}", context.next_request);
83 context.next_request += 1;
84 if let Some(response) = context.responses.get(&key) {
85 return Ok(response.clone());
86 }
87
88 let request_state = serde_json::to_string(&context.responses)
89 .expect("MCP input responses are JSON serializable");
90 Err(VmError::McpInputRequired(Box::new(McpInputRequired {
91 key,
92 request: serde_json::json!({"method": method, "params": params}),
93 request_state,
94 })))
95 })
96 .unwrap_or_else(|_| {
97 Err(VmError::Thrown(VmValue::String(arcstr::ArcStr::from(
98 format!(
99 "{error_prefix}: no active MCP request; this builtin is only valid inside a served MCP handler"
100 ),
101 ))))
102 })
103}
104
105fn supports_input(capabilities: &JsonValue, method: &str, params: &JsonValue) -> bool {
106 match method {
107 "elicitation/create" => {
108 let Some(elicitation) = capabilities
109 .get("elicitation")
110 .and_then(JsonValue::as_object)
111 else {
112 return false;
113 };
114 match params
115 .get("mode")
116 .and_then(JsonValue::as_str)
117 .unwrap_or("form")
118 {
119 "form" => elicitation.contains_key("form"),
120 "url" => elicitation.contains_key("url"),
121 _ => false,
122 }
123 }
124 "roots/list" => capabilities.get("roots").is_some(),
125 "sampling/createMessage" => capabilities.get("sampling").is_some(),
126 _ => false,
127 }
128}
129
130pub(crate) fn input_result(error: McpInputRequired) -> JsonValue {
131 serde_json::json!({
132 "resultType": "input_required",
133 "inputRequests": {error.key: error.request},
134 "requestState": error.request_state,
135 })
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[tokio::test]
143 async fn preserves_responses_across_reentered_rounds() {
144 let params = serde_json::json!({
145 "requestState": "{\"harn-input-0\":{\"roots\":[]}}",
146 "inputResponses": {"harn-input-1": {"action": "decline"}},
147 });
148 let result = scope_input_context(
149 ¶ms,
150 serde_json::json!({"roots": {}, "elicitation": {"form": {}}}),
151 async {
152 let roots = request_input("roots/list", serde_json::json!({}), "roots").unwrap();
153 let elicitation = request_input(
154 "elicitation/create",
155 serde_json::json!({"mode": "form"}),
156 "elicit",
157 )
158 .unwrap();
159 (roots, elicitation)
160 },
161 )
162 .await
163 .unwrap();
164 assert_eq!(result.0, serde_json::json!({"roots": []}));
165 assert_eq!(result.1, serde_json::json!({"action": "decline"}));
166 }
167
168 #[tokio::test]
169 async fn suspends_with_stable_input_required_payload() {
170 let error = scope_input_context(
171 &serde_json::json!({}),
172 serde_json::json!({"elicitation": {"form": {}}}),
173 async {
174 request_input(
175 "elicitation/create",
176 serde_json::json!({"mode": "form", "message": "Continue?"}),
177 "elicit",
178 )
179 },
180 )
181 .await
182 .unwrap()
183 .unwrap_err();
184 let VmError::McpInputRequired(error) = error else {
185 panic!("expected input-required control flow")
186 };
187 let result = input_result(*error);
188 assert_eq!(result["resultType"], "input_required");
189 assert_eq!(
190 result["inputRequests"]["harn-input-0"]["method"],
191 "elicitation/create"
192 );
193 }
194
195 #[test]
196 fn requires_the_declared_elicitation_mode() {
197 let form = serde_json::json!({"mode": "form"});
198 let url = serde_json::json!({"mode": "url"});
199
200 assert!(!supports_input(
201 &serde_json::json!({"elicitation": {}}),
202 "elicitation/create",
203 &form,
204 ));
205 assert!(supports_input(
206 &serde_json::json!({"elicitation": {"form": {}}}),
207 "elicitation/create",
208 &form,
209 ));
210 assert!(!supports_input(
211 &serde_json::json!({"elicitation": {"form": {}}}),
212 "elicitation/create",
213 &url,
214 ));
215 assert!(supports_input(
216 &serde_json::json!({"elicitation": {"url": {}}}),
217 "elicitation/create",
218 &url,
219 ));
220 }
221}