Skip to content

Commit 2900e72

Browse files
Copilotlarp0
andcommitted
Implement structured logging and enhanced error handling
Co-authored-by: larp0 <[email protected]>
1 parent e5bc32b commit 2900e72

File tree

6 files changed

+1260
-47
lines changed

6 files changed

+1260
-47
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ env_logger = "0.10"
99
chrono = "0.4"
1010
url = { version = "2.4.1", features = ["serde"] }
1111
anyhow = "1.0"
12+
thiserror = "1.0"
1213
serde = { version = "1.0", features = ["derive"] }
1314
serde_json = "1.0"
1415
tokio = { version = "1.0", features = ["full"] }
16+
tracing = "0.1"
17+
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
18+
uuid = { version = "1.0", features = ["v4"] }
19+
once_cell = "1.19"
1520
solana-client = "1.17"
1621
solana-sdk = "1.17"
1722
solana-account-decoder = "1.17"

src/error.rs

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
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+
}

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
pub mod config;
2+
pub mod error;
3+
pub mod logging;
24
pub mod protocol;
35
pub mod rpc;
46
pub mod server;
@@ -7,5 +9,7 @@ pub mod transport;
79
pub mod validation;
810

911
pub use config::{Config, SvmNetwork};
12+
pub use error::{McpError, McpResult};
13+
pub use logging::{init_logging, get_metrics};
1014
pub use server::start_server;
1115
pub use transport::CustomStdioTransport;

0 commit comments

Comments
 (0)