Skip to main content

devboy_mcp/
protocol.rs

1//! MCP protocol types based on JSON-RPC 2.0.
2//!
3//! The Model Context Protocol uses JSON-RPC 2.0 for communication.
4//! This module defines the message types for request/response handling.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9pub const JSONRPC_VERSION: &str = "2.0";
10
11/// MCP protocol version.
12///
13/// Server echoes back the version it supports. Clients that send a newer version
14/// (e.g., "2025-11-25") should still be compatible with this version.
15pub const MCP_VERSION: &str = "2025-11-25";
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct JsonRpcRequest {
19    pub jsonrpc: String,
20    pub id: RequestId,
21    pub method: String,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub params: Option<Value>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct JsonRpcResponse {
28    pub jsonrpc: String,
29    pub id: RequestId,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub result: Option<Value>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub error: Option<JsonRpcError>,
34}
35
36/// JSON-RPC notification (no response expected).
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct JsonRpcNotification {
39    pub jsonrpc: String,
40    pub method: String,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub params: Option<Value>,
43}
44
45/// Request ID - can be string, number, or null.
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47#[serde(untagged)]
48pub enum RequestId {
49    String(String),
50    Number(i64),
51    Null,
52}
53
54/// JSON-RPC error object.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct JsonRpcError {
57    pub code: i32,
58    pub message: String,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub data: Option<Value>,
61}
62
63// Standard JSON-RPC error codes
64impl JsonRpcError {
65    pub const PARSE_ERROR: i32 = -32700;
66    pub const INVALID_REQUEST: i32 = -32600;
67    pub const METHOD_NOT_FOUND: i32 = -32601;
68    pub const INVALID_PARAMS: i32 = -32602;
69    pub const INTERNAL_ERROR: i32 = -32603;
70
71    pub fn parse_error(msg: &str) -> Self {
72        Self {
73            code: Self::PARSE_ERROR,
74            message: format!("Parse error: {}", msg),
75            data: None,
76        }
77    }
78
79    pub fn invalid_request(msg: &str) -> Self {
80        Self {
81            code: Self::INVALID_REQUEST,
82            message: format!("Invalid request: {}", msg),
83            data: None,
84        }
85    }
86
87    pub fn method_not_found(method: &str) -> Self {
88        Self {
89            code: Self::METHOD_NOT_FOUND,
90            message: format!("Method not found: {}", method),
91            data: None,
92        }
93    }
94
95    pub fn invalid_params(msg: &str) -> Self {
96        Self {
97            code: Self::INVALID_PARAMS,
98            message: format!("Invalid params: {}", msg),
99            data: None,
100        }
101    }
102
103    pub fn internal_error(msg: &str) -> Self {
104        Self {
105            code: Self::INTERNAL_ERROR,
106            message: format!("Internal error: {}", msg),
107            data: None,
108        }
109    }
110}
111
112impl JsonRpcResponse {
113    /// Create a successful response.
114    pub fn success(id: RequestId, result: Value) -> Self {
115        Self {
116            jsonrpc: JSONRPC_VERSION.to_string(),
117            id,
118            result: Some(result),
119            error: None,
120        }
121    }
122
123    /// Create an error response.
124    pub fn error(id: RequestId, error: JsonRpcError) -> Self {
125        Self {
126            jsonrpc: JSONRPC_VERSION.to_string(),
127            id,
128            result: None,
129            error: Some(error),
130        }
131    }
132}
133
134// ============================================================================
135// MCP-specific types
136// ============================================================================
137
138/// MCP initialization request params.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct InitializeParams {
142    pub protocol_version: String,
143    pub capabilities: ClientCapabilities,
144    pub client_info: ClientInfo,
145}
146
147#[derive(Debug, Clone, Default, Serialize, Deserialize)]
148pub struct ClientCapabilities {
149    #[serde(default)]
150    pub roots: Option<RootsCapability>,
151    #[serde(default)]
152    pub sampling: Option<SamplingCapability>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct RootsCapability {
157    #[serde(default)]
158    pub list_changed: bool,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct SamplingCapability {}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct ClientInfo {
166    pub name: String,
167    pub version: String,
168}
169
170/// MCP initialization response.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct InitializeResult {
174    pub protocol_version: String,
175    pub capabilities: ServerCapabilities,
176    pub server_info: ServerInfo,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ServerCapabilities {
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub tools: Option<ToolsCapability>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub resources: Option<ResourcesCapability>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub prompts: Option<PromptsCapability>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct ToolsCapability {
191    #[serde(default)]
192    pub list_changed: bool,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ResourcesCapability {
197    #[serde(default)]
198    pub subscribe: bool,
199    #[serde(default)]
200    pub list_changed: bool,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct PromptsCapability {
205    #[serde(default)]
206    pub list_changed: bool,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ServerInfo {
211    pub name: String,
212    pub version: String,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct ToolDefinition {
218    pub name: String,
219    pub description: String,
220    pub input_schema: Value,
221    /// Tool category for filtering (not serialized to JSON).
222    #[serde(skip)]
223    pub category: Option<devboy_core::ToolCategory>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ToolsListResult {
228    pub tools: Vec<ToolDefinition>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct ToolCallParams {
233    pub name: String,
234    #[serde(default)]
235    pub arguments: Option<Value>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct ToolCallResult {
241    pub content: Vec<ToolResultContent>,
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub is_error: Option<bool>,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
247#[serde(tag = "type")]
248pub enum ToolResultContent {
249    /// Text content block (the only content kind we emit today).
250    #[serde(rename = "text")]
251    Text {
252        /// The text payload returned to the caller.
253        text: String,
254    },
255}
256
257impl ToolCallResult {
258    /// Create a successful text result.
259    pub fn text(content: String) -> Self {
260        Self {
261            content: vec![ToolResultContent::Text { text: content }],
262            is_error: None,
263        }
264    }
265
266    /// Create an error result.
267    pub fn error(message: String) -> Self {
268        Self {
269            content: vec![ToolResultContent::Text { text: message }],
270            is_error: Some(true),
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_request_serialization() {
281        let req = JsonRpcRequest {
282            jsonrpc: JSONRPC_VERSION.to_string(),
283            id: RequestId::Number(1),
284            method: "initialize".to_string(),
285            params: Some(serde_json::json!({"test": true})),
286        };
287
288        let json = serde_json::to_string(&req).unwrap();
289        assert!(json.contains("\"jsonrpc\":\"2.0\""));
290        assert!(json.contains("\"id\":1"));
291    }
292
293    #[test]
294    fn test_response_success() {
295        let resp = JsonRpcResponse::success(
296            RequestId::String("abc".to_string()),
297            serde_json::json!({"result": "ok"}),
298        );
299
300        assert!(resp.error.is_none());
301        assert!(resp.result.is_some());
302    }
303
304    #[test]
305    fn test_response_error() {
306        let resp =
307            JsonRpcResponse::error(RequestId::Number(1), JsonRpcError::method_not_found("test"));
308
309        assert!(resp.result.is_none());
310        assert!(resp.error.is_some());
311        assert_eq!(resp.error.unwrap().code, JsonRpcError::METHOD_NOT_FOUND);
312    }
313
314    #[test]
315    fn test_tool_call_result() {
316        let result = ToolCallResult::text("Hello".to_string());
317        let json = serde_json::to_string(&result).unwrap();
318        assert!(json.contains("\"type\":\"text\""));
319        assert!(json.contains("\"text\":\"Hello\""));
320    }
321
322    #[test]
323    fn test_tool_call_result_error() {
324        let result = ToolCallResult::error("Something failed".to_string());
325        assert_eq!(result.is_error, Some(true));
326        let json = serde_json::to_string(&result).unwrap();
327        assert!(json.contains("Something failed"));
328    }
329
330    #[test]
331    fn test_parse_error() {
332        let err = JsonRpcError::parse_error("bad json");
333        assert_eq!(err.code, JsonRpcError::PARSE_ERROR);
334        assert!(err.message.contains("bad json"));
335        assert!(err.data.is_none());
336    }
337
338    #[test]
339    fn test_invalid_request_error() {
340        let err = JsonRpcError::invalid_request("not initialized");
341        assert_eq!(err.code, JsonRpcError::INVALID_REQUEST);
342        assert!(err.message.contains("not initialized"));
343    }
344
345    #[test]
346    fn test_invalid_params_error() {
347        let err = JsonRpcError::invalid_params("missing field");
348        assert_eq!(err.code, JsonRpcError::INVALID_PARAMS);
349        assert!(err.message.contains("missing field"));
350    }
351
352    #[test]
353    fn test_internal_error() {
354        let err = JsonRpcError::internal_error("unexpected");
355        assert_eq!(err.code, JsonRpcError::INTERNAL_ERROR);
356        assert!(err.message.contains("unexpected"));
357    }
358
359    #[test]
360    fn test_request_id_variants() {
361        let num = RequestId::Number(42);
362        let str_id = RequestId::String("abc".to_string());
363        let null = RequestId::Null;
364
365        assert_eq!(num, RequestId::Number(42));
366        assert_eq!(str_id, RequestId::String("abc".to_string()));
367        assert_eq!(null, RequestId::Null);
368
369        // Serialization
370        let json = serde_json::to_string(&num).unwrap();
371        assert_eq!(json, "42");
372
373        let json = serde_json::to_string(&str_id).unwrap();
374        assert_eq!(json, "\"abc\"");
375
376        let json = serde_json::to_string(&null).unwrap();
377        assert_eq!(json, "null");
378    }
379
380    #[test]
381    fn test_notification_serialization() {
382        let notif = JsonRpcNotification {
383            jsonrpc: JSONRPC_VERSION.to_string(),
384            method: "initialized".to_string(),
385            params: None,
386        };
387
388        let json = serde_json::to_string(&notif).unwrap();
389        assert!(json.contains("\"method\":\"initialized\""));
390        // params should be skipped when None
391        assert!(!json.contains("params"));
392    }
393}