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
75 .as_ref()?
76 .get("name")?
77 .as_str()
78 }
79
80 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}