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(¬ification).unwrap();
247
248 // Should not include "params" field when None
249 assert!(!json.contains("params"));
250 assert!(json.contains("notifications/initialized"));
251 }
252}