Skip to main content

mcp/rpc/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! Shared JSON-RPC 2.0 codec.
3//!
4//! One set of wire types serves three surfaces: the MCP client (to external
5//! servers), the self-MCP server, and the private supervisor↔subagent
6//! control channel. They differ only in *framing* (see [`frame`]): MCP stdio
7//! is newline-delimited; the control channel is length-prefixed.
8//!
9//! Keeping every wire type behind `serde` in this one module is deliberate: it
10//! is the single isolation point from which the codec could be swapped to a
11//! lighter encoder (e.g. miniserde) without touching call sites, should the
12//! proc-macro compile weight ever need to come out of the dependency budget.
13
14pub mod frame;
15
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18
19/// JSON-RPC request/response id. Spec allows string or number (and, in
20/// responses to a parse error, null). We never *send* a null id.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(untagged)]
23pub enum Id {
24    Num(i64),
25    Str(String),
26}
27
28impl From<i64> for Id {
29    fn from(n: i64) -> Self {
30        Id::Num(n)
31    }
32}
33impl From<String> for Id {
34    fn from(s: String) -> Self {
35        Id::Str(s)
36    }
37}
38
39/// A JSON-RPC 2.0 request (has an `id`; expects a response).
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Request {
42    pub jsonrpc: Version,
43    pub id: Id,
44    pub method: String,
45    #[serde(skip_serializing_if = "Option::is_none", default)]
46    pub params: Option<Value>,
47}
48
49/// A JSON-RPC 2.0 notification (no `id`; no response).
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct Notification {
52    pub jsonrpc: Version,
53    pub method: String,
54    #[serde(skip_serializing_if = "Option::is_none", default)]
55    pub params: Option<Value>,
56}
57
58/// A JSON-RPC 2.0 response (exactly one of `result` / `error`).
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct Response {
61    pub jsonrpc: Version,
62    pub id: Id,
63    #[serde(skip_serializing_if = "Option::is_none", default)]
64    pub result: Option<Value>,
65    #[serde(skip_serializing_if = "Option::is_none", default)]
66    pub error: Option<RpcError>,
67}
68
69/// A JSON-RPC 2.0 error object. Distinct from a *successful* result that
70/// carries `isError: true` — the latter is a tool-domain failure fed back to
71/// the model as an observation, the former is a protocol/transport failure.
72/// That distinction is load-bearing in the loop: an `isError` result keeps the
73/// conversation going, an `RpcError` aborts the call.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct RpcError {
76    pub code: i64,
77    pub message: String,
78    #[serde(skip_serializing_if = "Option::is_none", default)]
79    pub data: Option<Value>,
80}
81
82/// Any inbound JSON-RPC frame, before we know which kind it is. A reader
83/// thread parses one of these per frame and dispatches: responses resolve a
84/// pending request by id; notifications fan out to handlers; requests (only
85/// on the server side / sampling-style server→client) are answered.
86#[derive(Debug, Clone, Deserialize)]
87#[serde(untagged)]
88pub enum Incoming {
89    // Order matters for untagged. `Request` first: its `method` is a *required*
90    // field, so it only matches frames that actually have one — and a Response's
91    // optional `result`/`error` would otherwise let `Response` swallow a Request.
92    // A Response (id, no method) then falls through to `Response`; a Notification
93    // (method, no id) to `Notification`.
94    Request(Request),
95    Response(Response),
96    Notification(Notification),
97}
98
99/// The literal `"2.0"`. A newtype so a malformed `jsonrpc` field is a parse
100/// error, not a silent mismatch.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct Version;
103
104impl Serialize for Version {
105    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
106        s.serialize_str("2.0")
107    }
108}
109impl<'de> Deserialize<'de> for Version {
110    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
111        let s = String::deserialize(d)?;
112        if s == "2.0" {
113            Ok(Version)
114        } else {
115            Err(serde::de::Error::custom("jsonrpc version must be \"2.0\""))
116        }
117    }
118}
119
120impl Request {
121    pub fn new(id: impl Into<Id>, method: impl Into<String>, params: Option<Value>) -> Self {
122        Request {
123            jsonrpc: Version,
124            id: id.into(),
125            method: method.into(),
126            params,
127        }
128    }
129}
130
131impl Notification {
132    pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
133        Notification {
134            jsonrpc: Version,
135            method: method.into(),
136            params,
137        }
138    }
139}
140
141impl Response {
142    pub fn ok(id: Id, result: Value) -> Self {
143        Response {
144            jsonrpc: Version,
145            id,
146            result: Some(result),
147            error: None,
148        }
149    }
150    pub fn err(id: Id, code: i64, message: impl Into<String>) -> Self {
151        Response {
152            jsonrpc: Version,
153            id,
154            result: None,
155            error: Some(RpcError {
156                code,
157                message: message.into(),
158                data: None,
159            }),
160        }
161    }
162}
163
164// Standard JSON-RPC error codes (subset we use).
165pub const PARSE_ERROR: i64 = -32700;
166pub const INVALID_REQUEST: i64 = -32600;
167pub const METHOD_NOT_FOUND: i64 = -32601;
168pub const INVALID_PARAMS: i64 = -32602;
169pub const INTERNAL_ERROR: i64 = -32603;
170/// MCP server-defined: a `resources/read` for a URI the server doesn't have.
171pub const RESOURCE_NOT_FOUND: i64 = -32002;
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn request_roundtrips() {
179        let r = Request::new(1, "tools/call", Some(serde_json::json!({"name": "x"})));
180        let s = serde_json::to_string(&r).unwrap();
181        assert!(s.contains("\"jsonrpc\":\"2.0\""));
182        assert!(s.contains("\"id\":1"));
183        let back: Request = serde_json::from_str(&s).unwrap();
184        assert_eq!(back.method, "tools/call");
185    }
186
187    #[test]
188    fn incoming_discriminates_response_vs_notification() {
189        let resp = r#"{"jsonrpc":"2.0","id":7,"result":{"ok":true}}"#;
190        match serde_json::from_str::<Incoming>(resp).unwrap() {
191            Incoming::Response(r) => assert_eq!(r.id, Id::Num(7)),
192            other => panic!("expected response, got {other:?}"),
193        }
194        let note = r#"{"jsonrpc":"2.0","method":"notifications/resources/updated","params":{"uri":"file://a"}}"#;
195        match serde_json::from_str::<Incoming>(note).unwrap() {
196            Incoming::Notification(n) => assert_eq!(n.method, "notifications/resources/updated"),
197            other => panic!("expected notification, got {other:?}"),
198        }
199    }
200
201    #[test]
202    fn incoming_parses_request_not_response() {
203        // Regression: a server→client request (has `method`) must parse as
204        // Request, not be swallowed by Response (whose fields are all optional).
205        let req = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
206        match serde_json::from_str::<Incoming>(req).unwrap() {
207            Incoming::Request(r) => assert_eq!(r.method, "initialize"),
208            other => panic!("expected request, got {other:?}"),
209        }
210    }
211
212    #[test]
213    fn bad_version_is_a_parse_error() {
214        let bad = r#"{"jsonrpc":"1.0","id":1,"method":"x"}"#;
215        assert!(serde_json::from_str::<Request>(bad).is_err());
216    }
217
218    #[test]
219    fn string_id_supported() {
220        let resp = r#"{"jsonrpc":"2.0","id":"abc","result":1}"#;
221        let r: Response = serde_json::from_str(resp).unwrap();
222        assert_eq!(r.id, Id::Str("abc".into()));
223    }
224}