Skip to main content

rustlavel_mcp/
rpc.rs

1//! JSON-RPC 2.0, written on `rustlavel_core::Json`.
2//!
3//! MCP speaks JSON-RPC and nothing else, so this layer knows nothing about
4//! tools or resources: it only understands requests, notifications, responses,
5//! error objects, and batches. Keeping it separate is what lets the protocol
6//! tests below be written against hand-typed JSON rather than against the
7//! server's behaviour.
8
9use rustlavel_core::Json;
10use std::fmt;
11
12/// The error codes JSON-RPC reserves, plus the one MCP adds.
13pub mod codes {
14    /// The payload was not valid JSON at all.
15    pub const PARSE_ERROR: i64 = -32700;
16    /// Valid JSON, but not a valid JSON-RPC message.
17    pub const INVALID_REQUEST: i64 = -32600;
18    pub const METHOD_NOT_FOUND: i64 = -32601;
19    pub const INVALID_PARAMS: i64 = -32602;
20    pub const INTERNAL_ERROR: i64 = -32603;
21    /// MCP's own reserved code for a resource URI the server does not serve.
22    pub const RESOURCE_NOT_FOUND: i64 = -32002;
23}
24
25/// A request id. JSON-RPC allows a number or a string, and a client is free to
26/// use either, so correlation has to handle both.
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub enum Id {
29    Number(i64),
30    Text(String),
31}
32
33impl Id {
34    pub fn to_json(&self) -> Json {
35        match self {
36            Id::Number(n) => Json::from(*n),
37            Id::Text(s) => Json::from(s.clone()),
38        }
39    }
40
41    /// Read an id, rejecting the shapes JSON-RPC does not allow (objects,
42    /// arrays, booleans) rather than coercing them.
43    pub fn from_json(value: &Json) -> Option<Id> {
44        match value {
45            Json::Number(n) => Some(Id::Number(*n as i64)),
46            Json::String(s) => Some(Id::Text(s.clone())),
47            _ => None,
48        }
49    }
50}
51
52impl From<i64> for Id {
53    fn from(value: i64) -> Self {
54        Id::Number(value)
55    }
56}
57
58impl From<&str> for Id {
59    fn from(value: &str) -> Self {
60        Id::Text(value.to_string())
61    }
62}
63
64impl From<String> for Id {
65    fn from(value: String) -> Self {
66        Id::Text(value)
67    }
68}
69
70impl fmt::Display for Id {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            Id::Number(n) => write!(f, "{n}"),
74            Id::Text(s) => f.write_str(s),
75        }
76    }
77}
78
79/// A JSON-RPC error object.
80#[derive(Debug, Clone, PartialEq)]
81pub struct RpcError {
82    pub code: i64,
83    pub message: String,
84    /// Extra detail — the list of validation failures, the offending method.
85    pub data: Option<Json>,
86}
87
88impl RpcError {
89    pub fn new(code: i64, message: impl Into<String>) -> Self {
90        RpcError { code, message: message.into(), data: None }
91    }
92
93    pub fn with_data(mut self, data: Json) -> Self {
94        self.data = Some(data);
95        self
96    }
97
98    pub fn parse_error(detail: impl fmt::Display) -> Self {
99        RpcError::new(codes::PARSE_ERROR, format!("Parse error: {detail}"))
100    }
101
102    pub fn invalid_request(detail: impl fmt::Display) -> Self {
103        RpcError::new(codes::INVALID_REQUEST, format!("Invalid Request: {detail}"))
104    }
105
106    pub fn method_not_found(method: &str) -> Self {
107        RpcError::new(codes::METHOD_NOT_FOUND, format!("Method not found: {method}"))
108    }
109
110    pub fn invalid_params(detail: impl fmt::Display) -> Self {
111        RpcError::new(codes::INVALID_PARAMS, format!("Invalid params: {detail}"))
112    }
113
114    pub fn internal_error(detail: impl fmt::Display) -> Self {
115        RpcError::new(codes::INTERNAL_ERROR, format!("Internal error: {detail}"))
116    }
117
118    pub fn resource_not_found(uri: &str) -> Self {
119        RpcError::new(codes::RESOURCE_NOT_FOUND, format!("Resource not found: {uri}"))
120            .with_data(Json::object([("uri", Json::from(uri))]))
121    }
122
123    pub fn to_json(&self) -> Json {
124        let mut pairs = vec![
125            ("code", Json::from(self.code)),
126            ("message", Json::from(self.message.clone())),
127        ];
128        if let Some(data) = &self.data {
129            pairs.push(("data", data.clone()));
130        }
131        Json::object(pairs)
132    }
133
134    pub fn from_json(value: &Json) -> Option<RpcError> {
135        let code = value.get("code")?.as_i64()?;
136        let message = value.get("message")?.as_str()?.to_string();
137        Some(RpcError { code, message, data: value.get("data").cloned() })
138    }
139}
140
141impl fmt::Display for RpcError {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        write!(f, "{} ({})", self.message, self.code)
144    }
145}
146
147/// A call that expects an answer.
148#[derive(Debug, Clone, PartialEq)]
149pub struct Request {
150    pub id: Id,
151    pub method: String,
152    /// `Json::Null` when the caller sent no params, so callers never unwrap.
153    pub params: Json,
154}
155
156impl Request {
157    pub fn new(id: impl Into<Id>, method: impl Into<String>, params: Json) -> Self {
158        Request { id: id.into(), method: method.into(), params }
159    }
160
161    pub fn to_json(&self) -> Json {
162        let mut pairs = vec![
163            ("jsonrpc", Json::from(VERSION)),
164            ("id", self.id.to_json()),
165            ("method", Json::from(self.method.clone())),
166        ];
167        if !self.params.is_null() {
168            pairs.push(("params", self.params.clone()));
169        }
170        Json::object(pairs)
171    }
172}
173
174/// A call that expects no answer.
175#[derive(Debug, Clone, PartialEq)]
176pub struct Notification {
177    pub method: String,
178    pub params: Json,
179}
180
181impl Notification {
182    pub fn new(method: impl Into<String>, params: Json) -> Self {
183        Notification { method: method.into(), params }
184    }
185
186    pub fn to_json(&self) -> Json {
187        let mut pairs =
188            vec![("jsonrpc", Json::from(VERSION)), ("method", Json::from(self.method.clone()))];
189        if !self.params.is_null() {
190            pairs.push(("params", self.params.clone()));
191        }
192        Json::object(pairs)
193    }
194}
195
196/// An answer: exactly one of `result` or `error`, never both.
197///
198/// The id is optional because an error raised before the id could be read —
199/// a malformed frame — must still be reported, with a null id.
200#[derive(Debug, Clone, PartialEq)]
201pub struct Response {
202    pub id: Option<Id>,
203    pub result: Result<Json, RpcError>,
204}
205
206impl Response {
207    pub fn success(id: Id, result: Json) -> Self {
208        Response { id: Some(id), result: Ok(result) }
209    }
210
211    pub fn failure(id: Option<Id>, error: RpcError) -> Self {
212        Response { id, result: Err(error) }
213    }
214
215    pub fn to_json(&self) -> Json {
216        let id = self.id.as_ref().map_or(Json::Null, Id::to_json);
217        match &self.result {
218            Ok(value) => Json::object([
219                ("jsonrpc", Json::from(VERSION)),
220                ("id", id),
221                ("result", value.clone()),
222            ]),
223            Err(error) => Json::object([
224                ("jsonrpc", Json::from(VERSION)),
225                ("id", id),
226                ("error", error.to_json()),
227            ]),
228        }
229    }
230}
231
232/// The protocol version string that must appear on every message.
233pub const VERSION: &str = "2.0";
234
235/// Any one JSON-RPC message.
236#[derive(Debug, Clone, PartialEq)]
237pub enum Message {
238    Request(Request),
239    Notification(Notification),
240    Response(Response),
241}
242
243impl Message {
244    /// Decode one message object.
245    ///
246    /// The shape decides the variant: `method` with an `id` is a request,
247    /// `method` without one is a notification, and `result`/`error` is an
248    /// answer to something we sent.
249    pub fn from_json(value: &Json) -> Result<Message, RpcError> {
250        let Some(object) = value.as_object() else {
251            return Err(RpcError::invalid_request("a message must be an object"));
252        };
253        match object.get("jsonrpc").and_then(Json::as_str) {
254            Some(VERSION) => {}
255            Some(other) => {
256                return Err(RpcError::invalid_request(format!("unsupported version `{other}`")));
257            }
258            None => return Err(RpcError::invalid_request("missing the `jsonrpc` field")),
259        }
260
261        if let Some(method) = object.get("method") {
262            let Some(method) = method.as_str() else {
263                return Err(RpcError::invalid_request("`method` must be a string"));
264            };
265            let params = object.get("params").cloned().unwrap_or(Json::Null);
266            return match object.get("id") {
267                None => Ok(Message::Notification(Notification::new(method, params))),
268                Some(raw) => match Id::from_json(raw) {
269                    Some(id) => Ok(Message::Request(Request::new(id, method, params))),
270                    None => Err(RpcError::invalid_request("`id` must be a string or a number")),
271                },
272            };
273        }
274
275        let id = object.get("id").and_then(Id::from_json);
276        if let Some(error) = object.get("error") {
277            let error = RpcError::from_json(error)
278                .ok_or_else(|| RpcError::invalid_request("malformed error object"))?;
279            return Ok(Message::Response(Response::failure(id, error)));
280        }
281        if let Some(result) = object.get("result") {
282            let id = id.ok_or_else(|| RpcError::invalid_request("a result needs an id"))?;
283            return Ok(Message::Response(Response::success(id, result.clone())));
284        }
285
286        Err(RpcError::invalid_request("neither a call nor an answer"))
287    }
288
289    pub fn to_json(&self) -> Json {
290        match self {
291            Message::Request(request) => request.to_json(),
292            Message::Notification(notification) => notification.to_json(),
293            Message::Response(response) => response.to_json(),
294        }
295    }
296
297    /// Decode a single message straight from text.
298    pub fn parse(text: &str) -> Result<Message, RpcError> {
299        let value = Json::parse(text).map_err(RpcError::parse_error)?;
300        Message::from_json(&value)
301    }
302}
303
304impl fmt::Display for Message {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        write!(f, "{}", self.to_json())
307    }
308}
309
310/// One frame off a transport: a lone message or a batch of them.
311///
312/// The frame is kept as raw `Json` rather than decoded messages because a
313/// single bad element of a batch must produce its own error response while its
314/// well-formed neighbours are still answered.
315#[derive(Debug, Clone, PartialEq)]
316pub enum Frame {
317    Single(Json),
318    Batch(Vec<Json>),
319}
320
321impl Frame {
322    pub fn parse(text: &str) -> Result<Frame, RpcError> {
323        let value = Json::parse(text).map_err(RpcError::parse_error)?;
324        match value {
325            Json::Array(items) if items.is_empty() => {
326                Err(RpcError::invalid_request("an empty batch has nothing to answer"))
327            }
328            Json::Array(items) => Ok(Frame::Batch(items)),
329            single => Ok(Frame::Single(single)),
330        }
331    }
332
333    /// Every element, so a caller can decode them one at a time.
334    pub fn items(&self) -> &[Json] {
335        match self {
336            Frame::Single(value) => std::slice::from_ref(value),
337            Frame::Batch(items) => items,
338        }
339    }
340
341    pub fn is_batch(&self) -> bool {
342        matches!(self, Frame::Batch(_))
343    }
344}
345
346/// Serialize answers back onto a transport.
347///
348/// `None` means "send nothing": a frame made only of notifications has no
349/// reply, which JSON-RPC is explicit about.
350pub fn encode(responses: Vec<Json>, batched: bool) -> Option<String> {
351    match (responses.is_empty(), batched) {
352        (true, _) => None,
353        (false, true) => Some(Json::Array(responses).to_string()),
354        (false, false) => responses.into_iter().next().map(|value| value.to_string()),
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn a_request_encodes_to_the_wire_shape() {
364        let request = Request::new(
365            1i64,
366            "tools/call",
367            Json::object([("name", Json::from("weather"))]),
368        );
369
370        assert_eq!(
371            request.to_json().to_string(),
372            r#"{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"name":"weather"}}"#
373        );
374    }
375
376    #[test]
377    fn a_request_without_params_omits_the_field() {
378        let request = Request::new("abc", "tools/list", Json::Null);
379        assert_eq!(request.to_json().to_string(), r#"{"id":"abc","jsonrpc":"2.0","method":"tools/list"}"#);
380    }
381
382    #[test]
383    fn decodes_a_hand_written_request() {
384        let message = Message::parse(r#"{"jsonrpc":"2.0","id":7,"method":"ping","params":{}}"#).unwrap();
385
386        match message {
387            Message::Request(request) => {
388                assert_eq!(request.id, Id::Number(7));
389                assert_eq!(request.method, "ping");
390                assert!(request.params.as_object().unwrap().is_empty());
391            }
392            other => panic!("expected a request, got {other:?}"),
393        }
394    }
395
396    #[test]
397    fn a_message_without_an_id_decodes_as_a_notification() {
398        let message = Message::parse(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#).unwrap();
399
400        assert_eq!(
401            message,
402            Message::Notification(Notification::new("notifications/initialized", Json::Null))
403        );
404    }
405
406    #[test]
407    fn decodes_both_kinds_of_answer() {
408        let ok = Message::parse(r#"{"jsonrpc":"2.0","id":"a","result":{"ok":true}}"#).unwrap();
409        let bad =
410            Message::parse(r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"nope"}}"#)
411                .unwrap();
412
413        match ok {
414            Message::Response(response) => {
415                assert_eq!(response.id, Some(Id::Text("a".into())));
416                assert_eq!(response.result.unwrap().get("ok").unwrap().as_bool(), Some(true));
417            }
418            other => panic!("expected a response, got {other:?}"),
419        }
420        match bad {
421            Message::Response(response) => {
422                assert_eq!(response.id, None);
423                assert_eq!(response.result.unwrap_err().code, codes::PARSE_ERROR);
424            }
425            other => panic!("expected a response, got {other:?}"),
426        }
427    }
428
429    #[test]
430    fn string_and_number_ids_both_survive_a_round_trip() {
431        for id in [Id::Number(-3), Id::Text("req-9".into())] {
432            let encoded = Request::new(id.clone(), "ping", Json::Null).to_json();
433            let Message::Request(decoded) = Message::from_json(&encoded).unwrap() else {
434                panic!("not a request");
435            };
436            assert_eq!(decoded.id, id);
437        }
438    }
439
440    #[test]
441    fn malformed_json_is_a_parse_error() {
442        let error = Message::parse("{ not json").unwrap_err();
443        assert_eq!(error.code, codes::PARSE_ERROR);
444    }
445
446    #[test]
447    fn a_missing_version_field_is_an_invalid_request() {
448        let error = Message::parse(r#"{"id":1,"method":"ping"}"#).unwrap_err();
449        assert_eq!(error.code, codes::INVALID_REQUEST);
450
451        let wrong = Message::parse(r#"{"jsonrpc":"1.0","id":1,"method":"ping"}"#).unwrap_err();
452        assert_eq!(wrong.code, codes::INVALID_REQUEST);
453    }
454
455    #[test]
456    fn a_non_object_message_is_an_invalid_request() {
457        assert_eq!(Message::parse("42").unwrap_err().code, codes::INVALID_REQUEST);
458        assert_eq!(
459            Message::parse(r#"{"jsonrpc":"2.0","id":1}"#).unwrap_err().code,
460            codes::INVALID_REQUEST
461        );
462    }
463
464    #[test]
465    fn an_id_that_is_an_object_is_rejected() {
466        let error = Message::parse(r#"{"jsonrpc":"2.0","id":{"a":1},"method":"ping"}"#).unwrap_err();
467        assert_eq!(error.code, codes::INVALID_REQUEST);
468    }
469
470    #[test]
471    fn a_batch_frame_keeps_every_element() {
472        let frame = Frame::parse(
473            r#"[{"jsonrpc":"2.0","id":1,"method":"ping"},{"jsonrpc":"2.0","method":"hi"},7]"#,
474        )
475        .unwrap();
476
477        assert!(frame.is_batch());
478        assert_eq!(frame.items().len(), 3);
479        // The bad element only fails when it is decoded, not when the frame is.
480        assert_eq!(
481            Message::from_json(&frame.items()[2]).unwrap_err().code,
482            codes::INVALID_REQUEST
483        );
484    }
485
486    #[test]
487    fn an_empty_batch_is_an_invalid_request() {
488        assert_eq!(Frame::parse("[]").unwrap_err().code, codes::INVALID_REQUEST);
489    }
490
491    #[test]
492    fn a_single_frame_still_exposes_one_item() {
493        let frame = Frame::parse(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#).unwrap();
494        assert!(!frame.is_batch());
495        assert_eq!(frame.items().len(), 1);
496    }
497
498    #[test]
499    fn encoding_answers_matches_the_frame_it_answers() {
500        let one = Response::success(Id::Number(1), Json::from(true)).to_json();
501
502        assert_eq!(
503            encode(vec![one.clone()], false).unwrap(),
504            r#"{"id":1,"jsonrpc":"2.0","result":true}"#
505        );
506        assert!(encode(vec![one], true).unwrap().starts_with('['));
507        // Notifications only: nothing to send back.
508        assert_eq!(encode(Vec::new(), true), None);
509        assert_eq!(encode(Vec::new(), false), None);
510    }
511
512    #[test]
513    fn error_objects_carry_their_data_through_a_round_trip() {
514        let error = RpcError::invalid_params("city must be a string")
515            .with_data(Json::object([("field", Json::from("city"))]));
516        let decoded = RpcError::from_json(&error.to_json()).unwrap();
517
518        assert_eq!(decoded, error);
519        assert_eq!(decoded.code, codes::INVALID_PARAMS);
520        assert_eq!(decoded.data.unwrap().get("field").unwrap().as_str(), Some("city"));
521    }
522
523    #[test]
524    fn the_reserved_codes_are_the_ones_the_specification_names() {
525        assert_eq!(RpcError::parse_error("x").code, -32700);
526        assert_eq!(RpcError::invalid_request("x").code, -32600);
527        assert_eq!(RpcError::method_not_found("x").code, -32601);
528        assert_eq!(RpcError::invalid_params("x").code, -32602);
529        assert_eq!(RpcError::internal_error("x").code, -32603);
530        assert_eq!(RpcError::resource_not_found("file:///x").code, -32002);
531    }
532}