Skip to main content

fez/mcp/
jsonrpc.rs

1//! Minimal JSON-RPC 2.0 types for the MCP stdio transport. A request without an
2//! `id` is a notification and receives no response (JSON-RPC 2.0 ยง4.1).
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// An incoming JSON-RPC message. `id` absent => notification.
7#[derive(Debug, Deserialize)]
8pub struct Request {
9    /// Request id; absent for notifications.
10    #[serde(default)]
11    pub id: Option<Value>,
12    /// JSON-RPC method name.
13    pub method: String,
14    /// Method parameters; defaults to JSON null when omitted.
15    #[serde(default)]
16    pub params: Value,
17}
18
19impl Request {
20    /// True when this message is a notification (has no `id`).
21    pub fn is_notification(&self) -> bool {
22        self.id.is_none()
23    }
24}
25
26/// An outgoing JSON-RPC response. Exactly one of `result`/`error` is set.
27#[derive(Debug, Serialize)]
28pub struct Response {
29    /// Protocol marker, always `"2.0"`.
30    pub jsonrpc: &'static str,
31    /// Id echoed from the originating request.
32    pub id: Value,
33    /// Success result, mutually exclusive with `error`.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub result: Option<Value>,
36    /// Error detail, mutually exclusive with `result`.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub error: Option<RpcError>,
39}
40
41/// A JSON-RPC error object.
42#[derive(Debug, Serialize)]
43pub struct RpcError {
44    /// Numeric JSON-RPC error code.
45    pub code: i64,
46    /// Human-readable error message.
47    pub message: String,
48}
49
50impl Response {
51    /// Build a success response carrying `result`.
52    pub fn ok(id: Value, result: Value) -> Self {
53        Response {
54            jsonrpc: "2.0",
55            id,
56            result: Some(result),
57            error: None,
58        }
59    }
60    /// Build an error response with the given code and message.
61    pub fn err(id: Value, code: i64, message: &str) -> Self {
62        Response {
63            jsonrpc: "2.0",
64            id,
65            result: None,
66            error: Some(RpcError {
67                code,
68                message: message.into(),
69            }),
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use serde_json::json;
78
79    #[test]
80    fn parses_request_with_id() {
81        let r: Request = serde_json::from_value(json!({
82            "jsonrpc":"2.0","id":1,"method":"tools/list","params":{}
83        }))
84        .unwrap();
85        assert_eq!(r.method, "tools/list");
86        assert!(!r.is_notification());
87    }
88
89    #[test]
90    fn parses_notification_without_id() {
91        let r: Request = serde_json::from_value(json!({
92            "jsonrpc":"2.0","method":"notifications/initialized"
93        }))
94        .unwrap();
95        assert!(r.is_notification());
96    }
97
98    #[test]
99    fn ok_response_omits_error() {
100        let v = serde_json::to_value(Response::ok(json!(1), json!({"ok":true}))).unwrap();
101        assert_eq!(v, json!({"jsonrpc":"2.0","id":1,"result":{"ok":true}}));
102    }
103
104    #[test]
105    fn err_response_omits_result() {
106        let v = serde_json::to_value(Response::err(json!(2), -32601, "method not found")).unwrap();
107        assert_eq!(
108            v,
109            json!({"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"method not found"}})
110        );
111    }
112}