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
75            .as_ref()?
76            .get("name")?
77            .as_str()
78    }
79
80    /// Create a JSON-RPC error response for a given request ID.
81    pub fn error_response(id: Value, code: i64, message: impl Into<String>) -> Self {
82        Self {
83            jsonrpc: "2.0".to_string(),
84            id: Some(id),
85            method: None,
86            params: None,
87            result: None,
88            error: Some(JsonRpcError {
89                code,
90                message: message.into(),
91                data: None,
92            }),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn parse_request() {
103        let raw = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/tmp/test"}}}"#;
104        let msg = McpMessage::parse(raw).unwrap();
105        assert!(msg.is_request());
106        assert!(!msg.is_response());
107        assert_eq!(msg.tool_name(), Some("read_file"));
108    }
109
110    #[test]
111    fn parse_response() {
112        let raw = r#"{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"hello"}]}}"#;
113        let msg = McpMessage::parse(raw).unwrap();
114        assert!(msg.is_response());
115        assert!(!msg.is_request());
116        assert_eq!(msg.tool_name(), None);
117    }
118
119    #[test]
120    fn parse_notification() {
121        let raw = r#"{"jsonrpc":"2.0","method":"notifications/progress","params":{"token":"abc"}}"#;
122        let msg = McpMessage::parse(raw).unwrap();
123        assert!(msg.is_notification());
124        assert!(!msg.is_request());
125    }
126
127    #[test]
128    fn error_response() {
129        let resp = McpMessage::error_response(
130            serde_json::json!(1),
131            -32600,
132            "denied by policy",
133        );
134        assert!(resp.is_response());
135        assert_eq!(resp.error.unwrap().code, -32600);
136    }
137
138    #[test]
139    fn roundtrip() {
140        let raw = r#"{"jsonrpc":"2.0","id":42,"method":"initialize","params":{"capabilities":{}}}"#;
141        let msg = McpMessage::parse(raw).unwrap();
142        let json = msg.to_json().unwrap();
143        let msg2 = McpMessage::parse(&json).unwrap();
144        assert_eq!(msg.method, msg2.method);
145        assert_eq!(msg.id, msg2.id);
146    }
147}