1use std::io::{self, BufRead, Write};
13
14use serde::{Deserialize, Serialize};
15use serde_json::{Value, json};
16
17pub const PARSE_ERROR: i64 = -32700;
20pub const INVALID_REQUEST: i64 = -32600;
22pub const METHOD_NOT_FOUND: i64 = -32601;
24pub const INVALID_PARAMS: i64 = -32602;
26pub const INTERNAL_ERROR: i64 = -32603;
29
30pub const SUPPORTED_PROTOCOL_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"];
33
34pub const LATEST_PROTOCOL_VERSION: &str = "2025-06-18";
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(untagged)]
43pub enum Id {
44 Number(i64),
45 String(String),
46 Null,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum Message {
53 Request {
54 method: String,
55 params: Option<Value>,
56 id: Id,
57 },
58 Notification {
59 method: String,
60 params: Option<Value>,
61 },
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum ParseError {
68 MalformedJson,
70 InvalidRequest(Option<Id>),
74}
75
76#[derive(Debug, Clone, Serialize)]
78pub struct Response {
79 pub jsonrpc: &'static str,
80 pub id: Id,
81 pub result: Value,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct RpcError {
87 pub code: i64,
88 pub message: String,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub data: Option<Value>,
91}
92
93#[derive(Debug, Clone, Serialize)]
95pub struct ErrorResponse {
96 pub jsonrpc: &'static str,
97 pub id: Id,
98 pub error: RpcError,
99}
100
101pub fn success(id: Id, result: Value) -> Response {
103 Response {
104 jsonrpc: "2.0",
105 id,
106 result,
107 }
108}
109
110pub fn parse_error() -> ErrorResponse {
113 ErrorResponse {
114 jsonrpc: "2.0",
115 id: Id::Null,
116 error: RpcError {
117 code: PARSE_ERROR,
118 message: "Parse error".into(),
119 data: None,
120 },
121 }
122}
123
124pub fn invalid_request(id: Id) -> ErrorResponse {
126 ErrorResponse {
127 jsonrpc: "2.0",
128 id,
129 error: RpcError {
130 code: INVALID_REQUEST,
131 message: "Invalid Request".into(),
132 data: None,
133 },
134 }
135}
136
137pub fn method_not_found(id: Id) -> ErrorResponse {
139 ErrorResponse {
140 jsonrpc: "2.0",
141 id,
142 error: RpcError {
143 code: METHOD_NOT_FOUND,
144 message: "Method not found".into(),
145 data: None,
146 },
147 }
148}
149
150pub fn invalid_params(id: Id) -> ErrorResponse {
152 ErrorResponse {
153 jsonrpc: "2.0",
154 id,
155 error: RpcError {
156 code: INVALID_PARAMS,
157 message: "Invalid params".into(),
158 data: None,
159 },
160 }
161}
162
163pub fn internal_error(id: Id) -> ErrorResponse {
165 ErrorResponse {
166 jsonrpc: "2.0",
167 id,
168 error: RpcError {
169 code: INTERNAL_ERROR,
170 message: "Internal error".into(),
171 data: None,
172 },
173 }
174}
175
176pub fn negotiate_protocol_version(client_version: &str) -> &'static str {
180 SUPPORTED_PROTOCOL_VERSIONS
181 .iter()
182 .find(|&&v| v == client_version)
183 .copied()
184 .unwrap_or(LATEST_PROTOCOL_VERSION)
185}
186
187pub fn initialize_result(protocol_version: &str, server_version: &str) -> Value {
189 json!({
190 "protocolVersion": protocol_version,
191 "capabilities": { "tools": { "listChanged": false } },
192 "serverInfo": { "name": "tix", "version": server_version },
193 "instructions": "tix issue tracker — see each tool's description/inputSchema"
194 })
195}
196
197pub fn read_line<R: BufRead>(reader: &mut R) -> io::Result<Option<String>> {
201 let mut line = String::new();
202 if reader.read_line(&mut line)? == 0 {
203 return Ok(None);
204 }
205 while line.ends_with('\n') || line.ends_with('\r') {
206 line.pop();
207 }
208 Ok(Some(line))
209}
210
211pub fn write_message<W: Write, T: Serialize>(writer: &mut W, message: &T) -> io::Result<()> {
216 let line = serde_json::to_string(message).map_err(io::Error::other)?;
217 writer.write_all(line.as_bytes())?;
218 writer.write_all(b"\n")?;
219 writer.flush()
220}
221
222pub fn parse_message(line: &str) -> Result<Message, ParseError> {
226 let value: Value = serde_json::from_str(line).map_err(|_| ParseError::MalformedJson)?;
227 if value.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
228 return Err(ParseError::InvalidRequest(extract_id(&value)));
229 }
230 let method = value
231 .get("method")
232 .and_then(Value::as_str)
233 .ok_or(ParseError::InvalidRequest(extract_id(&value)))?;
234 let params = value.get("params").cloned();
235 match extract_id(&value) {
236 Some(id) => Ok(Message::Request {
237 method: method.to_string(),
238 params,
239 id,
240 }),
241 None => Ok(Message::Notification {
242 method: method.to_string(),
243 params,
244 }),
245 }
246}
247
248fn extract_id(value: &Value) -> Option<Id> {
252 let raw = value.get("id")?;
253 if raw.is_object() || raw.is_array() || raw.is_boolean() {
254 return None;
255 }
256 Id::deserialize(raw).ok()
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn request_with_numeric_id_is_a_request() {
265 let msg = parse_message(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#).unwrap();
266 assert_eq!(
267 msg,
268 Message::Request {
269 method: "ping".into(),
270 params: None,
271 id: Id::Number(1),
272 }
273 );
274 }
275
276 #[test]
277 fn request_with_string_id_is_a_request() {
278 let msg = parse_message(r#"{"jsonrpc":"2.0","id":"a","method":"ping"}"#).unwrap();
279 assert!(matches!(
280 msg,
281 Message::Request { id: Id::String(s), .. } if s == "a"
282 ));
283 }
284
285 #[test]
286 fn explicit_null_id_is_a_request_not_a_notification() {
287 let msg = parse_message(r#"{"jsonrpc":"2.0","id":null,"method":"ping"}"#).unwrap();
290 assert!(matches!(msg, Message::Request { id: Id::Null, .. }));
291 }
292
293 #[test]
294 fn absent_id_is_a_notification() {
295 let msg =
296 parse_message(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#).unwrap();
297 assert!(
298 matches!(msg, Message::Notification { method, .. } if method == "notifications/initialized")
299 );
300 }
301
302 #[test]
303 fn malformed_json_is_parse_error() {
304 assert_eq!(
305 parse_message("{\"jsonrpc\":\"2.0\", not json"),
306 Err(ParseError::MalformedJson)
307 );
308 }
309
310 #[test]
311 fn non_22_jsonrpc_version_is_invalid_request() {
312 assert_eq!(
313 parse_message(r#"{"jsonrpc":"1.0","id":1,"method":"ping"}"#),
314 Err(ParseError::InvalidRequest(Some(Id::Number(1))))
315 );
316 }
317
318 #[test]
319 fn missing_method_is_invalid_request_with_id() {
320 assert_eq!(
321 parse_message(r#"{"jsonrpc":"2.0","id":7}"#),
322 Err(ParseError::InvalidRequest(Some(Id::Number(7))))
323 );
324 }
325
326 #[test]
327 fn invalid_id_type_is_treated_as_absent_id() {
328 assert!(matches!(
329 parse_message(r#"{"jsonrpc":"2.0","id":true,"method":"ping"}"#),
330 Ok(Message::Notification { method, .. }) if method == "ping"
331 ));
332 }
333
334 #[test]
335 fn request_carries_params_value() {
336 let msg = parse_message(
337 r#"{"jsonrpc":"2.0","id":2,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}"#,
338 )
339 .unwrap();
340 assert!(matches!(msg, Message::Request { method, params, id } if
341 method == "initialize"
342 && params == Some(json!({"protocolVersion": "2025-06-18"}))
343 && id == Id::Number(2)));
344 }
345
346 #[test]
347 fn version_negotiation_echoes_supported_versions() {
348 assert_eq!(negotiate_protocol_version("2025-06-18"), "2025-06-18");
349 assert_eq!(negotiate_protocol_version("2025-03-26"), "2025-03-26");
350 assert_eq!(negotiate_protocol_version("2024-11-05"), "2024-11-05");
351 }
352
353 #[test]
354 fn version_negotiation_falls_back_to_latest() {
355 assert_eq!(negotiate_protocol_version("2024-01-01"), "2025-06-18");
356 assert_eq!(negotiate_protocol_version(""), "2025-06-18");
357 }
358
359 #[test]
360 fn error_constructors_have_frozen_codes() {
361 let id = Id::Number(3);
362 assert_eq!(parse_error().id, Id::Null);
363 assert_eq!(parse_error().error.code, PARSE_ERROR);
364 assert_eq!(invalid_request(id.clone()).error.code, INVALID_REQUEST);
365 assert_eq!(method_not_found(id.clone()).error.code, METHOD_NOT_FOUND);
366 assert_eq!(invalid_params(id.clone()).error.code, INVALID_PARAMS);
367 assert_eq!(internal_error(id.clone()).error.code, INTERNAL_ERROR);
368 assert_eq!(invalid_request(id).error.message, "Invalid Request");
369 }
370
371 #[test]
372 fn success_response_serializes_as_single_2d0_message() {
373 let resp = success(Id::Number(1), json!({"ok": true}));
374 let mut out: Vec<u8> = Vec::new();
375 write_message(&mut out, &resp).unwrap();
376 let s = String::from_utf8(out).unwrap();
377 assert_eq!(
378 s,
379 "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"ok\":true}}\n"
380 );
381 }
382
383 #[test]
384 fn error_response_omits_data_when_absent() {
385 let resp = method_not_found(Id::Null);
386 let mut out: Vec<u8> = Vec::new();
387 write_message(&mut out, &resp).unwrap();
388 let s = String::from_utf8(out).unwrap();
389 assert_eq!(
390 s,
391 "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}\n"
392 );
393 }
394
395 #[test]
396 fn write_message_never_leaks_bytes_after_the_line() {
397 let resp = success(Id::String("x".into()), json!({}));
399 let mut out: Vec<u8> = Vec::new();
400 write_message(&mut out, &resp).unwrap();
401 assert_eq!(out.last(), Some(&b'\n'));
402 let s = String::from_utf8(out).unwrap();
403 assert_eq!(s.matches('\n').count(), 1);
404 assert!(s.ends_with('\n'));
405 }
406
407 #[test]
408 fn read_line_returns_none_on_eof() {
409 let mut reader: &[u8] = b"";
410 assert_eq!(read_line(&mut reader).unwrap(), None);
411 }
412
413 #[test]
414 fn read_line_strips_trailing_newline() {
415 let mut reader: &[u8] = b"{\"a\":1}\n{\"b\":2}\r\n";
416 assert_eq!(read_line(&mut reader).unwrap(), Some("{\"a\":1}".into()));
417 assert_eq!(read_line(&mut reader).unwrap(), Some("{\"b\":2}".into()));
418 assert_eq!(read_line(&mut reader).unwrap(), None);
419 }
420
421 #[test]
422 fn initialize_result_shape_matches_contract() {
423 let result = initialize_result("2025-06-18", "0.1.0");
424 assert_eq!(
425 result,
426 json!({
427 "protocolVersion": "2025-06-18",
428 "capabilities": { "tools": { "listChanged": false } },
429 "serverInfo": { "name": "tix", "version": "0.1.0" },
430 "instructions": "tix issue tracker — see each tool's description/inputSchema"
431 })
432 );
433 }
434}