Skip to main content

batuta/mcp/
types.rs

1//! MCP Protocol Types
2//!
3//! JSON-RPC 2.0 types for Model Context Protocol communication.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8// ============================================================================
9// JSON-RPC 2.0 Types
10// ============================================================================
11
12/// JSON-RPC 2.0 request
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct JsonRpcRequest {
15    pub jsonrpc: String,
16    pub id: Option<serde_json::Value>,
17    pub method: String,
18    #[serde(default)]
19    pub params: serde_json::Value,
20}
21
22/// JSON-RPC 2.0 response
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct JsonRpcResponse {
25    pub jsonrpc: String,
26    pub id: Option<serde_json::Value>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub result: Option<serde_json::Value>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub error: Option<JsonRpcError>,
31}
32
33/// JSON-RPC 2.0 error
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JsonRpcError {
36    pub code: i64,
37    pub message: String,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub data: Option<serde_json::Value>,
40}
41
42impl JsonRpcResponse {
43    pub fn success(id: Option<serde_json::Value>, result: serde_json::Value) -> Self {
44        Self { jsonrpc: "2.0".to_string(), id, result: Some(result), error: None }
45    }
46
47    pub fn error(id: Option<serde_json::Value>, code: i64, message: impl Into<String>) -> Self {
48        Self {
49            jsonrpc: "2.0".to_string(),
50            id,
51            result: None,
52            error: Some(JsonRpcError { code, message: message.into(), data: None }),
53        }
54    }
55}
56
57// ============================================================================
58// MCP Protocol Types
59// ============================================================================
60
61/// MCP server capabilities
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ServerCapabilities {
64    pub tools: ToolsCapability,
65}
66
67/// Tools capability declaration
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ToolsCapability {
70    #[serde(rename = "listChanged")]
71    pub list_changed: bool,
72}
73
74/// MCP tool definition
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ToolDefinition {
77    pub name: String,
78    pub description: String,
79    #[serde(rename = "inputSchema")]
80    pub input_schema: InputSchema,
81}
82
83/// JSON Schema for tool input
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct InputSchema {
86    #[serde(rename = "type")]
87    pub schema_type: String,
88    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89    pub properties: HashMap<String, PropertySchema>,
90    #[serde(default, skip_serializing_if = "Vec::is_empty")]
91    pub required: Vec<String>,
92}
93
94/// Property schema for tool parameters
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct PropertySchema {
97    #[serde(rename = "type")]
98    pub prop_type: String,
99    pub description: String,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub r#enum: Option<Vec<String>>,
102}
103
104/// MCP tool call result
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ToolCallResult {
107    pub content: Vec<ContentBlock>,
108    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
109    pub is_error: Option<bool>,
110}
111
112/// Content block in tool result
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ContentBlock {
115    #[serde(rename = "type")]
116    pub content_type: String,
117    pub text: String,
118}
119
120impl ContentBlock {
121    pub fn text(content: impl Into<String>) -> Self {
122        Self { content_type: "text".to_string(), text: content.into() }
123    }
124}
125
126impl ToolCallResult {
127    pub fn success(text: impl Into<String>) -> Self {
128        Self { content: vec![ContentBlock::text(text)], is_error: None }
129    }
130
131    pub fn error(text: impl Into<String>) -> Self {
132        Self { content: vec![ContentBlock::text(text)], is_error: Some(true) }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_json_rpc_response_success() {
142        let resp = JsonRpcResponse::success(Some(serde_json::json!(1)), serde_json::json!("ok"));
143        assert!(resp.result.is_some());
144        assert!(resp.error.is_none());
145        assert_eq!(resp.jsonrpc, "2.0");
146    }
147
148    #[test]
149    fn test_json_rpc_response_error() {
150        let resp = JsonRpcResponse::error(Some(serde_json::json!(1)), -32600, "Invalid Request");
151        assert!(resp.result.is_none());
152        assert!(resp.error.is_some());
153        assert_eq!(resp.error.as_ref().map(|e| e.code), Some(-32600));
154    }
155
156    #[test]
157    fn test_tool_call_result_success() {
158        let result = ToolCallResult::success("hello");
159        assert_eq!(result.content.len(), 1);
160        assert_eq!(result.content[0].text, "hello");
161        assert!(result.is_error.is_none());
162    }
163
164    #[test]
165    fn test_tool_call_result_error() {
166        let result = ToolCallResult::error("fail");
167        assert!(result.is_error == Some(true));
168    }
169
170    #[test]
171    fn test_content_block_text() {
172        let block = ContentBlock::text("test");
173        assert_eq!(block.content_type, "text");
174        assert_eq!(block.text, "test");
175    }
176
177    #[test]
178    fn test_json_rpc_request_deserialize() {
179        let json = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#;
180        let req: JsonRpcRequest = serde_json::from_str(json).expect("json deserialize failed");
181        assert_eq!(req.method, "tools/list");
182        assert_eq!(req.jsonrpc, "2.0");
183    }
184
185    #[test]
186    fn test_input_schema_serialization() {
187        let schema = InputSchema {
188            schema_type: "object".to_string(),
189            properties: HashMap::new(),
190            required: vec![],
191        };
192        let json = serde_json::to_string(&schema).expect("json serialize failed");
193        assert!(json.contains("\"type\":\"object\""));
194    }
195}