1use serde::{Deserialize, Serialize};
7
8#[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#[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 #[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}