1use std::fmt;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, Error>;
8
9#[derive(Debug, Error)]
11pub enum Error {
12 #[error("MCP server error: {0}")]
13 ServerError(String),
14
15 #[error("Configuration error: {0}")]
16 ConfigError(String),
17
18 #[error("Validation error: {0}")]
19 ValidationError(String),
20
21 #[error("Timeout after {0}ms")]
22 TimeoutError(u64),
23
24 #[error("Tool not found: {0}")]
25 ToolNotFound(String),
26
27 #[error("Permission denied for tool: {0}")]
28 PermissionDenied(String),
29
30 #[error("Tool execution failed: {0}")]
31 ExecutionError(String),
32
33 #[error("Parameter validation failed: {0}")]
34 ParameterValidationError(String),
35
36 #[error("Output validation failed: {0}")]
37 OutputValidationError(String),
38
39 #[error("Naming conflict: {0}")]
40 NamingConflict(String),
41
42 #[error("Connection error: {0}")]
43 ConnectionError(String),
44
45 #[error("Serialization error: {0}")]
46 SerializationError(#[from] serde_json::Error),
47
48 #[error("Storage error: {0}")]
49 StorageError(String),
50
51 #[error("IO error: {0}")]
52 IoError(#[from] std::io::Error),
53
54 #[error("Internal error: {0}")]
55 InternalError(String),
56
57 #[error("Server disconnected: {0}")]
58 ServerDisconnected(String),
59
60 #[error("Reconnection failed: {0}")]
61 ReconnectionFailed(String),
62
63 #[error("Max retries exceeded: {0}")]
64 MaxRetriesExceeded(String),
65
66 #[error("Tool execution interrupted")]
67 ExecutionInterrupted,
68
69 #[error("Invalid tool parameters: {0}")]
70 InvalidToolParameters(String),
71
72 #[error("Invalid tool output: {0}")]
73 InvalidToolOutput(String),
74
75 #[error("Configuration validation failed: {0}")]
76 ConfigValidationError(String),
77
78 #[error("Tool registration failed: {0}")]
79 ToolRegistrationError(String),
80
81 #[error("Multiple naming conflicts detected: {0}")]
82 MultipleNamingConflicts(String),
83}
84
85impl Error {
86 pub fn user_message(&self) -> String {
88 match self {
89 Error::ServerError(msg) => format!("MCP server error: {}", msg),
90 Error::ConfigError(msg) => format!("Configuration error: {}. Please check your configuration files.", msg),
91 Error::ValidationError(msg) => format!("Validation error: {}", msg),
92 Error::TimeoutError(ms) => format!("Operation timed out after {}ms. Please try again or increase the timeout.", ms),
93 Error::ToolNotFound(tool_id) => format!("Tool '{}' not found. Please check the tool ID and try again.", tool_id),
94 Error::PermissionDenied(tool_id) => format!("Permission denied for tool '{}'. Contact your administrator.", tool_id),
95 Error::ExecutionError(msg) => format!("Tool execution failed: {}. Please check the tool parameters and try again.", msg),
96 Error::ParameterValidationError(msg) => format!("Invalid tool parameters: {}. Please provide valid parameters.", msg),
97 Error::OutputValidationError(msg) => format!("Tool returned invalid output: {}. Please contact the tool provider.", msg),
98 Error::NamingConflict(msg) => format!("Naming conflict detected: {}. Please use a qualified tool name.", msg),
99 Error::ConnectionError(msg) => format!("Connection error: {}. Please check your network connection.", msg),
100 Error::SerializationError(msg) => format!("Serialization error: {}. Please check the data format.", msg),
101 Error::StorageError(msg) => format!("Storage error: {}. Please check your storage configuration.", msg),
102 Error::IoError(msg) => format!("IO error: {}. Please check file permissions.", msg),
103 Error::InternalError(msg) => format!("Internal error: {}. Please contact support.", msg),
104 Error::ServerDisconnected(server_id) => format!("Server '{}' disconnected. Attempting to reconnect...", server_id),
105 Error::ReconnectionFailed(msg) => format!("Failed to reconnect to server: {}. Please check the server status.", msg),
106 Error::MaxRetriesExceeded(msg) => format!("Maximum reconnection attempts exceeded: {}. Please check the server.", msg),
107 Error::ExecutionInterrupted => "Tool execution was interrupted. Please try again.".to_string(),
108 Error::InvalidToolParameters(msg) => format!("Invalid tool parameters: {}. Please provide valid parameters.", msg),
109 Error::InvalidToolOutput(msg) => format!("Tool returned invalid output: {}. Please contact the tool provider.", msg),
110 Error::ConfigValidationError(msg) => format!("Configuration validation failed: {}. Please fix your configuration.", msg),
111 Error::ToolRegistrationError(msg) => format!("Tool registration failed: {}. Please check the tool definition.", msg),
112 Error::MultipleNamingConflicts(msg) => format!("Multiple naming conflicts detected: {}. Please use qualified tool names.", msg),
113 }
114 }
115
116 pub fn error_type(&self) -> &'static str {
118 match self {
119 Error::ServerError(_) => "ServerError",
120 Error::ConfigError(_) => "ConfigError",
121 Error::ValidationError(_) => "ValidationError",
122 Error::TimeoutError(_) => "TimeoutError",
123 Error::ToolNotFound(_) => "ToolNotFound",
124 Error::PermissionDenied(_) => "PermissionDenied",
125 Error::ExecutionError(_) => "ExecutionError",
126 Error::ParameterValidationError(_) => "ParameterValidationError",
127 Error::OutputValidationError(_) => "OutputValidationError",
128 Error::NamingConflict(_) => "NamingConflict",
129 Error::ConnectionError(_) => "ConnectionError",
130 Error::SerializationError(_) => "SerializationError",
131 Error::StorageError(_) => "StorageError",
132 Error::IoError(_) => "IoError",
133 Error::InternalError(_) => "InternalError",
134 Error::ServerDisconnected(_) => "ServerDisconnected",
135 Error::ReconnectionFailed(_) => "ReconnectionFailed",
136 Error::MaxRetriesExceeded(_) => "MaxRetriesExceeded",
137 Error::ExecutionInterrupted => "ExecutionInterrupted",
138 Error::InvalidToolParameters(_) => "InvalidToolParameters",
139 Error::InvalidToolOutput(_) => "InvalidToolOutput",
140 Error::ConfigValidationError(_) => "ConfigValidationError",
141 Error::ToolRegistrationError(_) => "ToolRegistrationError",
142 Error::MultipleNamingConflicts(_) => "MultipleNamingConflicts",
143 }
144 }
145
146 pub fn is_recoverable(&self) -> bool {
148 matches!(
149 self,
150 Error::TimeoutError(_)
151 | Error::ConnectionError(_)
152 | Error::ServerDisconnected(_)
153 | Error::ExecutionInterrupted
154 )
155 }
156
157 pub fn is_permanent(&self) -> bool {
159 matches!(
160 self,
161 Error::ToolNotFound(_)
162 | Error::PermissionDenied(_)
163 | Error::NamingConflict(_)
164 | Error::MultipleNamingConflicts(_)
165 )
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct ErrorContext {
172 pub tool_id: Option<String>,
173 pub parameters: Option<String>,
174 pub server_id: Option<String>,
175 pub stack_trace: Option<String>,
176}
177
178impl ErrorContext {
179 pub fn new() -> Self {
181 Self {
182 tool_id: None,
183 parameters: None,
184 server_id: None,
185 stack_trace: None,
186 }
187 }
188
189 pub fn with_tool_id(mut self, tool_id: String) -> Self {
191 self.tool_id = Some(tool_id);
192 self
193 }
194
195 pub fn with_parameters(mut self, parameters: String) -> Self {
197 self.parameters = Some(parameters);
198 self
199 }
200
201 pub fn with_server_id(mut self, server_id: String) -> Self {
203 self.server_id = Some(server_id);
204 self
205 }
206
207 pub fn with_stack_trace(mut self, stack_trace: String) -> Self {
209 self.stack_trace = Some(stack_trace);
210 self
211 }
212}
213
214impl Default for ErrorContext {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220impl fmt::Display for ErrorContext {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 write!(f, "ErrorContext {{")?;
223 if let Some(tool_id) = &self.tool_id {
224 write!(f, " tool_id: {}", tool_id)?;
225 }
226 if let Some(parameters) = &self.parameters {
227 write!(f, " parameters: {}", parameters)?;
228 }
229 if let Some(server_id) = &self.server_id {
230 write!(f, " server_id: {}", server_id)?;
231 }
232 if let Some(stack_trace) = &self.stack_trace {
233 write!(f, " stack_trace: {}", stack_trace)?;
234 }
235 write!(f, " }}")
236 }
237}
238
239#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
241pub struct ToolCall {
242 pub tool_id: String,
243 pub parameters: serde_json::Value,
244}
245
246#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
248pub struct ToolResult {
249 pub success: bool,
250 pub output: serde_json::Value,
251 pub error: Option<String>,
252 pub duration_ms: u64,
253}
254
255#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
257pub struct ToolError {
258 pub tool_id: String,
259 pub error: String,
260 pub error_type: String,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub context: Option<String>,
263}
264
265impl ToolError {
266 pub fn new(tool_id: String, error: String, error_type: String) -> Self {
268 Self {
269 tool_id,
270 error,
271 error_type,
272 context: None,
273 }
274 }
275
276 pub fn with_context(mut self, context: String) -> Self {
278 self.context = Some(context);
279 self
280 }
281}
282
283#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
285pub struct ErrorLogEntry {
286 pub timestamp: String,
287 pub error_type: String,
288 pub message: String,
289 pub tool_id: Option<String>,
290 pub server_id: Option<String>,
291 pub parameters: Option<String>,
292 pub stack_trace: Option<String>,
293 pub is_recoverable: bool,
294 pub retry_count: Option<u32>,
295}
296
297impl ErrorLogEntry {
298 pub fn new(error_type: String, message: String) -> Self {
300 Self {
301 timestamp: chrono::Local::now().to_rfc3339(),
302 error_type,
303 message,
304 tool_id: None,
305 server_id: None,
306 parameters: None,
307 stack_trace: None,
308 is_recoverable: false,
309 retry_count: None,
310 }
311 }
312
313 pub fn with_tool_id(mut self, tool_id: String) -> Self {
315 self.tool_id = Some(tool_id);
316 self
317 }
318
319 pub fn with_server_id(mut self, server_id: String) -> Self {
321 self.server_id = Some(server_id);
322 self
323 }
324
325 pub fn with_parameters(mut self, parameters: String) -> Self {
327 self.parameters = Some(parameters);
328 self
329 }
330
331 pub fn with_stack_trace(mut self, stack_trace: String) -> Self {
333 self.stack_trace = Some(stack_trace);
334 self
335 }
336
337 pub fn with_recoverable(mut self, recoverable: bool) -> Self {
339 self.is_recoverable = recoverable;
340 self
341 }
342
343 pub fn with_retry_count(mut self, count: u32) -> Self {
345 self.retry_count = Some(count);
346 self
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn test_error_user_message() {
356 let error = Error::ToolNotFound("test-tool".to_string());
357 let msg = error.user_message();
358 assert!(msg.contains("test-tool"));
359 assert!(msg.contains("not found"));
360 }
361
362 #[test]
363 fn test_error_type() {
364 let error = Error::TimeoutError(5000);
365 assert_eq!(error.error_type(), "TimeoutError");
366 }
367
368 #[test]
369 fn test_error_is_recoverable() {
370 assert!(Error::TimeoutError(5000).is_recoverable());
371 assert!(Error::ConnectionError("test".to_string()).is_recoverable());
372 assert!(!Error::ToolNotFound("test".to_string()).is_recoverable());
373 }
374
375 #[test]
376 fn test_error_is_permanent() {
377 assert!(Error::ToolNotFound("test".to_string()).is_permanent());
378 assert!(Error::PermissionDenied("test".to_string()).is_permanent());
379 assert!(!Error::TimeoutError(5000).is_permanent());
380 }
381
382 #[test]
383 fn test_error_context() {
384 let context = ErrorContext::new()
385 .with_tool_id("test-tool".to_string())
386 .with_parameters("param1=value1".to_string())
387 .with_server_id("server1".to_string());
388
389 assert_eq!(context.tool_id, Some("test-tool".to_string()));
390 assert_eq!(context.parameters, Some("param1=value1".to_string()));
391 assert_eq!(context.server_id, Some("server1".to_string()));
392 }
393
394 #[test]
395 fn test_tool_error_with_context() {
396 let error = ToolError::new(
397 "test-tool".to_string(),
398 "Execution failed".to_string(),
399 "ExecutionError".to_string(),
400 )
401 .with_context("Additional context".to_string());
402
403 assert_eq!(error.tool_id, "test-tool");
404 assert_eq!(error.context, Some("Additional context".to_string()));
405 }
406
407 #[test]
408 fn test_error_log_entry() {
409 let entry = ErrorLogEntry::new(
410 "ExecutionError".to_string(),
411 "Tool execution failed".to_string(),
412 )
413 .with_tool_id("test-tool".to_string())
414 .with_recoverable(true)
415 .with_retry_count(3);
416
417 assert_eq!(entry.error_type, "ExecutionError");
418 assert_eq!(entry.tool_id, Some("test-tool".to_string()));
419 assert!(entry.is_recoverable);
420 assert_eq!(entry.retry_count, Some(3));
421 }
422}