use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::{base64::Base64, serde_as, skip_serializing_none};
pub const PROTOCOL_VERSION: &str = "2025-11-25";
pub use crate::jsonrpc::{
Body as JsonRpcBody, Error as JsonRpcError, Request as JsonRpcRequest,
Response as JsonRpcResponse, Version as JsonRpcVersion,
};
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerInfo {
pub name: String,
#[serde(default)]
pub version: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ServerCapabilities {
#[serde(default)]
pub tools: Option<Value>,
#[serde(default)]
pub resources: Option<Value>,
#[serde(default)]
pub prompts: Option<Value>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
pub protocol_version: String,
pub server_info: ServerInfo,
#[serde(default)]
pub capabilities: Option<ServerCapabilities>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolDefinition {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default = "default_object_schema")]
pub input_schema: Value,
#[serde(default)]
pub annotations: Option<ToolAnnotations>,
}
fn default_object_schema() -> Value {
serde_json::json!({"type": "object"})
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolAnnotations {
#[serde(default)]
pub read_only_hint: Option<bool>,
#[serde(default)]
pub idempotent_hint: Option<bool>,
#[serde(default)]
pub destructive_hint: Option<bool>,
#[serde(default)]
pub open_world_hint: Option<bool>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListToolsResult {
pub tools: Vec<ToolDefinition>,
#[serde(default)]
pub next_cursor: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolParams {
pub name: String,
#[serde(default)]
pub arguments: Option<Value>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResult {
pub content: Vec<ContentItem>,
#[serde(default)]
pub is_error: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ContentItem {
Text(TextContent),
Image(ImageContent),
Resource(ResourceContent),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextContent {
pub text: String,
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageContent {
#[serde_as(as = "Base64")]
pub data: Vec<u8>,
pub mime_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceContent {
pub resource: EmbeddedResource,
}
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmbeddedResource {
pub uri: String,
#[serde(default)]
pub mime_type: Option<String>,
#[serde(default)]
pub text: Option<String>,
#[serde_as(as = "Option<Base64>")]
#[serde(default)]
pub blob: Option<Vec<u8>>,
}
pub fn error_kind_to_jsonrpc_code(kind: &str) -> i32 {
use crate::constants::*;
match kind {
ERR_NOT_FOUND => -32601,
ERR_INVALID_ARGS => -32602,
ERR_INTERNAL => -32603,
_ => -32000,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tool_definition_deserialize() {
let json = json!({
"name": "get_weather",
"description": "Get weather",
"inputSchema": {
"type": "object",
"properties": { "city": { "type": "string" } }
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false
}
});
let tool: ToolDefinition = serde_json::from_value(json).unwrap();
assert_eq!(tool.name, "get_weather");
assert_eq!(tool.description.as_deref(), Some("Get weather"));
let ann = tool.annotations.unwrap();
assert_eq!(ann.read_only_hint, Some(true));
assert_eq!(ann.destructive_hint, Some(false));
assert_eq!(ann.idempotent_hint, None);
}
#[test]
fn tool_definition_minimal() {
let json = json!({ "name": "simple" });
let tool: ToolDefinition = serde_json::from_value(json).unwrap();
assert_eq!(tool.name, "simple");
assert_eq!(tool.input_schema, json!({"type": "object"}));
assert!(tool.annotations.is_none());
}
#[test]
fn tool_definition_omits_none_fields() {
let tool = ToolDefinition {
name: "x".to_string(),
description: None,
input_schema: default_object_schema(),
annotations: None,
};
let json = serde_json::to_string(&tool).unwrap();
assert!(!json.contains("\"description\""));
assert!(!json.contains("\"annotations\""));
}
#[test]
fn annotations_omits_none_hints() {
let ann = ToolAnnotations {
read_only_hint: Some(true),
..Default::default()
};
let json = serde_json::to_string(&ann).unwrap();
assert!(json.contains("readOnlyHint"));
assert!(!json.contains("idempotentHint"));
assert!(!json.contains("destructiveHint"));
assert!(!json.contains("openWorldHint"));
}
#[test]
fn content_item_text() {
let item: ContentItem = serde_json::from_value(json!({
"type": "text",
"text": "hello"
}))
.unwrap();
match item {
ContentItem::Text(t) => assert_eq!(t.text, "hello"),
_ => panic!("expected text"),
}
}
#[test]
fn content_item_image_roundtrip() {
let original = ImageContent {
data: b"\x89PNG\r\n".to_vec(),
mime_type: "image/png".to_string(),
};
let json = serde_json::to_value(&ContentItem::Image(original.clone())).unwrap();
assert_eq!(json["data"], "iVBORw0K");
assert_eq!(json["mimeType"], "image/png");
let item: ContentItem = serde_json::from_value(json).unwrap();
match item {
ContentItem::Image(i) => {
assert_eq!(i.data, b"\x89PNG\r\n");
assert_eq!(i.mime_type, "image/png");
}
_ => panic!("expected image"),
}
}
#[test]
fn content_item_resource_text() {
let item: ContentItem = serde_json::from_value(json!({
"type": "resource",
"resource": {
"uri": "file:///tmp/test.txt",
"text": "contents",
"mimeType": "text/plain"
}
}))
.unwrap();
match item {
ContentItem::Resource(r) => {
assert_eq!(r.resource.uri, "file:///tmp/test.txt");
assert_eq!(r.resource.text.as_deref(), Some("contents"));
assert!(r.resource.blob.is_none());
}
_ => panic!("expected resource"),
}
}
#[test]
fn content_item_resource_blob_roundtrip() {
let resource = EmbeddedResource {
uri: "file:///tmp/data.bin".to_string(),
mime_type: Some("application/octet-stream".to_string()),
text: None,
blob: Some(b"\x00\x01\x02".to_vec()),
};
let json = serde_json::to_value(&ResourceContent { resource }).unwrap();
assert_eq!(json["resource"]["blob"], "AAEC");
assert!(json["resource"].get("text").is_none());
let item: ContentItem = serde_json::from_value(json!({
"type": "resource",
"resource": {
"uri": "file:///tmp/data.bin",
"blob": "AAEC",
"mimeType": "application/octet-stream"
}
}))
.unwrap();
match item {
ContentItem::Resource(r) => {
assert_eq!(r.resource.blob.as_deref(), Some(b"\x00\x01\x02".as_slice()));
}
_ => panic!("expected resource"),
}
}
#[test]
fn call_tool_result_with_error() {
let result: CallToolResult = serde_json::from_value(json!({
"content": [{ "type": "text", "text": "oops" }],
"isError": true
}))
.unwrap();
assert_eq!(result.is_error, Some(true));
assert_eq!(result.content.len(), 1);
}
#[test]
fn call_tool_result_omits_is_error_when_none() {
let result = CallToolResult {
content: vec![],
is_error: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(!json.contains("isError"));
}
#[test]
fn call_tool_params_serialize() {
let params = CallToolParams {
name: "test".to_string(),
arguments: Some(json!({"key": "value"})),
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["name"], "test");
assert_eq!(json["arguments"]["key"], "value");
}
#[test]
fn initialize_result_serialize() {
let result = InitializeResult {
protocol_version: "2025-11-25".to_string(),
server_info: ServerInfo {
name: "test".to_string(),
version: Some("1.0".to_string()),
},
capabilities: Some(ServerCapabilities {
tools: Some(json!({})),
..Default::default()
}),
};
let json = serde_json::to_value(&result).unwrap();
assert_eq!(json["protocolVersion"], "2025-11-25");
assert_eq!(json["serverInfo"]["name"], "test");
assert!(json["capabilities"]["tools"].is_object());
assert!(json["capabilities"].get("resources").is_none());
}
}