mcpkit_core/
protocol.rs

1//! JSON-RPC 2.0 protocol types for the Model Context Protocol.
2//!
3//! This module provides the foundational JSON-RPC 2.0 types used for all
4//! MCP communication. These types handle message framing, request/response
5//! correlation, and notification delivery.
6//!
7//! # Protocol Overview
8//!
9//! MCP uses JSON-RPC 2.0 as its transport protocol. All messages are one of:
10//!
11//! - **Request**: A method call expecting a response
12//! - **Response**: A reply to a request (success or error)
13//! - **Notification**: A one-way message with no response
14//!
15//! # Example
16//!
17//! ```rust
18//! use mcpkit_core::protocol::{Request, Response, RequestId};
19//!
20//! // Create a request
21//! let request = Request::new("tools/list", RequestId::Number(1));
22//!
23//! // Parse a response
24//! let json = r#"{"jsonrpc": "2.0", "id": 1, "result": {}}"#;
25//! let response: Response = serde_json::from_str(json).unwrap();
26//! ```
27
28use crate::error::JsonRpcError;
29use serde::{Deserialize, Serialize};
30use std::borrow::Cow;
31
32/// The JSON-RPC version string. Always "2.0".
33pub const JSONRPC_VERSION: &str = "2.0";
34
35/// A JSON-RPC request ID.
36///
37/// Request IDs are used to correlate requests with their responses.
38/// They can be either numbers or strings per the JSON-RPC 2.0 specification.
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(untagged)]
41pub enum RequestId {
42    /// Numeric request ID (most common).
43    Number(u64),
44    /// String request ID.
45    String(String),
46}
47
48impl RequestId {
49    /// Create a new numeric request ID.
50    #[must_use]
51    pub fn number(id: u64) -> Self {
52        Self::Number(id)
53    }
54
55    /// Create a new string request ID.
56    #[must_use]
57    pub fn string(id: impl Into<String>) -> Self {
58        Self::String(id.into())
59    }
60}
61
62impl From<u64> for RequestId {
63    fn from(id: u64) -> Self {
64        Self::Number(id)
65    }
66}
67
68impl From<String> for RequestId {
69    fn from(id: String) -> Self {
70        Self::String(id)
71    }
72}
73
74impl From<&str> for RequestId {
75    fn from(id: &str) -> Self {
76        Self::String(id.to_string())
77    }
78}
79
80impl std::fmt::Display for RequestId {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            Self::Number(n) => write!(f, "{n}"),
84            Self::String(s) => write!(f, "{s}"),
85        }
86    }
87}
88
89/// A JSON-RPC 2.0 request message.
90///
91/// Requests are method calls that expect a response. Each request has a unique
92/// ID that is echoed in the corresponding response.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct Request {
95    /// The JSON-RPC version. Always "2.0".
96    pub jsonrpc: Cow<'static, str>,
97    /// The request ID for correlation.
98    pub id: RequestId,
99    /// The method to invoke.
100    pub method: Cow<'static, str>,
101    /// The method parameters, if any.
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub params: Option<serde_json::Value>,
104}
105
106impl Request {
107    /// Create a new request with no parameters.
108    #[must_use]
109    pub fn new(method: impl Into<Cow<'static, str>>, id: impl Into<RequestId>) -> Self {
110        Self {
111            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
112            id: id.into(),
113            method: method.into(),
114            params: None,
115        }
116    }
117
118    /// Create a new request with parameters.
119    #[must_use]
120    pub fn with_params(
121        method: impl Into<Cow<'static, str>>,
122        id: impl Into<RequestId>,
123        params: serde_json::Value,
124    ) -> Self {
125        Self {
126            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
127            id: id.into(),
128            method: method.into(),
129            params: Some(params),
130        }
131    }
132
133    /// Set the parameters for this request.
134    #[must_use]
135    pub fn params(mut self, params: serde_json::Value) -> Self {
136        self.params = Some(params);
137        self
138    }
139
140    /// Get the method name.
141    #[must_use]
142    pub fn method(&self) -> &str {
143        &self.method
144    }
145}
146
147/// A JSON-RPC 2.0 response message.
148///
149/// Responses are sent in reply to requests. They contain either a result
150/// (on success) or an error (on failure), never both.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct Response {
153    /// The JSON-RPC version. Always "2.0".
154    pub jsonrpc: Cow<'static, str>,
155    /// The request ID this response corresponds to.
156    pub id: RequestId,
157    /// The result on success.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub result: Option<serde_json::Value>,
160    /// The error on failure.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub error: Option<JsonRpcError>,
163}
164
165impl Response {
166    /// Create a successful response.
167    #[must_use]
168    pub fn success(id: impl Into<RequestId>, result: serde_json::Value) -> Self {
169        Self {
170            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
171            id: id.into(),
172            result: Some(result),
173            error: None,
174        }
175    }
176
177    /// Create an error response.
178    #[must_use]
179    pub fn error(id: impl Into<RequestId>, error: JsonRpcError) -> Self {
180        Self {
181            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
182            id: id.into(),
183            result: None,
184            error: Some(error),
185        }
186    }
187
188    /// Check if this response indicates success.
189    #[must_use]
190    pub fn is_success(&self) -> bool {
191        self.result.is_some() && self.error.is_none()
192    }
193
194    /// Check if this response indicates an error.
195    #[must_use]
196    pub fn is_error(&self) -> bool {
197        self.error.is_some()
198    }
199
200    /// Get the result, consuming self.
201    ///
202    /// Returns `Err` if this was an error response.
203    pub fn into_result(self) -> Result<serde_json::Value, JsonRpcError> {
204        if let Some(error) = self.error {
205            Err(error)
206        } else {
207            self.result.ok_or_else(|| JsonRpcError {
208                code: -32603,
209                message: "Response contained neither result nor error".to_string(),
210                data: None,
211            })
212        }
213    }
214}
215
216/// A JSON-RPC 2.0 notification message.
217///
218/// Notifications are one-way messages that do not expect a response.
219/// They have no ID field.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct Notification {
222    /// The JSON-RPC version. Always "2.0".
223    pub jsonrpc: Cow<'static, str>,
224    /// The notification method.
225    pub method: Cow<'static, str>,
226    /// The notification parameters, if any.
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub params: Option<serde_json::Value>,
229}
230
231impl Notification {
232    /// Create a new notification with no parameters.
233    #[must_use]
234    pub fn new(method: impl Into<Cow<'static, str>>) -> Self {
235        Self {
236            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
237            method: method.into(),
238            params: None,
239        }
240    }
241
242    /// Create a new notification with parameters.
243    #[must_use]
244    pub fn with_params(
245        method: impl Into<Cow<'static, str>>,
246        params: serde_json::Value,
247    ) -> Self {
248        Self {
249            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
250            method: method.into(),
251            params: Some(params),
252        }
253    }
254
255    /// Set the parameters for this notification.
256    #[must_use]
257    pub fn params(mut self, params: serde_json::Value) -> Self {
258        self.params = Some(params);
259        self
260    }
261
262    /// Get the method name.
263    #[must_use]
264    pub fn method(&self) -> &str {
265        &self.method
266    }
267}
268
269/// A JSON-RPC 2.0 message (request, response, or notification).
270///
271/// This enum allows handling all message types uniformly during
272/// parsing and routing.
273#[derive(Debug, Clone, Serialize, Deserialize)]
274#[serde(untagged)]
275pub enum Message {
276    /// A request message.
277    Request(Request),
278    /// A response message.
279    Response(Response),
280    /// A notification message.
281    Notification(Notification),
282}
283
284impl Message {
285    /// Get the method name if this is a request or notification.
286    #[must_use]
287    pub fn method(&self) -> Option<&str> {
288        match self {
289            Self::Request(r) => Some(&r.method),
290            Self::Notification(n) => Some(&n.method),
291            Self::Response(_) => None,
292        }
293    }
294
295    /// Get the request ID if this is a request or response.
296    #[must_use]
297    pub fn id(&self) -> Option<&RequestId> {
298        match self {
299            Self::Request(r) => Some(&r.id),
300            Self::Response(r) => Some(&r.id),
301            Self::Notification(_) => None,
302        }
303    }
304
305    /// Check if this is a request.
306    #[must_use]
307    pub fn is_request(&self) -> bool {
308        matches!(self, Self::Request(_))
309    }
310
311    /// Check if this is a response.
312    #[must_use]
313    pub fn is_response(&self) -> bool {
314        matches!(self, Self::Response(_))
315    }
316
317    /// Check if this is a notification.
318    #[must_use]
319    pub fn is_notification(&self) -> bool {
320        matches!(self, Self::Notification(_))
321    }
322
323    /// Try to get this as a request.
324    #[must_use]
325    pub fn as_request(&self) -> Option<&Request> {
326        match self {
327            Self::Request(r) => Some(r),
328            _ => None,
329        }
330    }
331
332    /// Try to get this as a response.
333    #[must_use]
334    pub fn as_response(&self) -> Option<&Response> {
335        match self {
336            Self::Response(r) => Some(r),
337            _ => None,
338        }
339    }
340
341    /// Try to get this as a notification.
342    #[must_use]
343    pub fn as_notification(&self) -> Option<&Notification> {
344        match self {
345            Self::Notification(n) => Some(n),
346            _ => None,
347        }
348    }
349}
350
351impl From<Request> for Message {
352    fn from(r: Request) -> Self {
353        Self::Request(r)
354    }
355}
356
357impl From<Response> for Message {
358    fn from(r: Response) -> Self {
359        Self::Response(r)
360    }
361}
362
363impl From<Notification> for Message {
364    fn from(n: Notification) -> Self {
365        Self::Notification(n)
366    }
367}
368
369/// A progress token for tracking long-running operations.
370///
371/// Progress tokens are included in requests that may take a long time,
372/// allowing the server to send progress updates.
373#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
374#[serde(untagged)]
375pub enum ProgressToken {
376    /// Numeric progress token.
377    Number(u64),
378    /// String progress token.
379    String(String),
380}
381
382impl std::fmt::Display for ProgressToken {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        match self {
385            Self::Number(n) => write!(f, "{n}"),
386            Self::String(s) => write!(f, "{s}"),
387        }
388    }
389}
390
391/// A cursor for paginated results.
392///
393/// Cursors are opaque strings that represent a position in a paginated
394/// result set. Pass the cursor from a previous response to get the next page.
395#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
396#[serde(transparent)]
397pub struct Cursor(pub String);
398
399impl Cursor {
400    /// Create a new cursor.
401    #[must_use]
402    pub fn new(cursor: impl Into<String>) -> Self {
403        Self(cursor.into())
404    }
405}
406
407impl std::fmt::Display for Cursor {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        write!(f, "{}", self.0)
410    }
411}
412
413impl From<String> for Cursor {
414    fn from(s: String) -> Self {
415        Self(s)
416    }
417}
418
419impl From<&str> for Cursor {
420    fn from(s: &str) -> Self {
421        Self(s.to_string())
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn test_request_serialization() {
431        let request = Request::new("tools/list", 1u64);
432        let json = serde_json::to_string(&request).unwrap();
433        assert!(json.contains("\"jsonrpc\":\"2.0\""));
434        assert!(json.contains("\"method\":\"tools/list\""));
435        assert!(json.contains("\"id\":1"));
436    }
437
438    #[test]
439    fn test_request_with_params() {
440        let request = Request::with_params(
441            "tools/call",
442            1u64,
443            serde_json::json!({"name": "search", "arguments": {"query": "test"}}),
444        );
445        let json = serde_json::to_string(&request).unwrap();
446        assert!(json.contains("\"params\""));
447        assert!(json.contains("\"name\":\"search\""));
448    }
449
450    #[test]
451    fn test_response_success() {
452        let response = Response::success(1u64, serde_json::json!({"tools": []}));
453        assert!(response.is_success());
454        assert!(!response.is_error());
455
456        let result = response.into_result().unwrap();
457        assert!(result.get("tools").is_some());
458    }
459
460    #[test]
461    fn test_response_error() {
462        let error = JsonRpcError {
463            code: -32601,
464            message: "Method not found".to_string(),
465            data: None,
466        };
467        let response = Response::error(1u64, error);
468        assert!(!response.is_success());
469        assert!(response.is_error());
470
471        let err = response.into_result().unwrap_err();
472        assert_eq!(err.code, -32601);
473    }
474
475    #[test]
476    fn test_notification() {
477        let notification = Notification::with_params(
478            "notifications/progress",
479            serde_json::json!({"progress": 50, "total": 100}),
480        );
481        let json = serde_json::to_string(&notification).unwrap();
482        assert!(json.contains("\"method\":\"notifications/progress\""));
483        assert!(!json.contains("\"id\"")); // Notifications have no ID
484    }
485
486    #[test]
487    fn test_message_parsing() {
488        // Request
489        let json = r#"{"jsonrpc":"2.0","id":1,"method":"test"}"#;
490        let msg: Message = serde_json::from_str(json).unwrap();
491        assert!(msg.is_request());
492        assert_eq!(msg.method(), Some("test"));
493
494        // Response
495        let json = r#"{"jsonrpc":"2.0","id":1,"result":{}}"#;
496        let msg: Message = serde_json::from_str(json).unwrap();
497        assert!(msg.is_response());
498
499        // Notification
500        let json = r#"{"jsonrpc":"2.0","method":"notify"}"#;
501        let msg: Message = serde_json::from_str(json).unwrap();
502        assert!(msg.is_notification());
503    }
504
505    #[test]
506    fn test_request_id_types() {
507        // Number ID
508        let request = Request::new("test", 42u64);
509        let json = serde_json::to_string(&request).unwrap();
510        assert!(json.contains("\"id\":42"));
511
512        // String ID
513        let request = Request::new("test", "req-001");
514        let json = serde_json::to_string(&request).unwrap();
515        assert!(json.contains("\"id\":\"req-001\""));
516    }
517}