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 const 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 const 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 const 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(method: impl Into<Cow<'static, str>>, params: serde_json::Value) -> Self {
245        Self {
246            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
247            method: method.into(),
248            params: Some(params),
249        }
250    }
251
252    /// Set the parameters for this notification.
253    #[must_use]
254    pub fn params(mut self, params: serde_json::Value) -> Self {
255        self.params = Some(params);
256        self
257    }
258
259    /// Get the method name.
260    #[must_use]
261    pub fn method(&self) -> &str {
262        &self.method
263    }
264}
265
266/// A JSON-RPC 2.0 message (request, response, or notification).
267///
268/// This enum allows handling all message types uniformly during
269/// parsing and routing.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(untagged)]
272pub enum Message {
273    /// A request message.
274    Request(Request),
275    /// A response message.
276    Response(Response),
277    /// A notification message.
278    Notification(Notification),
279}
280
281impl Message {
282    /// Get the method name if this is a request or notification.
283    #[must_use]
284    pub fn method(&self) -> Option<&str> {
285        match self {
286            Self::Request(r) => Some(&r.method),
287            Self::Notification(n) => Some(&n.method),
288            Self::Response(_) => None,
289        }
290    }
291
292    /// Get the request ID if this is a request or response.
293    #[must_use]
294    pub const fn id(&self) -> Option<&RequestId> {
295        match self {
296            Self::Request(r) => Some(&r.id),
297            Self::Response(r) => Some(&r.id),
298            Self::Notification(_) => None,
299        }
300    }
301
302    /// Check if this is a request.
303    #[must_use]
304    pub const fn is_request(&self) -> bool {
305        matches!(self, Self::Request(_))
306    }
307
308    /// Check if this is a response.
309    #[must_use]
310    pub const fn is_response(&self) -> bool {
311        matches!(self, Self::Response(_))
312    }
313
314    /// Check if this is a notification.
315    #[must_use]
316    pub const fn is_notification(&self) -> bool {
317        matches!(self, Self::Notification(_))
318    }
319
320    /// Try to get this as a request.
321    #[must_use]
322    pub const fn as_request(&self) -> Option<&Request> {
323        match self {
324            Self::Request(r) => Some(r),
325            _ => None,
326        }
327    }
328
329    /// Try to get this as a response.
330    #[must_use]
331    pub const fn as_response(&self) -> Option<&Response> {
332        match self {
333            Self::Response(r) => Some(r),
334            _ => None,
335        }
336    }
337
338    /// Try to get this as a notification.
339    #[must_use]
340    pub const fn as_notification(&self) -> Option<&Notification> {
341        match self {
342            Self::Notification(n) => Some(n),
343            _ => None,
344        }
345    }
346}
347
348impl From<Request> for Message {
349    fn from(r: Request) -> Self {
350        Self::Request(r)
351    }
352}
353
354impl From<Response> for Message {
355    fn from(r: Response) -> Self {
356        Self::Response(r)
357    }
358}
359
360impl From<Notification> for Message {
361    fn from(n: Notification) -> Self {
362        Self::Notification(n)
363    }
364}
365
366/// A progress token for tracking long-running operations.
367///
368/// Progress tokens are included in requests that may take a long time,
369/// allowing the server to send progress updates.
370#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
371#[serde(untagged)]
372pub enum ProgressToken {
373    /// Numeric progress token.
374    Number(u64),
375    /// String progress token.
376    String(String),
377}
378
379impl std::fmt::Display for ProgressToken {
380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381        match self {
382            Self::Number(n) => write!(f, "{n}"),
383            Self::String(s) => write!(f, "{s}"),
384        }
385    }
386}
387
388/// A cursor for paginated results.
389///
390/// Cursors are opaque strings that represent a position in a paginated
391/// result set. Pass the cursor from a previous response to get the next page.
392#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
393#[serde(transparent)]
394pub struct Cursor(pub String);
395
396impl Cursor {
397    /// Create a new cursor.
398    #[must_use]
399    pub fn new(cursor: impl Into<String>) -> Self {
400        Self(cursor.into())
401    }
402}
403
404impl std::fmt::Display for Cursor {
405    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406        write!(f, "{}", self.0)
407    }
408}
409
410impl From<String> for Cursor {
411    fn from(s: String) -> Self {
412        Self(s)
413    }
414}
415
416impl From<&str> for Cursor {
417    fn from(s: &str) -> Self {
418        Self(s.to_string())
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[test]
427    fn test_request_serialization() -> Result<(), Box<dyn std::error::Error>> {
428        let request = Request::new("tools/list", 1u64);
429        let json = serde_json::to_string(&request)?;
430        assert!(json.contains("\"jsonrpc\":\"2.0\""));
431        assert!(json.contains("\"method\":\"tools/list\""));
432        assert!(json.contains("\"id\":1"));
433        Ok(())
434    }
435
436    #[test]
437    fn test_request_with_params() -> Result<(), Box<dyn std::error::Error>> {
438        let request = Request::with_params(
439            "tools/call",
440            1u64,
441            serde_json::json!({"name": "search", "arguments": {"query": "test"}}),
442        );
443        let json = serde_json::to_string(&request)?;
444        assert!(json.contains("\"params\""));
445        assert!(json.contains("\"name\":\"search\""));
446        Ok(())
447    }
448
449    #[test]
450    fn test_response_success() -> Result<(), Box<dyn std::error::Error>> {
451        let response = Response::success(1u64, serde_json::json!({"tools": []}));
452        assert!(response.is_success());
453        assert!(!response.is_error());
454
455        let result = response
456            .into_result()
457            .map_err(|e| format!("Error: {}", e.message))?;
458        assert!(result.get("tools").is_some());
459        Ok(())
460    }
461
462    #[test]
463    fn test_response_error() {
464        let error = JsonRpcError {
465            code: -32601,
466            message: "Method not found".to_string(),
467            data: None,
468        };
469        let response = Response::error(1u64, error);
470        assert!(!response.is_success());
471        assert!(response.is_error());
472
473        // unwrap_err is intentional - we're testing the error path
474        let err = response.into_result().unwrap_err();
475        assert_eq!(err.code, -32601);
476    }
477
478    #[test]
479    fn test_notification() -> Result<(), Box<dyn std::error::Error>> {
480        let notification = Notification::with_params(
481            "notifications/progress",
482            serde_json::json!({"progress": 50, "total": 100}),
483        );
484        let json = serde_json::to_string(&notification)?;
485        assert!(json.contains("\"method\":\"notifications/progress\""));
486        assert!(!json.contains("\"id\"")); // Notifications have no ID
487        Ok(())
488    }
489
490    #[test]
491    fn test_message_parsing() -> Result<(), Box<dyn std::error::Error>> {
492        // Request
493        let json = r#"{"jsonrpc":"2.0","id":1,"method":"test"}"#;
494        let msg: Message = serde_json::from_str(json)?;
495        assert!(msg.is_request());
496        assert_eq!(msg.method(), Some("test"));
497
498        // Response
499        let json = r#"{"jsonrpc":"2.0","id":1,"result":{}}"#;
500        let msg: Message = serde_json::from_str(json)?;
501        assert!(msg.is_response());
502
503        // Notification
504        let json = r#"{"jsonrpc":"2.0","method":"notify"}"#;
505        let msg: Message = serde_json::from_str(json)?;
506        assert!(msg.is_notification());
507        Ok(())
508    }
509
510    #[test]
511    fn test_request_id_types() -> Result<(), Box<dyn std::error::Error>> {
512        // Number ID
513        let request = Request::new("test", 42u64);
514        let json = serde_json::to_string(&request)?;
515        assert!(json.contains("\"id\":42"));
516
517        // String ID
518        let request = Request::new("test", "req-001");
519        let json = serde_json::to_string(&request)?;
520        assert!(json.contains("\"id\":\"req-001\""));
521        Ok(())
522    }
523}