1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct McpMessage {
12 pub jsonrpc: String,
13
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub id: Option<Value>,
17
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub method: Option<String>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub params: Option<Value>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub result: Option<Value>,
29
30 #[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 pub fn parse(raw: &str) -> Result<Self, serde_json::Error> {
46 serde_json::from_str(raw)
47 }
48
49 pub fn to_json(&self) -> Result<String, serde_json::Error> {
51 serde_json::to_string(self)
52 }
53
54 pub fn is_request(&self) -> bool {
56 self.method.is_some() && self.id.is_some()
57 }
58
59 pub fn is_notification(&self) -> bool {
61 self.method.is_some() && self.id.is_none()
62 }
63
64 pub fn is_response(&self) -> bool {
66 self.id.is_some() && (self.result.is_some() || self.error.is_some())
67 }
68
69 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}