use std::io::{self, BufRead, Write};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
pub const PARSE_ERROR: i64 = -32700;
pub const INVALID_REQUEST: i64 = -32600;
pub const METHOD_NOT_FOUND: i64 = -32601;
pub const INVALID_PARAMS: i64 = -32602;
pub const INTERNAL_ERROR: i64 = -32603;
pub const SUPPORTED_PROTOCOL_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"];
pub const LATEST_PROTOCOL_VERSION: &str = "2025-06-18";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Id {
Number(i64),
String(String),
Null,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Message {
Request {
method: String,
params: Option<Value>,
id: Id,
},
Notification {
method: String,
params: Option<Value>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
MalformedJson,
InvalidRequest(Option<Id>),
}
#[derive(Debug, Clone, Serialize)]
pub struct Response {
pub jsonrpc: &'static str,
pub id: Id,
pub result: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ErrorResponse {
pub jsonrpc: &'static str,
pub id: Id,
pub error: RpcError,
}
pub fn success(id: Id, result: Value) -> Response {
Response {
jsonrpc: "2.0",
id,
result,
}
}
pub fn parse_error() -> ErrorResponse {
ErrorResponse {
jsonrpc: "2.0",
id: Id::Null,
error: RpcError {
code: PARSE_ERROR,
message: "Parse error".into(),
data: None,
},
}
}
pub fn invalid_request(id: Id) -> ErrorResponse {
ErrorResponse {
jsonrpc: "2.0",
id,
error: RpcError {
code: INVALID_REQUEST,
message: "Invalid Request".into(),
data: None,
},
}
}
pub fn method_not_found(id: Id) -> ErrorResponse {
ErrorResponse {
jsonrpc: "2.0",
id,
error: RpcError {
code: METHOD_NOT_FOUND,
message: "Method not found".into(),
data: None,
},
}
}
pub fn invalid_params(id: Id) -> ErrorResponse {
ErrorResponse {
jsonrpc: "2.0",
id,
error: RpcError {
code: INVALID_PARAMS,
message: "Invalid params".into(),
data: None,
},
}
}
pub fn internal_error(id: Id) -> ErrorResponse {
ErrorResponse {
jsonrpc: "2.0",
id,
error: RpcError {
code: INTERNAL_ERROR,
message: "Internal error".into(),
data: None,
},
}
}
pub fn negotiate_protocol_version(client_version: &str) -> &'static str {
SUPPORTED_PROTOCOL_VERSIONS
.iter()
.find(|&&v| v == client_version)
.copied()
.unwrap_or(LATEST_PROTOCOL_VERSION)
}
pub fn initialize_result(protocol_version: &str, server_version: &str) -> Value {
json!({
"protocolVersion": protocol_version,
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "tix", "version": server_version },
"instructions": "tix issue tracker — see each tool's description/inputSchema"
})
}
pub fn read_line<R: BufRead>(reader: &mut R) -> io::Result<Option<String>> {
let mut line = String::new();
if reader.read_line(&mut line)? == 0 {
return Ok(None);
}
while line.ends_with('\n') || line.ends_with('\r') {
line.pop();
}
Ok(Some(line))
}
pub fn write_message<W: Write, T: Serialize>(writer: &mut W, message: &T) -> io::Result<()> {
let line = serde_json::to_string(message).map_err(io::Error::other)?;
writer.write_all(line.as_bytes())?;
writer.write_all(b"\n")?;
writer.flush()
}
pub fn parse_message(line: &str) -> Result<Message, ParseError> {
let value: Value = serde_json::from_str(line).map_err(|_| ParseError::MalformedJson)?;
if value.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
return Err(ParseError::InvalidRequest(extract_id(&value)));
}
let method = value
.get("method")
.and_then(Value::as_str)
.ok_or(ParseError::InvalidRequest(extract_id(&value)))?;
let params = value.get("params").cloned();
match extract_id(&value) {
Some(id) => Ok(Message::Request {
method: method.to_string(),
params,
id,
}),
None => Ok(Message::Notification {
method: method.to_string(),
params,
}),
}
}
fn extract_id(value: &Value) -> Option<Id> {
let raw = value.get("id")?;
if raw.is_object() || raw.is_array() || raw.is_boolean() {
return None;
}
Id::deserialize(raw).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_with_numeric_id_is_a_request() {
let msg = parse_message(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#).unwrap();
assert_eq!(
msg,
Message::Request {
method: "ping".into(),
params: None,
id: Id::Number(1),
}
);
}
#[test]
fn request_with_string_id_is_a_request() {
let msg = parse_message(r#"{"jsonrpc":"2.0","id":"a","method":"ping"}"#).unwrap();
assert!(matches!(
msg,
Message::Request { id: Id::String(s), .. } if s == "a"
));
}
#[test]
fn explicit_null_id_is_a_request_not_a_notification() {
let msg = parse_message(r#"{"jsonrpc":"2.0","id":null,"method":"ping"}"#).unwrap();
assert!(matches!(msg, Message::Request { id: Id::Null, .. }));
}
#[test]
fn absent_id_is_a_notification() {
let msg =
parse_message(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#).unwrap();
assert!(
matches!(msg, Message::Notification { method, .. } if method == "notifications/initialized")
);
}
#[test]
fn malformed_json_is_parse_error() {
assert_eq!(
parse_message("{\"jsonrpc\":\"2.0\", not json"),
Err(ParseError::MalformedJson)
);
}
#[test]
fn non_22_jsonrpc_version_is_invalid_request() {
assert_eq!(
parse_message(r#"{"jsonrpc":"1.0","id":1,"method":"ping"}"#),
Err(ParseError::InvalidRequest(Some(Id::Number(1))))
);
}
#[test]
fn missing_method_is_invalid_request_with_id() {
assert_eq!(
parse_message(r#"{"jsonrpc":"2.0","id":7}"#),
Err(ParseError::InvalidRequest(Some(Id::Number(7))))
);
}
#[test]
fn invalid_id_type_is_treated_as_absent_id() {
assert!(matches!(
parse_message(r#"{"jsonrpc":"2.0","id":true,"method":"ping"}"#),
Ok(Message::Notification { method, .. }) if method == "ping"
));
}
#[test]
fn request_carries_params_value() {
let msg = parse_message(
r#"{"jsonrpc":"2.0","id":2,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}"#,
)
.unwrap();
assert!(matches!(msg, Message::Request { method, params, id } if
method == "initialize"
&& params == Some(json!({"protocolVersion": "2025-06-18"}))
&& id == Id::Number(2)));
}
#[test]
fn version_negotiation_echoes_supported_versions() {
assert_eq!(negotiate_protocol_version("2025-06-18"), "2025-06-18");
assert_eq!(negotiate_protocol_version("2025-03-26"), "2025-03-26");
assert_eq!(negotiate_protocol_version("2024-11-05"), "2024-11-05");
}
#[test]
fn version_negotiation_falls_back_to_latest() {
assert_eq!(negotiate_protocol_version("2024-01-01"), "2025-06-18");
assert_eq!(negotiate_protocol_version(""), "2025-06-18");
}
#[test]
fn error_constructors_have_frozen_codes() {
let id = Id::Number(3);
assert_eq!(parse_error().id, Id::Null);
assert_eq!(parse_error().error.code, PARSE_ERROR);
assert_eq!(invalid_request(id.clone()).error.code, INVALID_REQUEST);
assert_eq!(method_not_found(id.clone()).error.code, METHOD_NOT_FOUND);
assert_eq!(invalid_params(id.clone()).error.code, INVALID_PARAMS);
assert_eq!(internal_error(id.clone()).error.code, INTERNAL_ERROR);
assert_eq!(invalid_request(id).error.message, "Invalid Request");
}
#[test]
fn success_response_serializes_as_single_2d0_message() {
let resp = success(Id::Number(1), json!({"ok": true}));
let mut out: Vec<u8> = Vec::new();
write_message(&mut out, &resp).unwrap();
let s = String::from_utf8(out).unwrap();
assert_eq!(
s,
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"ok\":true}}\n"
);
}
#[test]
fn error_response_omits_data_when_absent() {
let resp = method_not_found(Id::Null);
let mut out: Vec<u8> = Vec::new();
write_message(&mut out, &resp).unwrap();
let s = String::from_utf8(out).unwrap();
assert_eq!(
s,
"{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}\n"
);
}
#[test]
fn write_message_never_leaks_bytes_after_the_line() {
let resp = success(Id::String("x".into()), json!({}));
let mut out: Vec<u8> = Vec::new();
write_message(&mut out, &resp).unwrap();
assert_eq!(out.last(), Some(&b'\n'));
let s = String::from_utf8(out).unwrap();
assert_eq!(s.matches('\n').count(), 1);
assert!(s.ends_with('\n'));
}
#[test]
fn read_line_returns_none_on_eof() {
let mut reader: &[u8] = b"";
assert_eq!(read_line(&mut reader).unwrap(), None);
}
#[test]
fn read_line_strips_trailing_newline() {
let mut reader: &[u8] = b"{\"a\":1}\n{\"b\":2}\r\n";
assert_eq!(read_line(&mut reader).unwrap(), Some("{\"a\":1}".into()));
assert_eq!(read_line(&mut reader).unwrap(), Some("{\"b\":2}".into()));
assert_eq!(read_line(&mut reader).unwrap(), None);
}
#[test]
fn initialize_result_shape_matches_contract() {
let result = initialize_result("2025-06-18", "0.1.0");
assert_eq!(
result,
json!({
"protocolVersion": "2025-06-18",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "tix", "version": "0.1.0" },
"instructions": "tix issue tracker — see each tool's description/inputSchema"
})
);
}
}