Skip to main content

do_memory_mcp/
jsonrpc.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::io::{self, BufRead, Read, Write};
4
5/// JSON-RPC request structure
6#[derive(Debug, Deserialize)]
7pub struct JsonRpcRequest {
8    #[serde(default)]
9    pub jsonrpc: Option<String>,
10    #[serde(default)]
11    pub id: Option<Value>,
12    pub method: String,
13    #[serde(default)]
14    pub params: Option<Value>,
15}
16
17/// JSON-RPC response structure
18#[derive(Debug, Serialize, Deserialize)]
19pub struct JsonRpcResponse {
20    pub jsonrpc: String,
21    pub id: Option<Value>,
22    // Always include result, even when null, to satisfy strict clients
23    pub result: Option<Value>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub error: Option<JsonRpcError>,
26}
27
28/// JSON-RPC error structure
29#[derive(Debug, Serialize, Deserialize)]
30pub struct JsonRpcError {
31    pub code: i32,
32    pub message: String,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub data: Option<Value>,
35}
36
37/// Read a message from a reader supporting both line-delimited JSON and LSP Content-Length framing.
38/// Returns (message, is_content_length) where is_content_length indicates whether the message
39/// came in with a Content-Length header (LSP-style)
40pub fn read_next_message<R: BufRead + Read>(reader: &mut R) -> io::Result<Option<(String, bool)>> {
41    loop {
42        let mut line = String::new();
43        let n = reader.read_line(&mut line)?;
44        if n == 0 {
45            // EOF
46            return Ok(None);
47        }
48
49        let trimmed = line.trim();
50        if trimmed.is_empty() {
51            continue;
52        }
53
54        // If the line looks like JSON directly, return it (not LSP framed)
55        if trimmed.starts_with('{') {
56            return Ok(Some((trimmed.to_string(), false)));
57        }
58
59        // If it's a Content-Length header, parse it and read the payload
60        let low = trimmed.to_ascii_lowercase();
61        if low.starts_with("content-length:") {
62            // Parse length
63            let parts: Vec<&str> = trimmed.splitn(2, ':').collect();
64            let len: usize = parts
65                .get(1)
66                .map(|s| s.trim().parse().ok().unwrap_or(0))
67                .unwrap_or(0);
68
69            // Consume remaining header lines until we reach an empty line
70            loop {
71                let mut hline = String::new();
72                let hn = reader.read_line(&mut hline)?;
73                if hn == 0 || hline.trim().is_empty() {
74                    break;
75                }
76            }
77
78            // Read exact number of bytes for the content
79            if len == 0 {
80                continue;
81            }
82            let mut buf = vec![0u8; len];
83            reader.read_exact(&mut buf)?;
84            return Ok(Some((String::from_utf8_lossy(&buf).to_string(), true)));
85        }
86
87        // Otherwise, skip the line (e.g., logs accidentally printed to stdout) and continue
88        continue;
89    }
90}
91
92/// Write a JSON-RPC response using Content-Length framing to support LSP-style clients.
93pub fn write_response_with_length<W: Write>(writer: &mut W, body: &str) -> io::Result<()> {
94    let bytes = body.as_bytes();
95    let header = format!("Content-Length: {}\r\n\r\n", bytes.len());
96    writer.write_all(header.as_bytes())?;
97    writer.write_all(bytes)?;
98    writer.flush()?;
99    Ok(())
100}