biblion/
protocol.rs

1//! MCP (Model Context Protocol) JSON-RPC types.
2//!
3//! Implements the subset of MCP 2024-11-05 needed for a stdio-based tool server.
4//! Adapted from ox-mcp (oxymake).
5
6use serde::{Deserialize, Serialize};
7
8// ---------------------------------------------------------------------------
9// JSON-RPC 2.0 envelope
10// ---------------------------------------------------------------------------
11
12#[derive(Debug, Deserialize)]
13pub struct JsonRpcRequest {
14    pub jsonrpc: String,
15    pub id: Option<serde_json::Value>,
16    pub method: String,
17    #[serde(default)]
18    pub params: serde_json::Value,
19}
20
21#[derive(Debug, Serialize)]
22pub struct JsonRpcResponse {
23    pub jsonrpc: String,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub id: Option<serde_json::Value>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub result: Option<serde_json::Value>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub error: Option<JsonRpcError>,
30}
31
32#[derive(Debug, Serialize)]
33pub struct JsonRpcError {
34    pub code: i64,
35    pub message: String,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub data: Option<serde_json::Value>,
38}
39
40impl JsonRpcResponse {
41    pub fn success(id: Option<serde_json::Value>, result: serde_json::Value) -> Self {
42        Self {
43            jsonrpc: "2.0".into(),
44            id,
45            result: Some(result),
46            error: None,
47        }
48    }
49
50    pub fn error(id: Option<serde_json::Value>, code: i64, message: String) -> Self {
51        Self {
52            jsonrpc: "2.0".into(),
53            id,
54            result: None,
55            error: Some(JsonRpcError {
56                code,
57                message,
58                data: None,
59            }),
60        }
61    }
62
63    pub fn method_not_found(id: Option<serde_json::Value>, method: &str) -> Self {
64        Self::error(id, -32601, format!("Method not found: {method}"))
65    }
66}
67
68// ---------------------------------------------------------------------------
69// MCP types
70// ---------------------------------------------------------------------------
71
72#[derive(Debug, Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct InitializeResult {
75    pub protocol_version: String,
76    pub capabilities: ServerCapabilities,
77    pub server_info: ServerInfo,
78    /// Instructions for the agent — explains what this server provides
79    /// and how to use it. Sent once on connection.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub instructions: Option<String>,
82}
83
84#[derive(Debug, Serialize)]
85pub struct ServerCapabilities {
86    pub tools: ToolsCapability,
87}
88
89#[derive(Debug, Serialize)]
90pub struct ToolsCapability {
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub list_changed: Option<bool>,
93}
94
95#[derive(Debug, Serialize)]
96pub struct ServerInfo {
97    pub name: String,
98    pub version: String,
99}
100
101#[derive(Debug, Serialize)]
102#[serde(rename_all = "camelCase")]
103pub struct ToolDefinition {
104    pub name: String,
105    pub description: String,
106    pub input_schema: serde_json::Value,
107}
108
109#[derive(Debug, Serialize)]
110pub struct ToolsListResult {
111    pub tools: Vec<ToolDefinition>,
112}
113
114#[derive(Debug, Deserialize)]
115pub struct ToolCallParams {
116    pub name: String,
117    #[serde(default)]
118    pub arguments: serde_json::Value,
119}
120
121#[derive(Debug, Serialize)]
122pub struct ToolCallResult {
123    pub content: Vec<ToolContent>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    #[serde(rename = "isError")]
126    pub is_error: Option<bool>,
127}
128
129#[derive(Debug, Serialize)]
130pub struct ToolContent {
131    #[serde(rename = "type")]
132    pub content_type: String,
133    pub text: String,
134}
135
136impl ToolCallResult {
137    pub fn text(text: String) -> Self {
138        Self {
139            content: vec![ToolContent {
140                content_type: "text".into(),
141                text,
142            }],
143            is_error: None,
144        }
145    }
146
147    pub fn error(message: String) -> Self {
148        Self {
149            content: vec![ToolContent {
150                content_type: "text".into(),
151                text: message,
152            }],
153            is_error: Some(true),
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use serde_json::json;
162
163    #[test]
164    fn json_rpc_request_deserializes() {
165        let raw = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#;
166        let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
167        assert_eq!(req.method, "tools/list");
168        assert_eq!(req.id, Some(json!(1)));
169    }
170
171    #[test]
172    fn json_rpc_request_without_id() {
173        let raw = r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#;
174        let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
175        assert!(req.id.is_none());
176    }
177
178    #[test]
179    fn json_rpc_request_default_params() {
180        let raw = r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#;
181        let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
182        assert!(req.params.is_null());
183    }
184
185    #[test]
186    fn success_response_serializes() {
187        let resp = JsonRpcResponse::success(Some(json!(42)), json!({"ok": true}));
188        let json = serde_json::to_value(&resp).unwrap();
189        assert_eq!(json["jsonrpc"], "2.0");
190        assert_eq!(json["id"], 42);
191        assert_eq!(json["result"]["ok"], true);
192        assert!(json.get("error").is_none());
193    }
194
195    #[test]
196    fn error_response_serializes() {
197        let resp = JsonRpcResponse::error(Some(json!(1)), -32600, "Invalid Request".into());
198        let json = serde_json::to_value(&resp).unwrap();
199        assert_eq!(json["error"]["code"], -32600);
200        assert!(json.get("result").is_none());
201    }
202
203    #[test]
204    fn method_not_found_response() {
205        let resp = JsonRpcResponse::method_not_found(Some(json!(5)), "foo/bar");
206        let json = serde_json::to_value(&resp).unwrap();
207        assert_eq!(json["error"]["code"], -32601);
208        assert!(
209            json["error"]["message"]
210                .as_str()
211                .unwrap()
212                .contains("foo/bar")
213        );
214    }
215
216    #[test]
217    fn success_response_omits_null_fields() {
218        let resp = JsonRpcResponse::success(None, json!("ok"));
219        let json_str = serde_json::to_string(&resp).unwrap();
220        assert!(!json_str.contains("\"id\""));
221        assert!(!json_str.contains("\"error\""));
222    }
223
224    #[test]
225    fn initialize_result_serializes_camel_case() {
226        let result = InitializeResult {
227            protocol_version: "2024-11-05".into(),
228            capabilities: ServerCapabilities {
229                tools: ToolsCapability {
230                    list_changed: Some(false),
231                },
232            },
233            server_info: ServerInfo {
234                name: "biblion".into(),
235                version: "0.1.0".into(),
236            },
237            instructions: None,
238        };
239        let json = serde_json::to_value(result).unwrap();
240        assert_eq!(json["protocolVersion"], "2024-11-05");
241        assert_eq!(json["serverInfo"]["name"], "biblion");
242    }
243
244    #[test]
245    fn tool_call_params_deserializes() {
246        let raw = r#"{"name":"zotero_search","arguments":{"query":"test"}}"#;
247        let params: ToolCallParams = serde_json::from_str(raw).unwrap();
248        assert_eq!(params.name, "zotero_search");
249        assert_eq!(params.arguments["query"], "test");
250    }
251
252    #[test]
253    fn tool_call_result_text() {
254        let r = ToolCallResult::text("hello".into());
255        assert_eq!(r.content[0].text, "hello");
256        assert!(r.is_error.is_none());
257    }
258
259    #[test]
260    fn tool_call_result_error() {
261        let r = ToolCallResult::error("boom".into());
262        assert_eq!(r.is_error, Some(true));
263        assert_eq!(r.content[0].text, "boom");
264    }
265
266    #[test]
267    fn tool_call_result_error_serializes_is_error() {
268        let r = ToolCallResult::error("fail".into());
269        let json = serde_json::to_value(&r).unwrap();
270        assert_eq!(json["isError"], true);
271    }
272
273    #[test]
274    fn tool_call_result_text_omits_is_error() {
275        let r = ToolCallResult::text("ok".into());
276        let json_str = serde_json::to_string(&r).unwrap();
277        assert!(!json_str.contains("isError"));
278    }
279}