Skip to main content

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