use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolDefinition {
pub name: String,
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCallResult {
pub content: Vec<McpContent>,
#[serde(rename = "isError", default)]
pub is_error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum McpContent {
Text {
text: String,
},
Image {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
},
Resource {
resource: McpResource,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpResource {
pub uri: String,
pub name: String,
pub description: Option<String>,
#[serde(rename = "mimeType")]
pub mime_type: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpResourceContent {
pub uri: String,
pub text: Option<String>,
pub blob: Option<String>,
#[serde(rename = "mimeType")]
pub mime_type: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct McpToolsListResult {
pub tools: Vec<McpToolDefinition>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct McpResourcesListResult {
pub resources: Vec<McpResource>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct McpResourceReadResult {
pub contents: Vec<McpResourceContent>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_tool_definition_roundtrip() {
let json = json!({
"name": "read_file",
"description": "Read a file's contents",
"inputSchema": {
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"]
}
});
let def: McpToolDefinition = serde_json::from_value(json.clone()).unwrap();
assert_eq!(def.name, "read_file");
assert_eq!(def.description.as_deref(), Some("Read a file's contents"));
let re = serde_json::to_value(&def).unwrap();
assert_eq!(re["name"], "read_file");
}
#[test]
fn test_tool_definition_no_description() {
let json = json!({
"name": "ping",
"inputSchema": { "type": "object" }
});
let def: McpToolDefinition = serde_json::from_value(json).unwrap();
assert_eq!(def.name, "ping");
assert!(def.description.is_none());
}
#[test]
fn test_content_text_deserialize() {
let json = json!({"type": "text", "text": "hello world"});
let content: McpContent = serde_json::from_value(json).unwrap();
match content {
McpContent::Text { text } => assert_eq!(text, "hello world"),
other => panic!("Expected Text, got {:?}", other),
}
}
#[test]
fn test_content_image_deserialize() {
let json = json!({
"type": "image",
"data": "aGVsbG8=",
"mimeType": "image/png"
});
let content: McpContent = serde_json::from_value(json).unwrap();
match content {
McpContent::Image { data, mime_type } => {
assert_eq!(data, "aGVsbG8=");
assert_eq!(mime_type, "image/png");
}
other => panic!("Expected Image, got {:?}", other),
}
}
#[test]
fn test_tool_call_result_success() {
let json = json!({
"content": [{"type": "text", "text": "done"}],
"isError": false
});
let result: McpToolCallResult = serde_json::from_value(json).unwrap();
assert!(!result.is_error);
assert_eq!(result.content.len(), 1);
}
#[test]
fn test_tool_call_result_error() {
let json = json!({
"content": [{"type": "text", "text": "file not found"}],
"isError": true
});
let result: McpToolCallResult = serde_json::from_value(json).unwrap();
assert!(result.is_error);
}
#[test]
fn test_tool_call_result_default_not_error() {
let json = json!({
"content": [{"type": "text", "text": "ok"}]
});
let result: McpToolCallResult = serde_json::from_value(json).unwrap();
assert!(!result.is_error, "isError should default to false");
}
#[test]
fn test_resource_roundtrip() {
let json = json!({
"uri": "file:///notes.txt",
"name": "notes.txt",
"description": "My notes",
"mimeType": "text/plain"
});
let resource: McpResource = serde_json::from_value(json).unwrap();
assert_eq!(resource.uri, "file:///notes.txt");
assert_eq!(resource.name, "notes.txt");
assert_eq!(resource.description.as_deref(), Some("My notes"));
assert_eq!(resource.mime_type.as_deref(), Some("text/plain"));
}
#[test]
fn test_resource_minimal() {
let json = json!({
"uri": "db://table/users",
"name": "users"
});
let resource: McpResource = serde_json::from_value(json).unwrap();
assert_eq!(resource.uri, "db://table/users");
assert!(resource.description.is_none());
assert!(resource.mime_type.is_none());
}
#[test]
fn test_tools_list_result() {
let json = json!({
"tools": [
{
"name": "tool_a",
"description": "Does A",
"inputSchema": { "type": "object" }
},
{
"name": "tool_b",
"inputSchema": { "type": "object" }
}
]
});
let result: McpToolsListResult = serde_json::from_value(json).unwrap();
assert_eq!(result.tools.len(), 2);
assert_eq!(result.tools[0].name, "tool_a");
assert_eq!(result.tools[1].name, "tool_b");
}
#[test]
fn test_resources_list_result() {
let json = json!({
"resources": [
{ "uri": "file:///a.txt", "name": "a.txt" },
{ "uri": "file:///b.txt", "name": "b.txt" }
]
});
let result: McpResourcesListResult = serde_json::from_value(json).unwrap();
assert_eq!(result.resources.len(), 2);
}
#[test]
fn test_resource_read_result() {
let json = json!({
"contents": [
{
"uri": "file:///a.txt",
"text": "hello",
"mimeType": "text/plain"
}
]
});
let result: McpResourceReadResult = serde_json::from_value(json).unwrap();
assert_eq!(result.contents.len(), 1);
assert_eq!(result.contents[0].uri, "file:///a.txt");
assert_eq!(result.contents[0].text.as_deref(), Some("hello"));
}
}