Skip to main content

nika_mcp/
protocol.rs

1//! JSON-RPC 2.0 Protocol Types for MCP
2//!
3//! This module provides the core JSON-RPC 2.0 types used for MCP communication:
4//! - [`JsonRpcRequest`]: Outgoing request to MCP server
5//! - [`JsonRpcResponse`]: Incoming response from MCP server
6//! - [`JsonRpcError`]: Error object in failed responses
7//!
8//! ## Protocol Overview
9//!
10//! MCP uses JSON-RPC 2.0 over stdio. Each message is a JSON object:
11//!
12//! ```json
13//! // Request
14//! {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {...}}
15//!
16//! // Success Response
17//! {"jsonrpc": "2.0", "id": 1, "result": {...}}
18//!
19//! // Error Response
20//! {"jsonrpc": "2.0", "id": 1, "error": {"code": -32600, "message": "..."}}
21//! ```
22//!
23//! ## Usage
24//!
25//! ```rust,ignore
26//! use nika_mcp::{JsonRpcRequest, JsonRpcResponse};
27//! use serde_json::json;
28//!
29//! // Create a request
30//! let request = JsonRpcRequest::new(1, "tools/call", json!({
31//!     "name": "novanet_context",
32//!     "arguments": {"mode": "page", "focus_key": "qr-code", "locale": "fr-FR"}
33//! }));
34//!
35//! // Serialize and send
36//! let json = serde_json::to_string(&request)?;
37//!
38//! // Parse response
39//! let response: JsonRpcResponse = serde_json::from_str(&response_str)?;
40//! if response.is_success() {
41//!     let result = response.result.unwrap();
42//! }
43//! ```
44
45use serde::{Deserialize, Serialize};
46use serde_json::Value;
47
48/// JSON-RPC 2.0 Request.
49///
50/// Sent to an MCP server to invoke a method (e.g., `initialize`, `tools/call`).
51#[derive(Debug, Serialize)]
52pub struct JsonRpcRequest {
53    /// Protocol version - always "2.0"
54    pub jsonrpc: &'static str,
55
56    /// Request ID - used to correlate responses
57    pub id: u64,
58
59    /// Method name (e.g., "initialize", "tools/call", "resources/read")
60    pub method: String,
61
62    /// Method parameters
63    pub params: Value,
64}
65
66impl JsonRpcRequest {
67    /// Create a new JSON-RPC request.
68    ///
69    /// # Arguments
70    ///
71    /// * `id` - Unique request ID for response correlation
72    /// * `method` - Method name to invoke
73    /// * `params` - Method parameters as JSON value
74    ///
75    /// # Example
76    ///
77    /// ```rust,ignore
78    /// let request = JsonRpcRequest::new(1, "tools/list", json!({}));
79    /// ```
80    pub fn new(id: u64, method: &str, params: Value) -> Self {
81        Self {
82            jsonrpc: "2.0",
83            id,
84            method: method.to_string(),
85            params,
86        }
87    }
88}
89
90/// JSON-RPC 2.0 Notification.
91///
92/// A notification is a request without an ID - the server should not respond.
93/// Used for one-way messages like `notifications/initialized`.
94#[derive(Debug, Serialize)]
95pub struct JsonRpcNotification {
96    /// Protocol version - always "2.0"
97    pub jsonrpc: &'static str,
98
99    /// Method name (e.g., "notifications/initialized")
100    pub method: String,
101
102    /// Method parameters (optional)
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub params: Option<Value>,
105}
106
107impl JsonRpcNotification {
108    /// Create a new JSON-RPC notification.
109    ///
110    /// # Arguments
111    ///
112    /// * `method` - Notification method name
113    ///
114    /// # Example
115    ///
116    /// ```rust,ignore
117    /// let notification = JsonRpcNotification::new("notifications/initialized");
118    /// ```
119    pub fn new(method: &str) -> Self {
120        Self {
121            jsonrpc: "2.0",
122            method: method.to_string(),
123            params: None,
124        }
125    }
126
127    /// Create a notification with parameters.
128    pub fn with_params(method: &str, params: Value) -> Self {
129        Self {
130            jsonrpc: "2.0",
131            method: method.to_string(),
132            params: Some(params),
133        }
134    }
135}
136
137/// JSON-RPC 2.0 Response.
138///
139/// Received from an MCP server after a request. Contains either a result or an error.
140#[derive(Debug, Deserialize)]
141pub struct JsonRpcResponse {
142    /// Protocol version - should be "2.0"
143    pub jsonrpc: String,
144
145    /// Request ID this response corresponds to (null for notifications)
146    #[serde(default)]
147    pub id: Option<u64>,
148
149    /// Successful result (mutually exclusive with error)
150    #[serde(default)]
151    pub result: Option<Value>,
152
153    /// Error information (mutually exclusive with result)
154    #[serde(default)]
155    pub error: Option<JsonRpcError>,
156}
157
158impl JsonRpcResponse {
159    /// Check if the response indicates success.
160    ///
161    /// A response is successful if it has a result and no error.
162    /// Note: a null result is still considered success.
163    pub fn is_success(&self) -> bool {
164        self.result.is_some() && self.error.is_none()
165    }
166}
167
168/// JSON-RPC 2.0 Error object.
169///
170/// Returned in the `error` field of a response when the request fails.
171///
172/// ## Standard Error Codes
173///
174/// | Code | Message |
175/// |------|---------|
176/// | -32700 | Parse error |
177/// | -32600 | Invalid Request |
178/// | -32601 | Method not found |
179/// | -32602 | Invalid params |
180/// | -32603 | Internal error |
181#[derive(Debug, Deserialize)]
182pub struct JsonRpcError {
183    /// Error code (negative integer per JSON-RPC spec)
184    pub code: i32,
185
186    /// Human-readable error message
187    pub message: String,
188
189    /// Additional error data (optional, implementation-defined)
190    #[serde(default)]
191    pub data: Option<Value>,
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use serde_json::json;
198
199    #[test]
200    fn test_request_new() {
201        let request = JsonRpcRequest::new(1, "test", json!({}));
202
203        assert_eq!(request.jsonrpc, "2.0");
204        assert_eq!(request.id, 1);
205        assert_eq!(request.method, "test");
206    }
207
208    #[test]
209    fn test_response_is_success() {
210        let json_str = r#"{"jsonrpc": "2.0", "id": 1, "result": {}}"#;
211        let response: JsonRpcResponse = serde_json::from_str(json_str).unwrap();
212
213        assert!(response.is_success());
214    }
215
216    #[test]
217    fn test_response_is_not_success_on_error() {
218        let json_str = r#"{"jsonrpc": "2.0", "id": 1, "error": {"code": -1, "message": "fail"}}"#;
219        let response: JsonRpcResponse = serde_json::from_str(json_str).unwrap();
220
221        assert!(!response.is_success());
222    }
223
224    #[test]
225    fn test_notification_new() {
226        let notification = JsonRpcNotification::new("notifications/initialized");
227
228        assert_eq!(notification.jsonrpc, "2.0");
229        assert_eq!(notification.method, "notifications/initialized");
230        assert!(notification.params.is_none());
231    }
232
233    #[test]
234    fn test_notification_with_params() {
235        let notification =
236            JsonRpcNotification::with_params("notifications/test", json!({"key": "value"}));
237
238        assert_eq!(notification.jsonrpc, "2.0");
239        assert_eq!(notification.method, "notifications/test");
240        assert!(notification.params.is_some());
241    }
242
243    #[test]
244    fn test_notification_serializes_without_params() {
245        let notification = JsonRpcNotification::new("notifications/initialized");
246        let json = serde_json::to_string(&notification).unwrap();
247
248        // Should not include "params" field when None
249        assert!(!json.contains("params"));
250        assert!(json.contains("notifications/initialized"));
251    }
252}