Skip to main content

flymcprust_core/
jsonrpc.rs

1//! JSON-RPC 2.0 envelope parsing and response serialization.
2//!
3//! Parsing enforces the size and depth limits before `serde` sees the input,
4//! rejects batches (not part of MCP 2026-07-28), null ids and responses, and
5//! keeps `params` as raw JSON so each handler deserializes exactly once.
6
7use std::borrow::Cow;
8
9use serde::{Deserialize, Serialize};
10use serde_json::value::RawValue;
11
12use crate::error::McpError;
13use crate::limits::Limits;
14use crate::spec::{JsonRpcVersion, RequestId};
15
16/// An incoming JSON-RPC message accepted by the server.
17#[derive(Debug)]
18pub enum Message {
19    /// A request, which expects a response.
20    Request(Request),
21    /// A notification, which must not be answered.
22    Notification(Notification),
23}
24
25/// A JSON-RPC request.
26#[derive(Debug)]
27pub struct Request {
28    /// Request id (never null).
29    pub id: RequestId,
30    /// Method name.
31    pub method: String,
32    /// Raw `params`, if present and not null.
33    pub params: Option<Box<RawValue>>,
34}
35
36/// A JSON-RPC notification.
37#[derive(Debug)]
38pub struct Notification {
39    /// Method name.
40    pub method: String,
41    /// Raw `params`, if present and not null.
42    pub params: Option<Box<RawValue>>,
43}
44
45/// Why an incoming message was rejected.
46#[derive(Debug)]
47pub enum Rejection {
48    /// The message is invalid; answer with this error.
49    Error {
50        /// The request id, when it could be read.
51        id: Option<RequestId>,
52        /// The error to send.
53        error: McpError,
54    },
55    /// The message is a JSON-RPC response, which clients must not send.
56    /// Transports drop it (stdio) or answer with an HTTP error (HTTP).
57    UnexpectedResponse,
58}
59
60#[derive(Deserialize)]
61struct Envelope<'a> {
62    #[serde(borrow, default)]
63    jsonrpc: Option<Cow<'a, str>>,
64    // `deserialize_with` keeps an explicit `null` (rejected later) distinct
65    // from an absent id (a notification).
66    #[serde(borrow, default, deserialize_with = "present")]
67    id: Option<&'a RawValue>,
68    #[serde(borrow, default)]
69    method: Option<Cow<'a, str>>,
70    #[serde(borrow, default)]
71    params: Option<&'a RawValue>,
72    #[serde(borrow, default)]
73    result: Option<&'a RawValue>,
74    #[serde(borrow, default)]
75    error: Option<&'a RawValue>,
76}
77
78fn present<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<&'de RawValue>, D::Error> {
79    <&'de RawValue>::deserialize(d).map(Some)
80}
81
82/// Parses one JSON-RPC message, enforcing `limits`.
83///
84/// # Errors
85///
86/// Returns a [`Rejection`] describing the response to send, if any.
87pub fn parse(bytes: &[u8], limits: &Limits) -> Result<Message, Rejection> {
88    let reject = |error| Rejection::Error { id: None, error };
89
90    if bytes.len() > limits.max_message_bytes {
91        return Err(reject(McpError::invalid_request("Message too large")));
92    }
93    if exceeds_depth(bytes, limits.effective_json_depth()) {
94        return Err(reject(McpError::invalid_request(
95            "Message nested too deeply",
96        )));
97    }
98    if first_significant_byte(bytes) == Some(b'[') {
99        return Err(reject(McpError::invalid_request(
100            "Batch requests are not supported",
101        )));
102    }
103
104    let envelope: Envelope<'_> = serde_json::from_slice(bytes).map_err(|e| {
105        reject(if e.is_data() {
106            McpError::invalid_request("Invalid JSON-RPC message")
107        } else {
108            McpError::parse_error()
109        })
110    })?;
111
112    let id = match envelope.id {
113        None => None,
114        Some(raw) if raw.get() == "null" => {
115            return Err(reject(McpError::invalid_request(
116                "Request id must not be null",
117            )));
118        }
119        Some(raw) => Some(
120            serde_json::from_str::<RequestId>(raw.get())
121                .map_err(|_| reject(McpError::invalid_request("Invalid request id")))?,
122        ),
123    };
124    let reject_with_id = |error| Rejection::Error {
125        id: id.clone(),
126        error,
127    };
128
129    if envelope.jsonrpc.as_deref() != Some(JsonRpcVersion::VALUE) {
130        return Err(reject_with_id(McpError::invalid_request(
131            "Unsupported JSON-RPC version",
132        )));
133    }
134    if envelope.result.is_some() || envelope.error.is_some() {
135        return Err(Rejection::UnexpectedResponse);
136    }
137    let Some(method) = envelope.method else {
138        return Err(reject_with_id(McpError::invalid_request("Missing method")));
139    };
140    let method = method.into_owned();
141    let params = envelope.params.map(RawValue::to_owned);
142
143    Ok(match id {
144        Some(id) => Message::Request(Request { id, method, params }),
145        None => Message::Notification(Notification { method, params }),
146    })
147}
148
149/// A client's answer to a request the server sent (legacy protocol).
150#[cfg_attr(not(any(feature = "stdio", feature = "http")), allow(dead_code))]
151#[derive(Debug)]
152pub(crate) struct ClientResponse {
153    pub(crate) id: RequestId,
154    /// `Ok(result)` or `Err(error object)`.
155    pub(crate) outcome: Result<serde_json::Value, serde_json::Value>,
156}
157
158#[cfg_attr(not(any(feature = "stdio", feature = "http")), allow(dead_code))]
159/// Parses a message that [`parse`] reported as
160/// [`Rejection::UnexpectedResponse`] (size and depth were already checked).
161pub(crate) fn parse_client_response(bytes: &[u8]) -> Option<ClientResponse> {
162    #[derive(Deserialize)]
163    struct Wire {
164        id: RequestId,
165        #[serde(default)]
166        result: Option<serde_json::Value>,
167        #[serde(default)]
168        error: Option<serde_json::Value>,
169    }
170    let wire: Wire = serde_json::from_slice(bytes).ok()?;
171    let outcome = match (wire.result, wire.error) {
172        (_, Some(error)) => Err(error),
173        (Some(result), None) => Ok(result),
174        (None, None) => return None,
175    };
176    Some(ClientResponse {
177        id: wire.id,
178        outcome,
179    })
180}
181
182/// A JSON-RPC response produced by the server.
183#[derive(Debug, Clone)]
184pub enum Response {
185    /// A successful result, already serialized.
186    Result {
187        /// The id of the request being answered.
188        id: RequestId,
189        /// The serialized `result` object.
190        result: Box<RawValue>,
191    },
192    /// An error.
193    Error {
194        /// The id of the request being answered, when known.
195        id: Option<RequestId>,
196        /// The error.
197        error: McpError,
198    },
199}
200
201#[derive(Serialize)]
202struct ResultWire<'a> {
203    jsonrpc: JsonRpcVersion,
204    id: &'a RequestId,
205    result: &'a RawValue,
206}
207
208#[derive(Serialize)]
209struct ErrorWire<'a> {
210    jsonrpc: JsonRpcVersion,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    id: Option<&'a RequestId>,
213    error: ErrorObjectWire<'a>,
214}
215
216#[derive(Serialize)]
217struct ErrorObjectWire<'a> {
218    code: i64,
219    message: &'a str,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    data: Option<&'a serde_json::Value>,
222}
223
224impl Response {
225    /// Builds an error response.
226    #[must_use]
227    pub fn error(id: Option<RequestId>, error: McpError) -> Self {
228        Self::Error { id, error }
229    }
230
231    /// The JSON-RPC error, if this is an error response.
232    #[must_use]
233    pub fn as_error(&self) -> Option<&McpError> {
234        match self {
235            Self::Error { error, .. } => Some(error),
236            Self::Result { .. } => None,
237        }
238    }
239
240    /// Serializes the response as a single line of JSON (no trailing newline).
241    #[must_use]
242    pub fn to_vec(&self) -> Vec<u8> {
243        let serialized = match self {
244            Self::Result { id, result } => serde_json::to_vec(&ResultWire {
245                jsonrpc: JsonRpcVersion::default(),
246                id,
247                result,
248            }),
249            Self::Error { id, error } => serde_json::to_vec(&ErrorWire {
250                jsonrpc: JsonRpcVersion::default(),
251                id: id.as_ref(),
252                error: ErrorObjectWire {
253                    code: error.code(),
254                    message: error.message(),
255                    data: error.data(),
256                },
257            }),
258        };
259        // Serializing these plain structs cannot fail; fall back to a fixed,
260        // valid error object just in case.
261        serialized.unwrap_or_else(|_| {
262            br#"{"jsonrpc":"2.0","error":{"code":-32603,"message":"Internal error"}}"#.to_vec()
263        })
264    }
265}
266
267/// Returns `true` when `bytes` nests objects/arrays deeper than `max`.
268///
269/// A single pass that skips string contents, so brackets inside strings are
270/// ignored. Strings are skipped with `memchr` (vectorized), which keeps the
271/// scan cheap for large payloads. Runs before `serde` to bound recursion.
272pub(crate) fn exceeds_depth(bytes: &[u8], max: usize) -> bool {
273    let mut depth = 0usize;
274    let mut i = 0;
275    while let Some(&b) = bytes.get(i) {
276        i += 1;
277        match b {
278            b'"' => {
279                // Jump to the closing quote, stepping over escapes.
280                loop {
281                    let rest = bytes.get(i..).unwrap_or_default();
282                    let Some(offset) = memchr::memchr2(b'"', b'\\', rest) else {
283                        // Unterminated string: the parser rejects it.
284                        return false;
285                    };
286                    let found = rest.get(offset).copied();
287                    i += offset + 1;
288                    if found == Some(b'\\') {
289                        // Skip the escaped character.
290                        i += 1;
291                    } else {
292                        break;
293                    }
294                }
295            }
296            b'{' | b'[' => {
297                depth += 1;
298                if depth > max {
299                    return true;
300                }
301            }
302            b'}' | b']' => depth = depth.saturating_sub(1),
303            _ => {}
304        }
305    }
306    false
307}
308
309fn first_significant_byte(bytes: &[u8]) -> Option<u8> {
310    bytes.iter().copied().find(|b| !b.is_ascii_whitespace())
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    fn limits() -> Limits {
318        Limits::default()
319    }
320
321    fn rejection_code(r: Result<Message, Rejection>) -> i64 {
322        match r {
323            Err(Rejection::Error { error, .. }) => error.code(),
324            other => panic!("expected an error rejection, got {other:?}"),
325        }
326    }
327
328    #[test]
329    fn parses_request_and_notification() {
330        let req = parse(
331            br#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"a":1}}"#,
332            &limits(),
333        );
334        match req {
335            Ok(Message::Request(r)) => {
336                assert_eq!(r.id, RequestId::Integer(1));
337                assert_eq!(r.method, "tools/list");
338                assert_eq!(
339                    r.params.map(|p| p.get().to_owned()).as_deref(),
340                    Some(r#"{"a":1}"#)
341                );
342            }
343            other => panic!("unexpected {other:?}"),
344        }
345        let note = parse(
346            br#"{"jsonrpc":"2.0","method":"notifications/cancelled"}"#,
347            &limits(),
348        );
349        assert!(matches!(note, Ok(Message::Notification(_))));
350    }
351
352    #[test]
353    fn rejects_invalid_messages() {
354        assert_eq!(rejection_code(parse(b"{not json", &limits())), -32700);
355        assert_eq!(rejection_code(parse(b"[]", &limits())), -32600);
356        assert_eq!(
357            rejection_code(parse(
358                br#"{"jsonrpc":"2.0","id":null,"method":"x"}"#,
359                &limits()
360            )),
361            -32600
362        );
363        assert_eq!(
364            rejection_code(parse(
365                br#"{"jsonrpc":"1.0","id":1,"method":"x"}"#,
366                &limits()
367            )),
368            -32600
369        );
370        assert_eq!(
371            rejection_code(parse(br#"{"jsonrpc":"2.0","id":1}"#, &limits())),
372            -32600
373        );
374        assert_eq!(
375            rejection_code(parse(
376                br#"{"jsonrpc":"2.0","id":1.5,"method":"x"}"#,
377                &limits()
378            )),
379            -32600
380        );
381        assert!(matches!(
382            parse(br#"{"jsonrpc":"2.0","id":1,"result":{}}"#, &limits()),
383            Err(Rejection::UnexpectedResponse)
384        ));
385    }
386
387    #[test]
388    fn enforces_size_and_depth() {
389        let small = Limits {
390            max_message_bytes: 10,
391            ..Limits::default()
392        };
393        assert_eq!(
394            rejection_code(parse(br#"{"jsonrpc":"2.0","method":"x"}"#, &small)),
395            -32600
396        );
397        let deep = format!(
398            r#"{{"jsonrpc":"2.0","method":"x","params":{}{}}}"#,
399            "[".repeat(100),
400            "]".repeat(100)
401        );
402        assert_eq!(rejection_code(parse(deep.as_bytes(), &limits())), -32600);
403    }
404
405    #[test]
406    fn depth_scan_ignores_strings() {
407        assert!(!exceeds_depth(br#"{"a":"[[[[[[[[[[\"[[["}"#, 2));
408        assert!(exceeds_depth(br#"{"a":[[1]]}"#, 2));
409        assert!(!exceeds_depth(br#"{"a":[1]}"#, 2));
410        // An escaped backslash right before the closing quote ends the string.
411        assert!(exceeds_depth(br#"{"a":"x\\","b":[[1]]}"#, 2));
412        // An escaped quote does not end it.
413        assert!(!exceeds_depth(br#"{"a":"\"[[[[","b":1}"#, 2));
414        // Unterminated strings are left to the parser.
415        assert!(!exceeds_depth(br#"{"a":"[[[["#, 2));
416    }
417
418    #[test]
419    fn serializes_responses() {
420        let ok = Response::Result {
421            id: RequestId::String("a".into()),
422            result: RawValue::from_string(r#"{"resultType":"complete"}"#.into()).unwrap(),
423        };
424        assert_eq!(
425            String::from_utf8(ok.to_vec()).unwrap(),
426            r#"{"jsonrpc":"2.0","id":"a","result":{"resultType":"complete"}}"#
427        );
428        let err = Response::error(None, McpError::parse_error());
429        assert_eq!(
430            String::from_utf8(err.to_vec()).unwrap(),
431            r#"{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"}}"#
432        );
433    }
434}