1use crate::{json, ActionError, BatchCall, Dispatcher, ErrorCode, Host, Invocation, Value};
5use alloc::{string::String, vec::Vec};
6
7pub const JSON_VERSION: &str = "1";
9pub const MCP_VERSION: &str = "2026-07-28";
11pub const MCP_LEGACY_VERSION: &str = "2025-11-25";
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Protocol {
17 Json,
19 Mcp,
21}
22
23pub fn requested_protocol(line: &[u8]) -> Option<Protocol> {
27 let request = json::parse(line).ok()?;
28 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0")
29 || request.get("method").and_then(Value::as_str).is_none()
30 || request.get("result").is_some()
31 || request.get("error").is_some()
32 || request
33 .get("params")
34 .is_some_and(|params| params.as_object().is_none())
35 || request.get("id").is_some_and(|id| {
36 !matches!(id, Value::Null | Value::String(_))
37 && id.as_u64().is_none()
38 && id.as_i64().is_none()
39 })
40 {
41 return None;
42 }
43 let is_mcp = request.get("params").is_some_and(|params| {
44 params
45 .get("_meta")
46 .and_then(|meta| meta.get("io.modelcontextprotocol/protocolVersion"))
47 .is_some()
48 || request.get("method").and_then(Value::as_str) == Some("initialize")
49 && params.get("protocolVersion").is_some()
50 });
51 Some(if is_mcp {
52 Protocol::Mcp
53 } else {
54 Protocol::Json
55 })
56}
57
58pub struct RpcSession {
60 pub(crate) protocol: Protocol,
61 pub(crate) server_name: String,
62 pub(crate) server_version: String,
63 pub(crate) legacy_initialized: bool,
64 pub(crate) legacy_ready: bool,
65}
66
67impl RpcSession {
68 pub fn requires_persistent_session(&self) -> bool {
71 self.legacy_initialized
72 }
73 pub fn new(protocol: Protocol, server_name: &str, server_version: &str) -> Self {
75 Self {
76 protocol,
77 server_name: server_name.into(),
78 server_version: server_version.into(),
79 legacy_initialized: false,
80 legacy_ready: false,
81 }
82 }
83 pub fn handle(
86 &mut self,
87 dispatcher: &Dispatcher,
88 host: &mut impl Host,
89 line: &[u8],
90 ) -> Option<String> {
91 let request = match json::parse(line) {
92 Ok(value) => value,
93 Err(error) => {
94 return Some(encode(error_response(
95 Value::Null,
96 RpcError {
97 code: -32700,
98 message: "Invalid JSON".into(),
99 data: Value::object([
100 ("offset", Value::u64(error.offset as u64)),
101 ("reason", Value::string(error.kind)),
102 ]),
103 },
104 )))
105 }
106 };
107 let id = request.get("id").cloned();
108 let valid_id = id.as_ref().is_none_or(|id| {
109 matches!(id, Value::String(_))
110 || id.as_i64().is_some()
111 || id.as_u64().is_some()
112 || self.protocol == Protocol::Json && matches!(id, Value::Null)
113 });
114 if request.as_object().is_none()
115 || request.get("jsonrpc").and_then(Value::as_str) != Some("2.0")
116 || request.get("method").and_then(Value::as_str).is_none()
117 || !valid_id
118 || request.get("result").is_some()
119 || request.get("error").is_some()
120 {
121 return Some(encode(error_response(
122 Value::Null,
123 RpcError::new(-32600, "Invalid JSON-RPC request"),
124 )));
125 }
126 let method = request.get("method").and_then(Value::as_str).unwrap_or("");
127 let empty = Value::object([]);
128 let params = request.get("params").unwrap_or(&empty);
129 if params.as_object().is_none() {
130 return id.map(|id| {
131 encode(error_response(
132 id,
133 RpcError::new(-32602, "Parameters must be an object"),
134 ))
135 });
136 }
137 if id.is_none() {
138 if dispatcher.enabled()
140 && self.protocol == Protocol::Mcp
141 && method == "notifications/initialized"
142 && self.legacy_initialized
143 {
144 self.legacy_ready = true;
145 }
146 return None;
149 }
150 let id = id.unwrap_or(Value::Null);
151 let result = dispatcher
152 .check_enabled()
153 .map_err(RpcError::action)
154 .and_then(|()| match self.protocol {
155 Protocol::Json => self.json_request(dispatcher, host, method, params),
156 Protocol::Mcp => self.mcp_request(dispatcher, host, method, params),
157 });
158 Some(encode(match result {
159 Ok(value) => Value::object([
160 ("jsonrpc", Value::string("2.0")),
161 ("id", id),
162 ("result", value),
163 ]),
164 Err(error) => error_response(id, error),
165 }))
166 }
167 fn json_request(
168 &self,
169 dispatcher: &Dispatcher,
170 host: &mut impl Host,
171 method: &str,
172 params: &Value,
173 ) -> Result<Value, RpcError> {
174 if params
175 .get("protocolVersion")
176 .is_some_and(|v| v.as_str() != Some(JSON_VERSION))
177 {
178 return Err(RpcError::new(
179 -32602,
180 "Unsupported control protocolVersion; expected 1",
181 ));
182 }
183 let mut clean = params.clone();
184 if let Value::Object(fields) = &mut clean {
185 fields.retain(|(key, _)| key != "protocolVersion");
186 }
187 let mut result = if method == "agent/info" {
188 allowed(&clean, &[]).map_err(RpcError::action)?;
189 Value::object([
190 ("protocolVersion", Value::string(JSON_VERSION)),
191 ("serverInfo", self.server_info()),
192 ("revision", Value::u64(host.revision())),
193 ("capabilities", Value::u64(host.capabilities())),
194 ("controlEnabled", Value::Bool(true)),
195 ])
196 } else {
197 if !matches!(
198 method,
199 "actions/list"
200 | "actions/describe"
201 | "actions/execute"
202 | "actions/batch"
203 | "state/inspect"
204 | "tasks/get"
205 | "tasks/cancel"
206 | "resources/read"
207 | "view/render"
208 ) {
209 return Err(RpcError::new(-32601, "Method not found"));
210 }
211 control(dispatcher, host, method, &clean).map_err(RpcError::action)?
212 };
213 result.insert("protocolVersion", Value::string(JSON_VERSION));
214 Ok(result)
215 }
216 pub(crate) fn server_info(&self) -> Value {
217 Value::object([
218 ("name", Value::string(&self.server_name)),
219 ("version", Value::string(&self.server_version)),
220 ])
221 }
222}
223
224pub fn control(
226 dispatcher: &Dispatcher,
227 host: &mut impl Host,
228 method: &str,
229 params: &Value,
230) -> Result<Value, ActionError> {
231 dispatcher.check_enabled()?;
232 if params.as_object().is_none() {
233 return Err(invalid("Parameters must be an object"));
234 }
235 match method {
236 "actions/list" => {
237 allowed(params, &["query", "cursor", "limit"])?;
238 let query = optional_str(params, "query")?.unwrap_or("");
239 let cursor = optional_str(params, "cursor")?;
240 let limit = optional_u64(params, "limit")?.unwrap_or(30);
241 if limit > 100 {
242 return Err(invalid("Discovery limit must be at most 100"));
243 }
244 Ok(dispatcher
245 .discover(host, query, cursor, limit as usize)?
246 .to_json())
247 }
248 "actions/describe" => {
249 allowed(params, &["action"])?;
250 dispatcher.describe(host, required_str(params, "action")?)
251 }
252 "actions/execute" => {
253 allowed(params, &["action", "input", "expectedRevision"])?;
254 let empty = Value::object([]);
255 let call = Invocation {
256 action: required_str(params, "action")?,
257 input: params.get("input").unwrap_or(&empty),
258 expected_revision: optional_u64(params, "expectedRevision")?,
259 };
260 Ok(dispatcher.execute(host, call)?.to_json())
261 }
262 "actions/batch" => {
263 allowed(params, &["calls", "atomic", "stopOnError"])?;
264 let Some(calls) = params.get("calls").and_then(Value::as_array) else {
265 return Err(invalid("calls must be an array"));
266 };
267 if calls.is_empty() || calls.len() > 128 {
268 return Err(invalid("A batch must contain 1 to 128 calls"));
269 }
270 let mut parsed = Vec::with_capacity(calls.len());
271 for call in calls {
272 allowed(call, &["action", "input", "expectedRevision"])?;
273 parsed.push(BatchCall {
274 action: required_str(call, "action")?.into(),
275 input: call
276 .get("input")
277 .cloned()
278 .unwrap_or_else(|| Value::object([])),
279 expected_revision: optional_u64(call, "expectedRevision")?,
280 });
281 }
282 Ok(dispatcher
283 .batch(
284 host,
285 &parsed,
286 optional_bool(params, "atomic")?.unwrap_or(false),
287 optional_bool(params, "stopOnError")?.unwrap_or(true),
288 )?
289 .to_json())
290 }
291 "state/inspect" => host.inspect(params),
292 "tasks/get" => {
293 allowed(params, &["id"])?;
294 host.task_status(required_u64(params, "id")?)
295 }
296 "tasks/cancel" => {
297 allowed(params, &["id"])?;
298 host.task_cancel(required_u64(params, "id")?)
299 }
300 "resources/read" => {
301 allowed(params, &["uri", "offset", "limit"])?;
302 let uri = required_str(params, "uri")?;
303 let offset = optional_u64(params, "offset")?.unwrap_or(0);
304 let limit = optional_u64(params, "limit")?.unwrap_or(32_768);
305 if limit == 0 || limit > 65_536 {
306 return Err(invalid("Resource limit must be 1 to 65536 bytes"));
307 }
308 let info = host.resource_info(uri)?;
309 if info.size.is_some_and(|size| offset > size) {
310 return Err(invalid("Resource offset exceeds its size"));
311 }
312 let bytes = host.resource_read(uri, offset, limit as usize)?;
313 if bytes.len() > limit as usize {
314 return Err(ActionError::new(
315 ErrorCode::Internal,
316 "Resource reader exceeded its limit",
317 ));
318 }
319 let next = offset
320 .checked_add(bytes.len() as u64)
321 .ok_or_else(|| invalid("Resource offset overflow"))?;
322 let eof = info.size.is_some_and(|size| next >= size) || bytes.len() < limit as usize;
323 Ok(Value::object([
324 ("resource", info.to_json()),
325 ("offset", Value::u64(offset)),
326 ("nextOffset", Value::u64(next)),
327 ("eof", Value::Bool(eof)),
328 ("encoding", Value::string("base64")),
329 ("data", Value::string(base64(&bytes))),
330 ]))
331 }
332 "view/render" => {
333 let call = Invocation {
334 action: "ui.render",
335 input: params,
336 expected_revision: None,
337 };
338 Ok(dispatcher.execute(host, call)?.to_json())
339 }
340 _ => Err(ActionError::new(
341 ErrorCode::NotFound,
342 "Control method not found",
343 )),
344 }
345}
346
347pub(crate) fn required_str<'a>(value: &'a Value, key: &str) -> Result<&'a str, ActionError> {
348 optional_str(value, key)?.ok_or_else(|| invalid("Required string parameter is missing"))
349}
350pub(crate) fn optional_str<'a>(
351 value: &'a Value,
352 key: &str,
353) -> Result<Option<&'a str>, ActionError> {
354 match value.get(key) {
355 None => Ok(None),
356 Some(Value::String(text)) if text.len() <= 4096 => Ok(Some(text)),
357 _ => Err(invalid("Expected a string of at most 4096 bytes")),
358 }
359}
360pub(crate) fn required_u64(value: &Value, key: &str) -> Result<u64, ActionError> {
361 optional_u64(value, key)?.ok_or_else(|| invalid("Required integer parameter is missing"))
362}
363pub(crate) fn optional_u64(value: &Value, key: &str) -> Result<Option<u64>, ActionError> {
364 match value.get(key) {
365 None => Ok(None),
366 Some(v) => v
367 .as_u64()
368 .map(Some)
369 .ok_or_else(|| invalid("Expected an unsigned integer")),
370 }
371}
372fn optional_bool(value: &Value, key: &str) -> Result<Option<bool>, ActionError> {
373 match value.get(key) {
374 None => Ok(None),
375 Some(Value::Bool(v)) => Ok(Some(*v)),
376 _ => Err(invalid("Expected a boolean")),
377 }
378}
379pub(crate) fn allowed(value: &Value, names: &[&str]) -> Result<(), ActionError> {
380 let Some(fields) = value.as_object() else {
381 return Err(invalid("Parameters must be an object"));
382 };
383 for (key, _) in fields {
384 if !names.contains(&key.as_str()) {
385 return Err(invalid("Unknown parameter")
386 .with_details(Value::object([("parameter", Value::string(key))])));
387 }
388 }
389 Ok(())
390}
391pub(crate) fn invalid(message: &str) -> ActionError {
392 ActionError::new(ErrorCode::InvalidInput, message)
393}
394
395pub(crate) struct RpcError {
396 pub code: i64,
397 pub message: String,
398 pub data: Value,
399}
400impl RpcError {
401 pub fn new(code: i64, message: &str) -> Self {
402 Self {
403 code,
404 message: message.into(),
405 data: Value::Null,
406 }
407 }
408 pub fn action(error: ActionError) -> Self {
409 Self {
410 code: if error.code == ErrorCode::InvalidInput {
411 -32602
412 } else {
413 -31000
414 },
415 message: error.message.clone(),
416 data: error.to_json(),
417 }
418 }
419}
420fn error_response(id: Value, error: RpcError) -> Value {
421 Value::object([
422 ("jsonrpc", Value::string("2.0")),
423 ("id", id),
424 (
425 "error",
426 Value::object([
427 ("code", Value::i64(error.code)),
428 ("message", Value::string(error.message)),
429 ("data", error.data),
430 ]),
431 ),
432 ])
433}
434fn encode(value: Value) -> String {
435 let mut result = match json::stringify(&value) {
436 Ok(text) => text,
437 Err(_) => {
438 let id = value.get("id").cloned().unwrap_or(Value::Null);
439 json::stringify(&error_response(id, RpcError::new(-32603, "Response exceeds encoding limits"))).unwrap_or_else(|_| String::from("{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32603,\"message\":\"Encoding failed\"}}"))
440 }
441 };
442 result.push('\n');
443 result
444}
445
446pub fn base64(bytes: &[u8]) -> String {
448 const DIGITS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
449 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
450 for part in bytes.chunks(3) {
451 let a = part[0];
452 let b = part.get(1).copied().unwrap_or(0);
453 let c = part.get(2).copied().unwrap_or(0);
454 out.push(DIGITS[(a >> 2) as usize] as char);
455 out.push(DIGITS[(((a & 3) << 4) | (b >> 4)) as usize] as char);
456 out.push(if part.len() > 1 {
457 DIGITS[(((b & 15) << 2) | (c >> 6)) as usize] as char
458 } else {
459 '='
460 });
461 out.push(if part.len() > 2 {
462 DIGITS[(c & 63) as usize] as char
463 } else {
464 '='
465 });
466 }
467 out
468}