use serde::{Deserialize, Serialize};
use serde_json::Value;
pub(crate) const PROTOCOL_VERSION: &str = "2025-03-26";
#[allow(dead_code)]
pub(crate) const DRAFT_2025_03_26: &str = PROTOCOL_VERSION;
pub(crate) const METHOD_INITIALIZE: &str = "initialize";
pub(crate) const METHOD_INITIALIZED: &str = "notifications/initialized";
pub(crate) const METHOD_TOOLS_LIST: &str = "tools/list";
pub(crate) const METHOD_TOOLS_CALL: &str = "tools/call";
#[allow(dead_code)]
pub(crate) const METHOD_TOOLS_LIST_CHANGED: &str = "notifications/tools/list_changed";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct Implementation {
pub(crate) name: String,
pub(crate) version: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct ClientCapabilities {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ServerCapabilities {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) tools: Option<ToolsCapability>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ToolsCapability {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) list_changed: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct InitializeRequestParams {
pub(crate) protocol_version: String,
pub(crate) capabilities: ClientCapabilities,
pub(crate) client_info: Implementation,
}
impl Default for InitializeRequestParams {
fn default() -> Self {
Self {
protocol_version: PROTOCOL_VERSION.to_string(),
capabilities: ClientCapabilities::default(),
client_info: Implementation {
name: "magi-code".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct InitializeResult {
pub(crate) protocol_version: String,
pub(crate) capabilities: ServerCapabilities,
pub(crate) server_info: Implementation,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) instructions: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Tool {
pub(crate) name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) description: Option<String>,
#[serde(default = "default_input_schema")]
pub(crate) input_schema: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) output_schema: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) annotations: Option<ToolAnnotations>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ToolAnnotations {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) read_only_hint: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) destructive_hint: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) idempotent_hint: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) open_world_hint: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ListToolsParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ListToolsResult {
pub(crate) tools: Vec<Tool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CallToolParams {
pub(crate) name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) arguments: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CallToolResult {
pub(crate) content: Vec<ContentBlock>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) structured_content: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) is_error: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
pub(crate) enum ContentBlock {
Text { text: String },
Image { data: String, mime_type: String },
Audio { data: String, mime_type: String },
Resource { resource: ResourceContents },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ResourceContents {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) uri: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) mime_type: Option<String>,
#[serde(flatten)]
pub(crate) extra: serde_json::Map<String, Value>,
}
fn default_input_schema() -> Value {
serde_json::json!({"type":"object","properties":{}})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn initialize_params_serialize_camel_case_empty_capabilities() {
let value = serde_json::to_value(InitializeRequestParams::default()).unwrap();
assert_eq!(value["protocolVersion"], "2025-03-26");
assert_eq!(value["capabilities"], json!({}));
assert_eq!(value["clientInfo"]["name"], "magi-code");
}
#[test]
fn tool_round_trips_camel_case_schema_fields() {
let tool: Tool = serde_json::from_value(json!({
"name":"echo",
"title":"Echo",
"description":"Echo input",
"inputSchema":{"type":"object"},
"outputSchema":{"type":"object"},
"annotations":{"readOnlyHint":true,"openWorldHint":false}
}))
.unwrap();
assert_eq!(tool.input_schema, json!({"type":"object"}));
assert_eq!(
tool.annotations.as_ref().unwrap().read_only_hint,
Some(true)
);
assert!(
serde_json::to_value(tool)
.unwrap()
.get("inputSchema")
.is_some()
);
}
#[test]
fn call_tool_result_deserializes_content_blocks() {
let result: CallToolResult = serde_json::from_value(json!({
"content":[
{"type":"text","text":"ok"},
{"type":"image","data":"aW1n","mimeType":"image/png"},
{"type":"audio","data":"YXVkaW8=","mimeType":"audio/wav"},
{"type":"resource","resource":{"uri":"file:///tmp/a.txt","mimeType":"text/plain","text":"body"}}
],
"structuredContent":{"ok":true},
"isError":false
}))
.unwrap();
assert!(matches!(result.content[0], ContentBlock::Text { .. }));
assert!(matches!(
&result.content[1],
ContentBlock::Image { data, mime_type } if data == "aW1n" && mime_type == "image/png"
));
assert!(matches!(
&result.content[2],
ContentBlock::Audio { data, mime_type } if data == "YXVkaW8=" && mime_type == "audio/wav"
));
assert!(matches!(
&result.content[3],
ContentBlock::Resource { resource } if resource.uri.as_deref() == Some("file:///tmp/a.txt") && resource.mime_type.as_deref() == Some("text/plain")
));
let value = serde_json::to_value(result).unwrap();
assert_eq!(value["content"][1]["mimeType"], "image/png");
assert_eq!(value["content"][2]["mimeType"], "audio/wav");
assert_eq!(value["content"][3]["resource"]["mimeType"], "text/plain");
}
#[test]
fn missing_input_schema_defaults_to_empty_object_schema() {
let tool: Tool = serde_json::from_value(json!({"name":"no_schema"})).unwrap();
assert_eq!(tool.input_schema, json!({"type":"object","properties":{}}));
}
}