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 #[allow(clippy::wildcard_imports)]
262 use super::*;
263
264 #[test]
265 fn request_with_numeric_id_is_a_request() {
266 let msg = parse_message(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#).unwrap();
267 assert_eq!(
268 msg,
269 Message::Request {
270 method: "ping".into(),
271 params: None,
272 id: Id::Number(1),
273 }
274 );
275 }
276
277 #[test]
278 fn request_with_string_id_is_a_request() {
279 let msg = parse_message(r#"{"jsonrpc":"2.0","id":"a","method":"ping"}"#).unwrap();
280 assert!(matches!(
281 msg,
282 Message::Request { id: Id::String(s), .. } if s == "a"
283 ));
284 }
285
286 #[test]
287 fn explicit_null_id_is_a_request_not_a_notification() {
288 let msg = parse_message(r#"{"jsonrpc":"2.0","id":null,"method":"ping"}"#).unwrap();
291 assert!(matches!(msg, Message::Request { id: Id::Null, .. }));
292 }
293
294 #[test]
295 fn absent_id_is_a_notification() {
296 let msg =
297 parse_message(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#).unwrap();
298 assert!(
299 matches!(msg, Message::Notification { method, .. } if method == "notifications/initialized")
300 );
301 }
302
303 #[test]
304 fn malformed_json_is_parse_error() {
305 assert_eq!(
306 parse_message("{\"jsonrpc\":\"2.0\", not json"),
307 Err(ParseError::MalformedJson)
308 );
309 }
310
311 #[test]
312 fn non_22_jsonrpc_version_is_invalid_request() {
313 assert_eq!(
314 parse_message(r#"{"jsonrpc":"1.0","id":1,"method":"ping"}"#),
315 Err(ParseError::InvalidRequest(Some(Id::Number(1))))
316 );
317 }
318
319 #[test]
320 fn missing_method_is_invalid_request_with_id() {
321 assert_eq!(
322 parse_message(r#"{"jsonrpc":"2.0","id":7}"#),
323 Err(ParseError::InvalidRequest(Some(Id::Number(7))))
324 );
325 }
326
327 #[test]
328 fn invalid_id_type_is_treated_as_absent_id() {
329 assert!(matches!(
330 parse_message(r#"{"jsonrpc":"2.0","id":true,"method":"ping"}"#),
331 Ok(Message::Notification { method, .. }) if method == "ping"
332 ));
333 }
334
335 #[test]
336 fn request_carries_params_value() {
337 let msg = parse_message(
338 r#"{"jsonrpc":"2.0","id":2,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}"#,
339 )
340 .unwrap();
341 assert!(matches!(msg, Message::Request { method, params, id } if
342 method == "initialize"
343 && params == Some(json!({"protocolVersion": "2025-06-18"}))
344 && id == Id::Number(2)));
345 }
346
347 #[test]
348 fn version_negotiation_echoes_supported_versions() {
349 assert_eq!(negotiate_protocol_version("2025-06-18"), "2025-06-18");
350 assert_eq!(negotiate_protocol_version("2025-03-26"), "2025-03-26");
351 assert_eq!(negotiate_protocol_version("2024-11-05"), "2024-11-05");
352 }
353
354 #[test]
355 fn version_negotiation_falls_back_to_latest() {
356 assert_eq!(negotiate_protocol_version("2024-01-01"), "2025-06-18");
357 assert_eq!(negotiate_protocol_version(""), "2025-06-18");
358 }
359
360 #[test]
361 fn error_constructors_have_frozen_codes() {
362 let id = Id::Number(3);
363 assert_eq!(parse_error().id, Id::Null);
364 assert_eq!(parse_error().error.code, PARSE_ERROR);
365 assert_eq!(invalid_request(id.clone()).error.code, INVALID_REQUEST);
366 assert_eq!(method_not_found(id.clone()).error.code, METHOD_NOT_FOUND);
367 assert_eq!(invalid_params(id.clone()).error.code, INVALID_PARAMS);
368 assert_eq!(internal_error(id.clone()).error.code, INTERNAL_ERROR);
369 assert_eq!(invalid_request(id).error.message, "Invalid Request");
370 }
371
372 #[test]
373 fn success_response_serializes_as_single_2d0_message() {
374 let resp = success(Id::Number(1), json!({"ok": true}));
375 let mut out: Vec<u8> = Vec::new();
376 write_message(&mut out, &resp).unwrap();
377 let s = String::from_utf8(out).unwrap();
378 assert_eq!(
379 s,
380 "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"ok\":true}}\n"
381 );
382 }
383
384 #[test]
385 fn error_response_omits_data_when_absent() {
386 let resp = method_not_found(Id::Null);
387 let mut out: Vec<u8> = Vec::new();
388 write_message(&mut out, &resp).unwrap();
389 let s = String::from_utf8(out).unwrap();
390 assert_eq!(
391 s,
392 "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}\n"
393 );
394 }
395
396 #[test]
397 fn write_message_never_leaks_bytes_after_the_line() {
398 let resp = success(Id::String("x".into()), json!({}));
400 let mut out: Vec<u8> = Vec::new();
401 write_message(&mut out, &resp).unwrap();
402 assert_eq!(out.last(), Some(&b'\n'));
403 let s = String::from_utf8(out).unwrap();
404 assert_eq!(s.matches('\n').count(), 1);
405 assert!(s.ends_with('\n'));
406 }
407
408 #[test]
409 fn read_line_returns_none_on_eof() {
410 let mut reader: &[u8] = b"";
411 assert_eq!(read_line(&mut reader).unwrap(), None);
412 }
413
414 #[test]
415 fn read_line_strips_trailing_newline() {
416 let mut reader: &[u8] = b"{\"a\":1}\n{\"b\":2}\r\n";
417 assert_eq!(read_line(&mut reader).unwrap(), Some("{\"a\":1}".into()));
418 assert_eq!(read_line(&mut reader).unwrap(), Some("{\"b\":2}".into()));
419 assert_eq!(read_line(&mut reader).unwrap(), None);
420 }
421
422 #[test]
423 fn initialize_result_shape_matches_contract() {
424 let result = initialize_result("2025-06-18", "0.1.0");
425 assert_eq!(
426 result,
427 json!({
428 "protocolVersion": "2025-06-18",
429 "capabilities": { "tools": { "listChanged": false } },
430 "serverInfo": { "name": "tix", "version": "0.1.0" },
431 "instructions": "tix issue tracker — see each tool's description/inputSchema"
432 })
433 );
434 }
435}