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    /// If this is a `resources/read` request, extract the resource URI from `params.uri`.
78    pub fn resource_uri(&self) -> Option<&str> {
79        if self.method.as_deref() != Some("resources/read") {
80            return None;
81        }
82        self.params.as_ref()?.get("uri")?.as_str()
83    }
84
85    /// If this is a `prompts/get` request, extract the prompt name from `params.name`.
86    pub fn prompt_name(&self) -> Option<&str> {
87        if self.method.as_deref() != Some("prompts/get") {
88            return None;
89        }
90        self.params.as_ref()?.get("name")?.as_str()
91    }
92
93    /// Return the primary "resource identifier" being accessed for any MCP method.
94    ///
95    /// - `tools/call`      -> tool name (`params.name`)
96    /// - `resources/read`  -> resource URI (`params.uri`)
97    /// - `prompts/get`     -> prompt name (`params.name`)
98    /// - Listing methods (`resources/list`, `tools/list`, `prompts/list`) -> `None`
99    /// - All other methods -> `None`
100    pub fn method_resource_name(&self) -> Option<&str> {
101        match self.method.as_deref()? {
102            "tools/call" => self.params.as_ref()?.get("name")?.as_str(),
103            "resources/read" => self.params.as_ref()?.get("uri")?.as_str(),
104            "prompts/get" => self.params.as_ref()?.get("name")?.as_str(),
105            _ => None,
106        }
107    }
108
109    /// Create a JSON-RPC error response for a given request ID.
110    pub fn error_response(id: Value, code: i64, message: impl Into<String>) -> Self {
111        Self {
112            jsonrpc: "2.0".to_string(),
113            id: Some(id),
114            method: None,
115            params: None,
116            result: None,
117            error: Some(JsonRpcError {
118                code,
119                message: message.into(),
120                data: None,
121            }),
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn parse_request() {
132        let raw = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/tmp/test"}}}"#;
133        let msg = McpMessage::parse(raw).unwrap();
134        assert!(msg.is_request());
135        assert!(!msg.is_response());
136        assert_eq!(msg.tool_name(), Some("read_file"));
137    }
138
139    #[test]
140    fn parse_response() {
141        let raw =
142            r#"{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"hello"}]}}"#;
143        let msg = McpMessage::parse(raw).unwrap();
144        assert!(msg.is_response());
145        assert!(!msg.is_request());
146        assert_eq!(msg.tool_name(), None);
147    }
148
149    #[test]
150    fn parse_notification() {
151        let raw = r#"{"jsonrpc":"2.0","method":"notifications/progress","params":{"token":"abc"}}"#;
152        let msg = McpMessage::parse(raw).unwrap();
153        assert!(msg.is_notification());
154        assert!(!msg.is_request());
155    }
156
157    #[test]
158    fn error_response() {
159        let resp = McpMessage::error_response(serde_json::json!(1), -32600, "denied by policy");
160        assert!(resp.is_response());
161        assert_eq!(resp.error.unwrap().code, -32600);
162    }
163
164    #[test]
165    fn roundtrip() {
166        let raw = r#"{"jsonrpc":"2.0","id":42,"method":"initialize","params":{"capabilities":{}}}"#;
167        let msg = McpMessage::parse(raw).unwrap();
168        let json = msg.to_json().unwrap();
169        let msg2 = McpMessage::parse(&json).unwrap();
170        assert_eq!(msg.method, msg2.method);
171        assert_eq!(msg.id, msg2.id);
172    }
173
174    #[test]
175    fn test_resource_uri_extraction() {
176        let raw = r#"{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"file:///etc/hosts"}}"#;
177        let msg = McpMessage::parse(raw).unwrap();
178        assert_eq!(msg.resource_uri(), Some("file:///etc/hosts"));
179    }
180
181    #[test]
182    fn test_prompt_name_extraction() {
183        let raw = r#"{"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"summarize","arguments":{"text":"hello"}}}"#;
184        let msg = McpMessage::parse(raw).unwrap();
185        assert_eq!(msg.prompt_name(), Some("summarize"));
186    }
187
188    #[test]
189    fn test_method_resource_name_covers_all_types() {
190        // tools/call -> tool name
191        let raw = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{}}}"#;
192        let msg = McpMessage::parse(raw).unwrap();
193        assert_eq!(msg.method_resource_name(), Some("read_file"));
194
195        // resources/read -> URI
196        let raw = r#"{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"file:///tmp/data.json"}}"#;
197        let msg = McpMessage::parse(raw).unwrap();
198        assert_eq!(msg.method_resource_name(), Some("file:///tmp/data.json"));
199
200        // prompts/get -> prompt name
201        let raw =
202            r#"{"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"code_review"}}"#;
203        let msg = McpMessage::parse(raw).unwrap();
204        assert_eq!(msg.method_resource_name(), Some("code_review"));
205
206        // listing methods -> None
207        for method in &["resources/list", "tools/list", "prompts/list"] {
208            let raw = format!(
209                r#"{{"jsonrpc":"2.0","id":10,"method":"{}","params":{{}}}}"#,
210                method
211            );
212            let msg = McpMessage::parse(&raw).unwrap();
213            assert_eq!(
214                msg.method_resource_name(),
215                None,
216                "{} should return None",
217                method
218            );
219        }
220
221        // other methods -> None
222        let raw = r#"{"jsonrpc":"2.0","id":99,"method":"initialize","params":{"capabilities":{}}}"#;
223        let msg = McpMessage::parse(raw).unwrap();
224        assert_eq!(msg.method_resource_name(), None);
225    }
226
227    #[test]
228    fn test_resource_uri_returns_none_for_other_methods() {
229        // tools/call should not return a resource URI
230        let raw = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file"}}"#;
231        let msg = McpMessage::parse(raw).unwrap();
232        assert_eq!(msg.resource_uri(), None);
233
234        // prompts/get should not return a resource URI
235        let raw =
236            r#"{"jsonrpc":"2.0","id":2,"method":"prompts/get","params":{"name":"summarize"}}"#;
237        let msg = McpMessage::parse(raw).unwrap();
238        assert_eq!(msg.resource_uri(), None);
239
240        // response should not return a resource URI
241        let raw = r#"{"jsonrpc":"2.0","id":3,"result":{"contents":[]}}"#;
242        let msg = McpMessage::parse(raw).unwrap();
243        assert_eq!(msg.resource_uri(), None);
244    }
245
246    #[test]
247    fn test_prompt_name_returns_none_for_other_methods() {
248        // tools/call should not return a prompt name
249        let raw = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file"}}"#;
250        let msg = McpMessage::parse(raw).unwrap();
251        assert_eq!(msg.prompt_name(), None);
252
253        // resources/read should not return a prompt name
254        let raw = r#"{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"file:///tmp/x"}}"#;
255        let msg = McpMessage::parse(raw).unwrap();
256        assert_eq!(msg.prompt_name(), None);
257
258        // response should not return a prompt name
259        let raw = r#"{"jsonrpc":"2.0","id":3,"result":{"messages":[]}}"#;
260        let msg = McpMessage::parse(raw).unwrap();
261        assert_eq!(msg.prompt_name(), None);
262    }
263}