Skip to main content

reddb_server/mcp/
protocol.rs

1//! JSON-RPC protocol handling for MCP server.
2//!
3//! The Content-Length framed JSON-RPC 2.0 transport used by the Model Context
4//! Protocol over stdio is a message-framing wire codec and lives in the
5//! protocol-authority crate (`reddb_wire::jsonrpc`), per ADR 0046. This module
6//! binds the server's JSON value type to that codec via [`ServerJson`] and
7//! re-exports the framing functions, so call sites keep using
8//! `protocol::read_payload`, `protocol::build_result_message`, etc. unchanged.
9
10use crate::json::{Map, Value as JsonValue};
11use reddb_wire::jsonrpc::{self, JsonRpcSerializer};
12
13pub use reddb_wire::jsonrpc::{read_payload, write_message};
14
15/// Binds `reddb-server`'s JSON value type to the wire crate's framed JSON-RPC
16/// envelope builders.
17///
18/// Objects are backed by [`Map`] (a `BTreeMap`), which sorts keys on compact
19/// serialization — that sort order is what pins the emitted field order.
20pub struct ServerJson;
21
22impl JsonRpcSerializer for ServerJson {
23    type Value = JsonValue;
24
25    fn null() -> JsonValue {
26        JsonValue::Null
27    }
28
29    fn string(value: &str) -> JsonValue {
30        JsonValue::String(value.to_string())
31    }
32
33    fn number(value: i64) -> JsonValue {
34        JsonValue::Number(value as f64)
35    }
36
37    fn object(entries: Vec<(&'static str, JsonValue)>) -> JsonValue {
38        let mut object = Map::new();
39        for (key, value) in entries {
40            object.insert(key.to_string(), value);
41        }
42        JsonValue::Object(object)
43    }
44
45    fn to_compact_string(value: &JsonValue) -> String {
46        value.to_string_compact()
47    }
48}
49
50/// Build a JSON-RPC 2.0 result message.
51pub fn build_result_message(id: Option<&JsonValue>, result: JsonValue) -> String {
52    jsonrpc::build_result_message::<ServerJson>(id, result)
53}
54
55/// Build a JSON-RPC 2.0 error message.
56pub fn build_error_message(id: Option<&JsonValue>, code: i64, message: &str) -> String {
57    jsonrpc::build_error_message::<ServerJson>(id, code, message)
58}
59
60/// Build a JSON-RPC 2.0 notification (no id, no response expected).
61pub fn build_notification(method: &str, params: JsonValue) -> String {
62    jsonrpc::build_notification::<ServerJson>(method, params)
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::json::from_str;
69
70    #[test]
71    fn test_build_result_message() {
72        let id = JsonValue::Number(1.0);
73        let result = JsonValue::Bool(true);
74        let msg = build_result_message(Some(&id), result);
75        let parsed: JsonValue = from_str(&msg).unwrap();
76        assert_eq!(parsed.get("jsonrpc").and_then(|v| v.as_str()), Some("2.0"));
77        assert_eq!(parsed.get("id").and_then(|v| v.as_f64()), Some(1.0));
78        // Field order is pinned by the BTreeMap-backed serializer.
79        assert_eq!(msg, r#"{"id":1,"jsonrpc":"2.0","result":true}"#);
80    }
81
82    #[test]
83    fn test_build_error_message() {
84        let id = JsonValue::Number(2.0);
85        let msg = build_error_message(Some(&id), -32601, "method not found");
86        let parsed: JsonValue = from_str(&msg).unwrap();
87        assert_eq!(parsed.get("jsonrpc").and_then(|v| v.as_str()), Some("2.0"));
88        let error = parsed.get("error").unwrap();
89        assert_eq!(error.get("code").and_then(|v| v.as_f64()), Some(-32601.0));
90        assert_eq!(
91            error.get("message").and_then(|v| v.as_str()),
92            Some("method not found")
93        );
94        assert_eq!(
95            msg,
96            r#"{"error":{"code":-32601,"message":"method not found"},"id":2,"jsonrpc":"2.0"}"#
97        );
98    }
99
100    #[test]
101    fn test_build_notification() {
102        let msg = build_notification("test/event", JsonValue::Null);
103        let parsed: JsonValue = from_str(&msg).unwrap();
104        assert_eq!(
105            parsed.get("method").and_then(|v| v.as_str()),
106            Some("test/event")
107        );
108        assert!(parsed.get("id").is_none());
109        assert_eq!(
110            msg,
111            r#"{"jsonrpc":"2.0","method":"test/event","params":null}"#
112        );
113    }
114
115    #[test]
116    fn test_read_payload_basic() {
117        let body = r#"{"id":1}"#;
118        let msg = format!("Content-Length: {}\r\n\r\n{}", body.len(), body);
119        let mut reader = std::io::BufReader::new(msg.as_bytes());
120        let payload = read_payload(&mut reader).unwrap();
121        assert_eq!(payload, Some(body.to_string()));
122    }
123
124    #[test]
125    fn test_read_payload_eof() {
126        let input = b"";
127        let mut reader = std::io::BufReader::new(&input[..]);
128        let payload = read_payload(&mut reader).unwrap();
129        assert!(payload.is_none());
130    }
131}