1+ use thiserror::Error;
2+ use serde_json::Value;
3+ use uuid::Uuid;
4+
5+ /// Comprehensive error types for the Solana MCP Server
6+ ///
7+ /// This module defines a hierarchy of error types that provide
8+ /// rich context for debugging and monitoring while maintaining
9+ /// security by avoiding sensitive data exposure.
10+ #[derive(Error, Debug)]
11+ pub enum McpError {
12+ /// Client-side errors (invalid input, malformed requests)
13+ #[error("Client error: {message}")]
14+ Client {
15+ message: String,
16+ request_id: Option<Uuid>,
17+ method: Option<String>,
18+ },
19+
20+ /// Server-side errors (internal failures, service unavailable)
21+ #[error("Server error: {message}")]
22+ Server {
23+ message: String,
24+ request_id: Option<Uuid>,
25+ method: Option<String>,
26+ source: Option<Box<dyn std::error::Error + Send + Sync>>,
27+ },
28+
29+ /// RPC-specific errors (Solana client failures)
30+ #[error("RPC error: {message}")]
31+ Rpc {
32+ message: String,
33+ request_id: Option<Uuid>,
34+ method: Option<String>,
35+ rpc_url: Option<String>,
36+ source: Option<Box<dyn std::error::Error + Send + Sync>>,
37+ },
38+
39+ /// Validation errors (invalid parameters, security checks)
40+ #[error("Validation error: {message}")]
41+ Validation {
42+ message: String,
43+ request_id: Option<Uuid>,
44+ method: Option<String>,
45+ parameter: Option<String>,
46+ },
47+
48+ /// Network errors (connectivity issues, timeouts)
49+ #[error("Network error: {message}")]
50+ Network {
51+ message: String,
52+ request_id: Option<Uuid>,
53+ method: Option<String>,
54+ endpoint: Option<String>,
55+ },
56+
57+ /// Authentication/Authorization errors
58+ #[error("Auth error: {message}")]
59+ Auth {
60+ message: String,
61+ request_id: Option<Uuid>,
62+ method: Option<String>,
63+ },
64+ }
65+
66+ impl McpError {
67+ /// Creates a client error with context
68+ pub fn client(message: impl Into<String>) -> Self {
69+ Self::Client {
70+ message: message.into(),
71+ request_id: None,
72+ method: None,
73+ }
74+ }
75+
76+ /// Creates a server error with context
77+ pub fn server(message: impl Into<String>) -> Self {
78+ Self::Server {
79+ message: message.into(),
80+ request_id: None,
81+ method: None,
82+ source: None,
83+ }
84+ }
85+
86+ /// Creates an RPC error with context
87+ pub fn rpc(message: impl Into<String>) -> Self {
88+ Self::Rpc {
89+ message: message.into(),
90+ request_id: None,
91+ method: None,
92+ rpc_url: None,
93+ source: None,
94+ }
95+ }
96+
97+ /// Creates a validation error with context
98+ pub fn validation(message: impl Into<String>) -> Self {
99+ Self::Validation {
100+ message: message.into(),
101+ request_id: None,
102+ method: None,
103+ parameter: None,
104+ }
105+ }
106+
107+ /// Creates a network error with context
108+ pub fn network(message: impl Into<String>) -> Self {
109+ Self::Network {
110+ message: message.into(),
111+ request_id: None,
112+ method: None,
113+ endpoint: None,
114+ }
115+ }
116+
117+ /// Creates an auth error with context
118+ pub fn auth(message: impl Into<String>) -> Self {
119+ Self::Auth {
120+ message: message.into(),
121+ request_id: None,
122+ method: None,
123+ }
124+ }
125+
126+ /// Adds request ID context to the error
127+ pub fn with_request_id(mut self, request_id: Uuid) -> Self {
128+ match &mut self {
129+ McpError::Client { request_id: ref mut id, .. } => *id = Some(request_id),
130+ McpError::Server { request_id: ref mut id, .. } => *id = Some(request_id),
131+ McpError::Rpc { request_id: ref mut id, .. } => *id = Some(request_id),
132+ McpError::Validation { request_id: ref mut id, .. } => *id = Some(request_id),
133+ McpError::Network { request_id: ref mut id, .. } => *id = Some(request_id),
134+ McpError::Auth { request_id: ref mut id, .. } => *id = Some(request_id),
135+ }
136+ self
137+ }
138+
139+ /// Adds method context to the error
140+ pub fn with_method(mut self, method: impl Into<String>) -> Self {
141+ let method = method.into();
142+ match &mut self {
143+ McpError::Client { method: ref mut m, .. } => *m = Some(method),
144+ McpError::Server { method: ref mut m, .. } => *m = Some(method),
145+ McpError::Rpc { method: ref mut m, .. } => *m = Some(method),
146+ McpError::Validation { method: ref mut m, .. } => *m = Some(method),
147+ McpError::Network { method: ref mut m, .. } => *m = Some(method),
148+ McpError::Auth { method: ref mut m, .. } => *m = Some(method),
149+ }
150+ self
151+ }
152+
153+ /// Adds parameter context to validation errors
154+ pub fn with_parameter(mut self, parameter: impl Into<String>) -> Self {
155+ if let McpError::Validation { parameter: ref mut p, .. } = &mut self {
156+ *p = Some(parameter.into());
157+ }
158+ self
159+ }
160+
161+ /// Adds RPC URL context to RPC errors
162+ pub fn with_rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
163+ if let McpError::Rpc { rpc_url: ref mut url, .. } = &mut self {
164+ *url = Some(rpc_url.into());
165+ }
166+ self
167+ }
168+
169+ /// Adds endpoint context to network errors
170+ pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
171+ if let McpError::Network { endpoint: ref mut e, .. } = &mut self {
172+ *e = Some(endpoint.into());
173+ }
174+ self
175+ }
176+
177+ /// Adds source error context
178+ pub fn with_source(mut self, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
179+ match &mut self {
180+ McpError::Server { source: ref mut s, .. } => *s = Some(source),
181+ McpError::Rpc { source: ref mut s, .. } => *s = Some(source),
182+ _ => {}, // Other error types don't have source fields
183+ }
184+ self
185+ }
186+
187+ /// Returns the JSON-RPC error code for this error type
188+ pub fn json_rpc_code(&self) -> i32 {
189+ match self {
190+ McpError::Client { .. } => -32602, // Invalid params
191+ McpError::Validation { .. } => -32602, // Invalid params
192+ McpError::Auth { .. } => -32601, // Method not found (for security)
193+ McpError::Server { .. } => -32603, // Internal error
194+ McpError::Rpc { .. } => -32603, // Internal error
195+ McpError::Network { .. } => -32603, // Internal error
196+ }
197+ }
198+
199+ /// Returns a safe error message for client responses (no sensitive info)
200+ pub fn safe_message(&self) -> String {
201+ match self {
202+ McpError::Client { message, .. } => message.clone(),
203+ McpError::Validation { message, .. } => message.clone(),
204+ McpError::Auth { .. } => "Authentication required".to_string(),
205+ McpError::Server { .. } => "Internal server error".to_string(),
206+ McpError::Rpc { .. } => "RPC service temporarily unavailable".to_string(),
207+ McpError::Network { .. } => "Network service temporarily unavailable".to_string(),
208+ }
209+ }
210+
211+ /// Returns the request ID if available
212+ pub fn request_id(&self) -> Option<Uuid> {
213+ match self {
214+ McpError::Client { request_id, .. } => *request_id,
215+ McpError::Server { request_id, .. } => *request_id,
216+ McpError::Rpc { request_id, .. } => *request_id,
217+ McpError::Validation { request_id, .. } => *request_id,
218+ McpError::Network { request_id, .. } => *request_id,
219+ McpError::Auth { request_id, .. } => *request_id,
220+ }
221+ }
222+
223+ /// Returns the method name if available
224+ pub fn method(&self) -> Option<&str> {
225+ match self {
226+ McpError::Client { method, .. } => method.as_deref(),
227+ McpError::Server { method, .. } => method.as_deref(),
228+ McpError::Rpc { method, .. } => method.as_deref(),
229+ McpError::Validation { method, .. } => method.as_deref(),
230+ McpError::Network { method, .. } => method.as_deref(),
231+ McpError::Auth { method, .. } => method.as_deref(),
232+ }
233+ }
234+
235+ /// Converts to a JSON value for structured logging
236+ pub fn to_log_value(&self) -> Value {
237+ let mut log_data = serde_json::Map::new();
238+
239+ log_data.insert("error_type".to_string(), Value::String(self.error_type().to_string()));
240+ log_data.insert("message".to_string(), Value::String(self.to_string()));
241+
242+ if let Some(request_id) = self.request_id() {
243+ log_data.insert("request_id".to_string(), Value::String(request_id.to_string()));
244+ }
245+
246+ if let Some(method) = self.method() {
247+ log_data.insert("method".to_string(), Value::String(method.to_string()));
248+ }
249+
250+ match self {
251+ McpError::Validation { parameter, .. } => {
252+ if let Some(param) = parameter {
253+ log_data.insert("parameter".to_string(), Value::String(param.clone()));
254+ }
255+ },
256+ McpError::Rpc { rpc_url, .. } => {
257+ if let Some(url) = rpc_url {
258+ // Sanitize URL for logging
259+ let sanitized = crate::validation::sanitize_for_logging(url);
260+ log_data.insert("rpc_url".to_string(), Value::String(sanitized));
261+ }
262+ },
263+ McpError::Network { endpoint, .. } => {
264+ if let Some(ep) = endpoint {
265+ let sanitized = crate::validation::sanitize_for_logging(ep);
266+ log_data.insert("endpoint".to_string(), Value::String(sanitized));
267+ }
268+ },
269+ _ => {}
270+ }
271+
272+ Value::Object(log_data)
273+ }
274+
275+ /// Returns the error type as a string for categorization
276+ pub fn error_type(&self) -> &'static str {
277+ match self {
278+ McpError::Client { .. } => "client",
279+ McpError::Server { .. } => "server",
280+ McpError::Rpc { .. } => "rpc",
281+ McpError::Validation { .. } => "validation",
282+ McpError::Network { .. } => "network",
283+ McpError::Auth { .. } => "auth",
284+ }
285+ }
286+ }
287+
288+ /// Convert anyhow errors to McpError
289+ impl From<anyhow::Error> for McpError {
290+ fn from(err: anyhow::Error) -> Self {
291+ McpError::server(err.to_string())
292+ .with_source(err.into())
293+ }
294+ }
295+
296+ /// Convert solana client errors to McpError
297+ impl From<solana_client::client_error::ClientError> for McpError {
298+ fn from(err: solana_client::client_error::ClientError) -> Self {
299+ use solana_client::client_error::ClientErrorKind;
300+
301+ match err.kind() {
302+ ClientErrorKind::Io(_) => McpError::network(err.to_string()),
303+ ClientErrorKind::Reqwest(_) => McpError::network(err.to_string()),
304+ ClientErrorKind::RpcError(_) => McpError::rpc(err.to_string()),
305+ ClientErrorKind::SerdeJson(_) => McpError::server(err.to_string()),
306+ _ => McpError::server(err.to_string()),
307+ }.with_source(Box::new(err))
308+ }
309+ }
310+
311+ /// Result type alias for MCP operations
312+ pub type McpResult<T> = Result<T, McpError>;
313+
314+ #[cfg(test)]
315+ mod tests {
316+ use super::*;
317+
318+ #[test]
319+ fn test_error_creation_and_chaining() {
320+ let request_id = Uuid::new_v4();
321+ let error = McpError::validation("Invalid pubkey format")
322+ .with_request_id(request_id)
323+ .with_method("getBalance")
324+ .with_parameter("pubkey");
325+
326+ assert_eq!(error.json_rpc_code(), -32602);
327+ assert_eq!(error.request_id(), Some(request_id));
328+ assert_eq!(error.method(), Some("getBalance"));
329+ assert_eq!(error.error_type(), "validation");
330+ }
331+
332+ #[test]
333+ fn test_safe_message() {
334+ let server_error = McpError::server("Database connection failed with password: secret123");
335+ assert_eq!(server_error.safe_message(), "Internal server error");
336+
337+ let validation_error = McpError::validation("Invalid pubkey format");
338+ assert_eq!(validation_error.safe_message(), "Invalid pubkey format");
339+ }
340+
341+ #[test]
342+ fn test_log_value_serialization() {
343+ let request_id = Uuid::new_v4();
344+ let error = McpError::rpc("Connection timeout")
345+ .with_request_id(request_id)
346+ .with_method("getBalance")
347+ .with_rpc_url("https://api.mainnet-beta.solana.com");
348+
349+ let log_value = error.to_log_value();
350+ assert!(log_value.get("error_type").is_some());
351+ assert!(log_value.get("request_id").is_some());
352+ assert!(log_value.get("method").is_some());
353+ assert!(log_value.get("rpc_url").is_some());
354+ }
355+ }
0 commit comments