Skip to main content

netget/server/mcp/
jsonrpc.rs

1//! JSON-RPC 2.0 message types for MCP
2//!
3//! Implements the JSON-RPC 2.0 specification used by the Model Context Protocol.
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// JSON-RPC 2.0 version string
9pub const JSONRPC_VERSION: &str = "2.0";
10
11/// JSON-RPC 2.0 request message
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct JsonRpcRequest {
14    /// JSON-RPC version (always "2.0")
15    pub jsonrpc: String,
16    /// Request method name
17    pub method: String,
18    /// Optional request parameters
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub params: Option<Value>,
21    /// Request identifier (for matching responses)
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub id: Option<RequestId>,
24}
25
26/// JSON-RPC 2.0 response message
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct JsonRpcResponse {
29    /// JSON-RPC version (always "2.0")
30    pub jsonrpc: String,
31    /// Success result (mutually exclusive with error)
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub result: Option<Value>,
34    /// Error result (mutually exclusive with result)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub error: Option<JsonRpcError>,
37    /// Request identifier (matches request id)
38    pub id: Option<RequestId>,
39}
40
41/// JSON-RPC 2.0 notification message (no response expected)
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct JsonRpcNotification {
44    /// JSON-RPC version (always "2.0")
45    pub jsonrpc: String,
46    /// Notification method name
47    pub method: String,
48    /// Optional notification parameters
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub params: Option<Value>,
51}
52
53/// JSON-RPC 2.0 error object
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct JsonRpcError {
56    /// Error code
57    pub code: i32,
58    /// Error message
59    pub message: String,
60    /// Optional additional error data
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub data: Option<Value>,
63}
64
65/// Request/Response ID (can be string, number, or null)
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
67#[serde(untagged)]
68pub enum RequestId {
69    String(String),
70    Number(i64),
71}
72
73impl From<String> for RequestId {
74    fn from(s: String) -> Self {
75        Self::String(s)
76    }
77}
78
79impl From<i64> for RequestId {
80    fn from(n: i64) -> Self {
81        Self::Number(n)
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::String(s) => write!(f, "{}", s),
89            Self::Number(n) => write!(f, "{}", n),
90        }
91    }
92}
93
94/// Standard JSON-RPC 2.0 error codes
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum ErrorCode {
97    /// Parse error - Invalid JSON
98    ParseError = -32700,
99    /// Invalid Request - Missing required fields
100    InvalidRequest = -32600,
101    /// Method not found - Unknown method
102    MethodNotFound = -32601,
103    /// Invalid params - Parameter validation failed
104    InvalidParams = -32602,
105    /// Internal error - Server-side failure
106    InternalError = -32603,
107}
108
109impl ErrorCode {
110    /// Get the standard error message for this code
111    pub fn message(&self) -> &'static str {
112        match self {
113            Self::ParseError => "Parse error",
114            Self::InvalidRequest => "Invalid Request",
115            Self::MethodNotFound => "Method not found",
116            Self::InvalidParams => "Invalid params",
117            Self::InternalError => "Internal error",
118        }
119    }
120
121    /// Convert to i32
122    pub fn as_i32(&self) -> i32 {
123        *self as i32
124    }
125}
126
127impl JsonRpcError {
128    /// Create a new error from an error code
129    pub fn new(code: ErrorCode) -> Self {
130        Self {
131            code: code.as_i32(),
132            message: code.message().to_string(),
133            data: None,
134        }
135    }
136
137    /// Create an error with additional data
138    pub fn with_data(code: ErrorCode, data: Value) -> Self {
139        Self {
140            code: code.as_i32(),
141            message: code.message().to_string(),
142            data: Some(data),
143        }
144    }
145
146    /// Create a custom error with specific message
147    pub fn custom(code: ErrorCode, message: String) -> Self {
148        Self {
149            code: code.as_i32(),
150            message,
151            data: None,
152        }
153    }
154}
155
156impl JsonRpcResponse {
157    /// Create a success response
158    pub fn success(id: Option<RequestId>, result: Value) -> Self {
159        Self {
160            jsonrpc: JSONRPC_VERSION.to_string(),
161            result: Some(result),
162            error: None,
163            id,
164        }
165    }
166
167    /// Create an error response
168    pub fn error(id: Option<RequestId>, error: JsonRpcError) -> Self {
169        Self {
170            jsonrpc: JSONRPC_VERSION.to_string(),
171            result: None,
172            error: Some(error),
173            id,
174        }
175    }
176}
177
178impl JsonRpcRequest {
179    /// Create a new request
180    pub fn new(method: String, params: Option<Value>, id: Option<RequestId>) -> Self {
181        Self {
182            jsonrpc: JSONRPC_VERSION.to_string(),
183            method,
184            params,
185            id,
186        }
187    }
188}
189
190impl JsonRpcNotification {
191    /// Create a new notification
192    pub fn new(method: String, params: Option<Value>) -> Self {
193        Self {
194            jsonrpc: JSONRPC_VERSION.to_string(),
195            method,
196            params,
197        }
198    }
199}
200
201/// Parse an incoming JSON message into either a request or notification
202#[derive(Debug, Clone)]
203pub enum JsonRpcMessage {
204    Request(JsonRpcRequest),
205    Notification(JsonRpcNotification),
206}
207
208impl JsonRpcMessage {
209    /// Parse from JSON value
210    pub fn from_value(value: Value) -> Result<Self, JsonRpcError> {
211        // Try parsing as request first (has id field)
212        if value.get("id").is_some() {
213            match serde_json::from_value::<JsonRpcRequest>(value) {
214                Ok(req) => Ok(Self::Request(req)),
215                Err(e) => {
216                    // serde's message quotes the payload and names our own Rust fields, so it
217                    // is logged rather than returned - see `crate::utils::wire_failure`.
218                    tracing::debug!("MCP request did not parse: {e}");
219                    Err(JsonRpcError::custom(
220                        ErrorCode::InvalidRequest,
221                        "Failed to parse request".to_string(),
222                    ))
223                }
224            }
225        } else {
226            // Try parsing as notification (no id field)
227            match serde_json::from_value::<JsonRpcNotification>(value) {
228                Ok(notif) => Ok(Self::Notification(notif)),
229                Err(e) => {
230                    tracing::debug!("MCP notification did not parse: {e}");
231                    Err(JsonRpcError::custom(
232                        ErrorCode::InvalidRequest,
233                        "Failed to parse notification".to_string(),
234                    ))
235                }
236            }
237        }
238    }
239
240    /// Get the method name
241    pub fn method(&self) -> &str {
242        match self {
243            Self::Request(req) => &req.method,
244            Self::Notification(notif) => &notif.method,
245        }
246    }
247
248    /// Get the parameters
249    pub fn params(&self) -> Option<&Value> {
250        match self {
251            Self::Request(req) => req.params.as_ref(),
252            Self::Notification(notif) => notif.params.as_ref(),
253        }
254    }
255
256    /// Get the request ID (None for notifications)
257    pub fn id(&self) -> Option<&RequestId> {
258        match self {
259            Self::Request(req) => req.id.as_ref(),
260            Self::Notification(_) => None,
261        }
262    }
263}