Skip to main content

dome_core/
message.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// A parsed MCP message (JSON-RPC 2.0 envelope).
5///
6/// MCP uses JSON-RPC 2.0 over stdio or HTTP. Every message is one of:
7/// - Request: has `method` + `params`, has `id`
8/// - Response: has `result` or `error`, has `id`
9/// - Notification: has `method` + `params`, no `id`
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct McpMessage {
12    pub jsonrpc: String,
13
14    /// Request/response correlation ID. None for notifications.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub id: Option<Value>,
17
18    /// Method name (e.g. "tools/call", "tools/list", "initialize").
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub method: Option<String>,
21
22    /// Method parameters.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub params: Option<Value>,
25
26    /// Success response payload.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub result: Option<Value>,
29
30    /// Error response payload.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub error: Option<JsonRpcError>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct JsonRpcError {
37    pub code: i64,
38    pub message: String,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub data: Option<Value>,
41}
42
43impl McpMessage {
44    /// Parse a JSON-RPC message from a raw JSON string.
45    pub fn parse(raw: &str) -> Result<Self, serde_json::Error> {
46        serde_json::from_str(raw)
47    }
48
49    /// Serialize this message to a JSON string (no trailing newline).
50    pub fn to_json(&self) -> Result<String, serde_json::Error> {
51        serde_json::to_string(self)
52    }
53
54    /// True if this is a request (has method + id).
55    pub fn is_request(&self) -> bool {
56        self.method.is_some() && self.id.is_some()
57    }
58
59    /// True if this is a notification (has method, no id).
60    pub fn is_notification(&self) -> bool {
61        self.method.is_some() && self.id.is_none()
62    }
63
64    /// True if this is a response (has result or error + id).
65    pub fn is_response(&self) -> bool {
66        self.id.is_some() && (self.result.is_some() || self.error.is_some())
67    }
68
69    /// If this is a tools/call request, extract the tool name.
70    pub fn tool_name(&self) -> Option<&str> {
71        if self.method.as_deref() != Some("tools/call") {
72            return None;
73        }
74        self.params.as_ref()?.get("name")?.as_str()
75    }
76
77    /// Create a JSON-RPC error response for a given request ID.
78    pub fn error_response(id: Value, code: i64, message: impl Into<String>) -> Self {
79        Self {
80            jsonrpc: "2.0".to_string(),
81            id: Some(id),
82            method: None,
83            params: None,
84            result: None,
85            error: Some(JsonRpcError {
86                code,
87                message: message.into(),
88                data: None,
89            }),
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn parse_request() {
100        let raw = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/tmp/test"}}}"#;
101        let msg = McpMessage::parse(raw).unwrap();
102        assert!(msg.is_request());
103        assert!(!msg.is_response());
104        assert_eq!(msg.tool_name(), Some("read_file"));
105    }
106
107    #[test]
108    fn parse_response() {
109        let raw =
110            r#"{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"hello"}]}}"#;
111        let msg = McpMessage::parse(raw).unwrap();
112        assert!(msg.is_response());
113        assert!(!msg.is_request());
114        assert_eq!(msg.tool_name(), None);
115    }
116
117    #[test]
118    fn parse_notification() {
119        let raw = r#"{"jsonrpc":"2.0","method":"notifications/progress","params":{"token":"abc"}}"#;
120        let msg = McpMessage::parse(raw).unwrap();
121        assert!(msg.is_notification());
122        assert!(!msg.is_request());
123    }
124
125    #[test]
126    fn error_response() {
127        let resp = McpMessage::error_response(serde_json::json!(1), -32600, "denied by policy");
128        assert!(resp.is_response());
129        assert_eq!(resp.error.unwrap().code, -32600);
130    }
131
132    #[test]
133    fn roundtrip() {
134        let raw = r#"{"jsonrpc":"2.0","id":42,"method":"initialize","params":{"capabilities":{}}}"#;
135        let msg = McpMessage::parse(raw).unwrap();
136        let json = msg.to_json().unwrap();
137        let msg2 = McpMessage::parse(&json).unwrap();
138        assert_eq!(msg.method, msg2.method);
139        assert_eq!(msg.id, msg2.id);
140    }
141}