use serde::{Deserialize, Deserializer, Serialize, de};
use serde_json::Value;
#[allow(dead_code)]
pub(crate) const PARSE_ERROR: i64 = -32700;
#[allow(dead_code)]
pub(crate) const INVALID_REQUEST: i64 = -32600;
#[allow(dead_code)]
pub(crate) const METHOD_NOT_FOUND: i64 = -32601;
#[allow(dead_code)]
pub(crate) const INVALID_PARAMS: i64 = -32602;
#[allow(dead_code)]
pub(crate) const INTERNAL_ERROR: i64 = -32603;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum RequestId {
Number(i64),
String(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct JsonRpcRequest {
pub(crate) jsonrpc: String,
pub(crate) id: RequestId,
pub(crate) method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) params: Option<Value>,
}
impl JsonRpcRequest {
pub(crate) fn new(id: RequestId, method: impl Into<String>, params: Option<Value>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
method: method.into(),
params,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct JsonRpcResponse {
pub(crate) jsonrpc: String,
pub(crate) id: RequestId,
pub(crate) result: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct JsonRpcError {
pub(crate) jsonrpc: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) id: Option<RequestId>,
pub(crate) error: ErrorData,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct JsonRpcNotification {
pub(crate) jsonrpc: String,
pub(crate) method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) params: Option<Value>,
}
impl JsonRpcNotification {
pub(crate) fn new(method: impl Into<String>, params: Option<Value>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
method: method.into(),
params,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub(crate) struct ErrorData {
pub(crate) code: i64,
pub(crate) message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) data: Option<Value>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum JsonRpcMessage {
Response(JsonRpcResponse),
Error(JsonRpcError),
Notification(JsonRpcNotification),
Request(JsonRpcRequest),
}
impl<'de> Deserialize<'de> for JsonRpcMessage {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
if value.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
return Err(de::Error::custom(
"JSON-RPC message missing jsonrpc \"2.0\"",
));
}
#[derive(Deserialize)]
#[serde(untagged)]
enum UntaggedMessage {
Response(JsonRpcResponse),
Error(JsonRpcError),
Notification(JsonRpcNotification),
Request(JsonRpcRequest),
}
match UntaggedMessage::deserialize(value).map_err(de::Error::custom)? {
UntaggedMessage::Response(message) => Ok(JsonRpcMessage::Response(message)),
UntaggedMessage::Error(message) => Ok(JsonRpcMessage::Error(message)),
UntaggedMessage::Notification(message) => Ok(JsonRpcMessage::Notification(message)),
UntaggedMessage::Request(message) => Ok(JsonRpcMessage::Request(message)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn request_id_round_trips_number_and_string() {
assert_eq!(
serde_json::from_value::<RequestId>(json!(7)).unwrap(),
RequestId::Number(7)
);
assert_eq!(
serde_json::from_value::<RequestId>(json!("abc")).unwrap(),
RequestId::String("abc".to_string())
);
}
#[test]
fn jsonrpc_request_serializes_2_0() {
let value = serde_json::to_value(JsonRpcRequest::new(
RequestId::Number(1),
"tools/list",
Some(json!({"cursor":"a"})),
))
.unwrap();
assert_eq!(value["jsonrpc"], "2.0");
assert_eq!(value["id"], 1);
assert_eq!(value["method"], "tools/list");
}
#[test]
fn mcp_jsonrpc_serialization_fixture_remains_newline_framing_agnostic() {
let request = JsonRpcRequest::new(
RequestId::Number(42),
"tools/call",
Some(json!({"name":"echo","arguments":{"text":"hi"}})),
);
let encoded = serde_json::to_string(&request).unwrap();
assert_eq!(
encoded,
r#"{"jsonrpc":"2.0","id":42,"method":"tools/call","params":{"arguments":{"text":"hi"},"name":"echo"}}"#
);
assert!(!encoded.contains("Content-Length"));
assert!(!encoded.contains('\n'));
}
#[test]
fn jsonrpc_error_deserializes() {
let error: JsonRpcMessage = serde_json::from_value(json!({
"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"missing"}
}))
.unwrap();
assert!(matches!(error, JsonRpcMessage::Error(_)));
}
#[test]
fn jsonrpc_message_rejects_wrong_version() {
let error = serde_json::from_value::<JsonRpcMessage>(json!({
"jsonrpc":"1.0","id":1,"result":{}
}))
.unwrap_err()
.to_string();
assert!(error.contains("jsonrpc"), "{error}");
}
#[test]
fn jsonrpc_message_rejects_missing_version() {
let error = serde_json::from_value::<JsonRpcMessage>(json!({
"id":1,"result":{}
}))
.unwrap_err()
.to_string();
assert!(error.contains("jsonrpc"), "{error}");
}
}