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 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}