Skip to main content

model_context_protocol/
protocol.rs

1//! MCP JSON-RPC Protocol Types
2//!
3//! This module defines the core JSON-RPC 2.0 protocol types used for
4//! communicating with Model Context Protocol (MCP) servers.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::fmt;
9
10/// Current MCP protocol version.
11pub const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
12
13/// JSON-RPC 2.0 Request
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct JsonRpcRequest {
16    pub jsonrpc: String,
17    pub id: JsonRpcId,
18    pub method: String,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub params: Option<Value>,
21}
22
23impl JsonRpcRequest {
24    pub fn new(id: JsonRpcId, method: impl Into<String>, params: Option<Value>) -> Self {
25        Self {
26            jsonrpc: "2.0".to_string(),
27            id,
28            method: method.into(),
29            params,
30        }
31    }
32}
33
34/// JSON-RPC 2.0 Response
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct JsonRpcResponse {
37    pub jsonrpc: String,
38    pub id: JsonRpcId,
39    #[serde(flatten)]
40    pub payload: JsonRpcPayload,
41}
42
43impl JsonRpcResponse {
44    /// Create a success response.
45    pub fn success(id: impl Into<JsonRpcId>, result: Value) -> Self {
46        Self {
47            jsonrpc: "2.0".to_string(),
48            id: id.into(),
49            payload: JsonRpcPayload::Success { result },
50        }
51    }
52
53    /// Create an error response.
54    pub fn error(
55        id: impl Into<JsonRpcId>,
56        code: i32,
57        message: impl Into<String>,
58        data: Option<Value>,
59    ) -> Self {
60        Self {
61            jsonrpc: "2.0".to_string(),
62            id: id.into(),
63            payload: JsonRpcPayload::Error {
64                error: JsonRpcError {
65                    code,
66                    message: message.into(),
67                    data,
68                },
69            },
70        }
71    }
72
73    /// Get the result if this is a success response.
74    pub fn result(&self) -> Option<&Value> {
75        match &self.payload {
76            JsonRpcPayload::Success { result } => Some(result),
77            JsonRpcPayload::Error { .. } => None,
78        }
79    }
80
81    /// Get the error if this is an error response.
82    pub fn error_info(&self) -> Option<&JsonRpcError> {
83        match &self.payload {
84            JsonRpcPayload::Success { .. } => None,
85            JsonRpcPayload::Error { error } => Some(error),
86        }
87    }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(untagged)]
92pub enum JsonRpcPayload {
93    Success { result: Value },
94    Error { error: JsonRpcError },
95}
96
97/// JSON-RPC 2.0 Error
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct JsonRpcError {
100    pub code: i32,
101    pub message: String,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub data: Option<Value>,
104}
105
106impl fmt::Display for JsonRpcError {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "JSON-RPC Error {}: {}", self.code, self.message)
109    }
110}
111
112impl std::error::Error for JsonRpcError {}
113
114/// Standard JSON-RPC error codes
115pub mod error_codes {
116    /// Invalid JSON was received by the server
117    pub const PARSE_ERROR: i32 = -32700;
118    /// The JSON sent is not a valid Request object
119    pub const INVALID_REQUEST: i32 = -32600;
120    /// The method does not exist / is not available
121    pub const METHOD_NOT_FOUND: i32 = -32601;
122    /// Invalid method parameter(s)
123    pub const INVALID_PARAMS: i32 = -32602;
124    /// Internal JSON-RPC error
125    pub const INTERNAL_ERROR: i32 = -32603;
126}
127
128// =============================================================================
129// MCP Server Types
130// =============================================================================
131
132/// MCP server information.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct McpServerInfo {
135    pub name: String,
136    pub version: String,
137}
138
139/// MCP server capabilities.
140#[derive(Debug, Clone, Serialize, Deserialize, Default)]
141pub struct McpCapabilities {
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub tools: Option<Value>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub resources: Option<Value>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub prompts: Option<Value>,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub logging: Option<Value>,
150}
151
152// =============================================================================
153// JSON-RPC ID
154// =============================================================================
155
156/// JSON-RPC ID can be string, number, or null
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
158#[serde(untagged)]
159pub enum JsonRpcId {
160    String(String),
161    Number(i64),
162    Null,
163}
164
165impl From<i64> for JsonRpcId {
166    fn from(n: i64) -> Self {
167        JsonRpcId::Number(n)
168    }
169}
170
171impl From<String> for JsonRpcId {
172    fn from(s: String) -> Self {
173        JsonRpcId::String(s)
174    }
175}
176
177impl From<&str> for JsonRpcId {
178    fn from(s: &str) -> Self {
179        JsonRpcId::String(s.to_string())
180    }
181}
182
183impl From<Value> for JsonRpcId {
184    fn from(v: Value) -> Self {
185        match v {
186            Value::String(s) => JsonRpcId::String(s),
187            Value::Number(n) => JsonRpcId::Number(n.as_i64().unwrap_or(0)),
188            Value::Null => JsonRpcId::Null,
189            _ => JsonRpcId::Null,
190        }
191    }
192}
193
194impl From<JsonRpcId> for Value {
195    fn from(id: JsonRpcId) -> Self {
196        match id {
197            JsonRpcId::String(s) => Value::String(s),
198            JsonRpcId::Number(n) => Value::Number(n.into()),
199            JsonRpcId::Null => Value::Null,
200        }
201    }
202}
203
204impl fmt::Display for JsonRpcId {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        match self {
207            JsonRpcId::String(s) => write!(f, "{}", s),
208            JsonRpcId::Number(n) => write!(f, "{}", n),
209            JsonRpcId::Null => write!(f, "null"),
210        }
211    }
212}
213
214// =============================================================================
215// MCP Tool Types
216// =============================================================================
217
218/// MCP Tool Definition (from tools/list response)
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct McpToolDef {
221    pub name: String,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub description: Option<String>,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub group: Option<String>,
226    #[serde(default = "default_input_schema")]
227    pub input_schema: Value,
228}
229
230fn default_input_schema() -> Value {
231    serde_json::json!({"type": "object"})
232}
233
234impl McpToolDef {
235    /// Create a new tool definition.
236    pub fn new(name: impl Into<String>) -> Self {
237        Self {
238            name: name.into(),
239            description: None,
240            group: None,
241            input_schema: default_input_schema(),
242        }
243    }
244
245    /// Set the group.
246    pub fn with_group(mut self, group: impl Into<String>) -> Self {
247        self.group = Some(group.into());
248        self
249    }
250
251    /// Set the description.
252    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
253        self.description = Some(desc.into());
254        self
255    }
256
257    /// Set the input schema.
258    pub fn with_schema(mut self, schema: Value) -> Self {
259        self.input_schema = schema;
260        self
261    }
262}
263
264/// JSON Schema for tool inputs (legacy format)
265#[derive(Debug, Clone, Serialize, Deserialize, Default)]
266pub struct ToolInputSchema {
267    #[serde(rename = "type")]
268    pub schema_type: String,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub properties: Option<Value>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub required: Option<Vec<String>>,
273}
274
275/// MCP tools/list request parameters
276#[derive(Debug, Clone, Serialize, Deserialize, Default)]
277pub struct ListToolsParams {
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub cursor: Option<String>,
280}
281
282/// MCP tools/list response
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct ListToolsResult {
285    pub tools: Vec<McpToolDef>,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub next_cursor: Option<String>,
288}
289
290/// MCP tools/call request parameters
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct CallToolParams {
293    pub name: String,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub arguments: Option<Value>,
296}
297
298/// MCP tools/call response
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct CallToolResult {
301    #[serde(default)]
302    pub content: Vec<ToolContent>,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub is_error: Option<bool>,
305}
306
307/// Tool execution content (text or other types)
308#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(tag = "type")]
310pub enum ToolContent {
311    #[serde(rename = "text")]
312    Text { text: String },
313    #[serde(rename = "image")]
314    Image { data: String, mime_type: String },
315    #[serde(rename = "resource")]
316    Resource {
317        uri: String,
318        mime_type: Option<String>,
319    },
320}
321
322impl ToolContent {
323    /// Create a text content item.
324    pub fn text(text: impl Into<String>) -> Self {
325        Self::Text { text: text.into() }
326    }
327
328    /// Create an image content item.
329    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
330        Self::Image {
331            data: data.into(),
332            mime_type: mime_type.into(),
333        }
334    }
335
336    /// Create a resource content item.
337    pub fn resource(uri: impl Into<String>, mime_type: Option<String>) -> Self {
338        Self::Resource {
339            uri: uri.into(),
340            mime_type,
341        }
342    }
343
344    /// Get text content if this is a text item.
345    pub fn as_text(&self) -> Option<&str> {
346        match self {
347            Self::Text { text } => Some(text),
348            _ => None,
349        }
350    }
351
352    /// Check if this is an error indicator.
353    pub fn is_text(&self) -> bool {
354        matches!(self, Self::Text { .. })
355    }
356}
357
358/// Tool definition for LLM providers.
359///
360/// This is a simplified tool definition that can be used by LLM providers
361/// to understand available tools.
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct ToolDefinition {
364    pub name: String,
365    pub description: String,
366    pub parameters: Value,
367}
368
369impl From<McpToolDef> for ToolDefinition {
370    fn from(tool: McpToolDef) -> Self {
371        Self {
372            name: tool.name,
373            description: tool.description.unwrap_or_default(),
374            parameters: serde_json::to_value(&tool.input_schema).unwrap_or_default(),
375        }
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[test]
384    fn test_jsonrpc_request_serialization() {
385        let req = JsonRpcRequest::new(JsonRpcId::Number(1), "tools/list", None);
386
387        let json = serde_json::to_string(&req).unwrap();
388        assert!(json.contains(r#""jsonrpc":"2.0""#));
389        assert!(json.contains(r#""id":1"#));
390        assert!(json.contains(r#""method":"tools/list""#));
391    }
392
393    #[test]
394    fn test_jsonrpc_response_success() {
395        let json = r#"{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}"#;
396        let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
397
398        assert!(matches!(resp.payload, JsonRpcPayload::Success { .. }));
399    }
400
401    #[test]
402    fn test_jsonrpc_response_error() {
403        let json =
404            r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}"#;
405        let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
406
407        assert!(matches!(resp.payload, JsonRpcPayload::Error { .. }));
408    }
409
410    #[test]
411    fn test_tool_content_text() {
412        let content = ToolContent::text("Hello, world!");
413        assert_eq!(content.as_text(), Some("Hello, world!"));
414
415        let json = serde_json::to_string(&content).unwrap();
416        assert!(json.contains(r#""type":"text""#));
417        assert!(json.contains(r#""text":"Hello, world!""#));
418    }
419
420    #[test]
421    fn test_jsonrpc_id_display() {
422        assert_eq!(JsonRpcId::Number(42).to_string(), "42");
423        assert_eq!(JsonRpcId::String("test".to_string()).to_string(), "test");
424        assert_eq!(JsonRpcId::Null.to_string(), "null");
425    }
426
427    #[test]
428    fn test_mcp_tool_deserialization() {
429        let json = r#"{
430            "name": "test_tool",
431            "description": "A test tool",
432            "input_schema": {
433                "type": "object",
434                "properties": {
435                    "name": {"type": "string"}
436                },
437                "required": ["name"]
438            }
439        }"#;
440
441        let tool: McpToolDef = serde_json::from_str(json).unwrap();
442        assert_eq!(tool.name, "test_tool");
443        assert_eq!(tool.description, Some("A test tool".to_string()));
444        assert_eq!(tool.input_schema["type"], "object");
445    }
446}