1use serde_json::{Map, Value};
5
6pub const INVALID_REQUEST: i64 = -32600;
8pub const METHOD_NOT_FOUND: i64 = -32601;
10pub const INTERNAL_ERROR: i64 = -32603;
12
13#[derive(Clone, Debug, PartialEq, thiserror::Error)]
15pub enum McpError {
16 #[error("{message}")]
18 Rpc {
19 code: i64,
21 message: String,
23 data: Option<Value>,
25 },
26 #[error("{0}")]
28 Closed(String),
29 #[error("MCP request timed out after {0}ms")]
31 Timeout(u64),
32 #[error("MCP request aborted")]
34 Aborted,
35 #[error("{message}")]
37 Http {
38 status: u16,
40 message: String,
42 },
43 #[error("MCP server requires authentication")]
45 AuthRequired {
46 challenge: Option<String>,
48 },
49 #[error("MCP session expired")]
51 SessionExpired,
52 #[error("MCP OAuth authorization requires user interaction")]
55 SignInRequired,
56 #[error("{0}")]
58 Network(String),
59 #[error("{0}")]
61 Other(String),
62}
63
64impl McpError {
65 pub fn closed() -> McpError {
67 McpError::Closed("MCP connection closed".into())
68 }
69
70 pub fn invalid(message: impl Into<String>) -> McpError {
72 McpError::Rpc {
73 code: INVALID_REQUEST,
74 message: message.into(),
75 data: None,
76 }
77 }
78
79 pub fn status(&self) -> Option<u16> {
81 match self {
82 McpError::Http { status, .. } => Some(*status),
83 McpError::AuthRequired { .. } => Some(401),
84 McpError::SessionExpired => Some(404),
85 _ => None,
86 }
87 }
88
89 pub fn is_transient(&self) -> bool {
92 match self {
93 McpError::Network(_) => true,
94 McpError::Http { status, .. } => {
95 *status == 408 || *status == 429 || (*status >= 500 && *status != 501)
96 }
97 _ => false,
98 }
99 }
100}
101
102#[derive(Debug, PartialEq)]
104pub enum Incoming<'a> {
105 Request {
107 id: &'a Value,
109 method: &'a str,
111 params: Option<&'a Value>,
113 },
114 Notification {
116 method: &'a str,
118 params: Option<&'a Value>,
120 },
121 Response {
123 id: &'a Value,
125 outcome: Result<&'a Value, McpError>,
127 },
128}
129
130pub fn is_id(value: &Value) -> bool {
132 value.is_string() || value.as_f64().is_some_and(f64::is_finite)
133}
134
135pub fn classify(value: &Value) -> Option<Incoming<'_>> {
137 let object = value.as_object()?;
138 if object.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
139 return None;
140 }
141 let method = object.get("method").and_then(Value::as_str);
142 match (object.get("id"), method) {
143 (Some(id), Some(method)) if is_id(id) => Some(Incoming::Request {
144 id,
145 method,
146 params: object.get("params"),
147 }),
148 (None, Some(method)) => Some(Incoming::Notification {
149 method,
150 params: object.get("params"),
151 }),
152 (Some(id), None) if is_id(id) => response(id, object),
153 _ => None,
154 }
155}
156
157fn response<'a>(id: &'a Value, object: &'a Map<String, Value>) -> Option<Incoming<'a>> {
158 if let Some(result) = object.get("result") {
159 return (!object.contains_key("error")).then_some(Incoming::Response {
160 id,
161 outcome: Ok(result),
162 });
163 }
164 let error = object.get("error")?.as_object()?;
165 let code = error.get("code")?.as_f64()?;
166 let message = error.get("message")?.as_str()?;
167 Some(Incoming::Response {
168 id,
169 outcome: Err(McpError::Rpc {
170 code: code as i64,
171 message: message.to_owned(),
172 data: error.get("data").cloned(),
173 }),
174 })
175}
176
177pub fn id_key(id: &Value) -> String {
179 id.to_string()
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use serde_json::json;
186
187 #[test]
188 fn classifies_like_pi() {
189 let request = json!({"jsonrpc": "2.0", "id": 1, "method": "ping"});
190 assert!(matches!(
191 classify(&request),
192 Some(Incoming::Request { method: "ping", .. })
193 ));
194 let notification =
195 json!({"jsonrpc": "2.0", "method": "notifications/progress", "params": {}});
196 assert!(matches!(
197 classify(¬ification),
198 Some(Incoming::Notification { .. })
199 ));
200 let failure =
201 json!({"jsonrpc": "2.0", "id": "a", "error": {"code": -32601, "message": "nope"}});
202 assert_eq!(
203 classify(&failure),
204 Some(Incoming::Response {
205 id: &json!("a"),
206 outcome: Err(McpError::Rpc {
207 code: -32601,
208 message: "nope".into(),
209 data: None
210 })
211 })
212 );
213 assert_eq!(
214 classify(&json!({"jsonrpc": "2.0", "id": 1, "result": 1, "error": {}})),
215 None
216 );
217 assert_eq!(classify(&json!({"jsonrpc": "1.0", "method": "x"})), None);
218 assert_ne!(id_key(&json!(1)), id_key(&json!("1")));
219 }
220}