use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Serialize)]
pub(crate) struct JsonRpcRequest {
pub jsonrpc: &'static str,
pub id: u64,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl JsonRpcRequest {
pub fn new(id: u64, method: impl Into<String>, params: Option<Value>) -> Self {
Self {
jsonrpc: "2.0",
id,
method: method.into(),
params,
}
}
}
#[derive(Debug, Serialize)]
pub(crate) struct JsonRpcNotification {
pub jsonrpc: &'static str,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl JsonRpcNotification {
pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
Self {
jsonrpc: "2.0",
method: method.into(),
params,
}
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct JsonRpcResponse {
#[allow(dead_code)]
pub id: u64,
pub result: Option<Value>,
pub error: Option<JsonRpcError>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct JsonRpcError {
pub code: i64,
pub message: String,
}
#[derive(Debug, Deserialize)]
pub struct McpToolDef {
pub name: String,
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
}
#[derive(Debug, Deserialize)]
pub(crate) struct ToolsListResult {
pub tools: Vec<McpToolDef>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct ToolCallResult {
pub content: Vec<ContentBlock>,
#[serde(default)]
pub is_error: bool,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
#[allow(dead_code)]
pub(crate) enum ContentBlock {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
},
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn serialize_request() {
let req = JsonRpcRequest::new(1, "tools/list", None);
let json = serde_json::to_value(&req).expect("serialization should succeed");
assert_eq!(json["jsonrpc"], "2.0");
assert_eq!(json["id"], 1);
assert_eq!(json["method"], "tools/list");
assert!(json.get("params").is_none());
}
#[test]
fn serialize_request_with_params() {
let params = json!({"name": "test", "arguments": {}});
let req = JsonRpcRequest::new(42, "tools/call", Some(params.clone()));
let json = serde_json::to_value(&req).expect("serialization should succeed");
assert_eq!(json["params"], params);
}
#[test]
fn serialize_notification() {
let notif = JsonRpcNotification::new("notifications/initialized", None);
let json = serde_json::to_value(¬if).expect("serialization should succeed");
assert_eq!(json["jsonrpc"], "2.0");
assert_eq!(json["method"], "notifications/initialized");
assert!(json.get("params").is_none());
}
#[test]
fn deserialize_response_success() {
let raw = json!({
"jsonrpc": "2.0",
"id": 1,
"result": {"tools": []}
});
let resp: JsonRpcResponse =
serde_json::from_value(raw).expect("deserialization should succeed");
assert_eq!(resp.id, 1);
assert!(resp.result.is_some());
assert!(resp.error.is_none());
}
#[test]
fn deserialize_response_error() {
let raw = json!({
"jsonrpc": "2.0",
"id": 2,
"error": {"code": -32601, "message": "Method not found"}
});
let resp: JsonRpcResponse =
serde_json::from_value(raw).expect("deserialization should succeed");
assert_eq!(resp.id, 2);
assert!(resp.result.is_none());
let err = resp.error.expect("error should be present");
assert_eq!(err.code, -32601);
assert_eq!(err.message, "Method not found");
}
#[test]
fn deserialize_tool_def() {
let raw = json!({
"name": "query_dashboards",
"description": "Query Grafana dashboards",
"inputSchema": {
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}
});
let def: McpToolDef = serde_json::from_value(raw).expect("deserialization should succeed");
assert_eq!(def.name, "query_dashboards");
assert_eq!(def.description.as_deref(), Some("Query Grafana dashboards"));
assert_eq!(def.input_schema["type"], "object");
}
#[test]
fn deserialize_tool_def_no_description() {
let raw = json!({
"name": "minimal",
"inputSchema": {"type": "object", "properties": {}}
});
let def: McpToolDef = serde_json::from_value(raw).expect("deserialization should succeed");
assert_eq!(def.name, "minimal");
assert!(def.description.is_none());
}
#[test]
fn deserialize_tools_list_result() {
let raw = json!({
"tools": [
{
"name": "tool_a",
"description": "Tool A",
"inputSchema": {"type": "object", "properties": {}}
},
{
"name": "tool_b",
"inputSchema": {"type": "object", "properties": {}}
}
]
});
let result: ToolsListResult =
serde_json::from_value(raw).expect("deserialization should succeed");
assert_eq!(result.tools.len(), 2);
assert_eq!(result.tools[0].name, "tool_a");
assert_eq!(result.tools[1].name, "tool_b");
}
#[test]
fn deserialize_tool_call_result_text() {
let raw = json!({
"content": [
{"type": "text", "text": "Hello world"}
]
});
let result: ToolCallResult =
serde_json::from_value(raw).expect("deserialization should succeed");
assert_eq!(result.content.len(), 1);
assert!(!result.is_error);
match &result.content[0] {
ContentBlock::Text { text } => assert_eq!(text, "Hello world"),
_ => panic!("expected text content block"),
}
}
#[test]
fn deserialize_tool_call_result_error() {
let raw = json!({
"content": [
{"type": "text", "text": "something went wrong"}
],
"is_error": true
});
let result: ToolCallResult =
serde_json::from_value(raw).expect("deserialization should succeed");
assert!(result.is_error);
}
#[test]
fn deserialize_tool_call_result_image() {
let raw = json!({
"content": [
{"type": "image", "data": "aWNvbg==", "mimeType": "image/png"}
]
});
let result: ToolCallResult =
serde_json::from_value(raw).expect("deserialization should succeed");
match &result.content[0] {
ContentBlock::Image { data, mime_type } => {
assert_eq!(data, "aWNvbg==");
assert_eq!(mime_type, "image/png");
}
_ => panic!("expected image content block"),
}
}
#[test]
fn deserialize_tool_call_result_mixed_content() {
let raw = json!({
"content": [
{"type": "text", "text": "Result:"},
{"type": "text", "text": "42"}
]
});
let result: ToolCallResult =
serde_json::from_value(raw).expect("deserialization should succeed");
assert_eq!(result.content.len(), 2);
}
}