ricecoder_mcp/
error.rs

1//! Error types for MCP integration
2
3use std::fmt;
4use thiserror::Error;
5
6/// Result type for MCP operations
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Errors that can occur during MCP operations
10#[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    /// Creates a user-friendly error message
87    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    /// Gets the error type for logging
117    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    /// Checks if this is a recoverable error
147    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    /// Checks if this is a permanent error
158    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/// Error context for detailed error reporting
170#[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    /// Creates a new error context
180    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    /// Sets the tool ID
190    pub fn with_tool_id(mut self, tool_id: String) -> Self {
191        self.tool_id = Some(tool_id);
192        self
193    }
194
195    /// Sets the parameters
196    pub fn with_parameters(mut self, parameters: String) -> Self {
197        self.parameters = Some(parameters);
198        self
199    }
200
201    /// Sets the server ID
202    pub fn with_server_id(mut self, server_id: String) -> Self {
203        self.server_id = Some(server_id);
204        self
205    }
206
207    /// Sets the stack trace
208    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/// Protocol message types for MCP communication
240#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
241pub struct ToolCall {
242    pub tool_id: String,
243    pub parameters: serde_json::Value,
244}
245
246/// Result of a tool execution
247#[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/// Error response from tool execution
256#[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    /// Creates a new tool error
267    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    /// Sets the error context
277    pub fn with_context(mut self, context: String) -> Self {
278        self.context = Some(context);
279        self
280    }
281}
282
283/// Structured error log entry for comprehensive error logging
284#[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    /// Creates a new error log entry
299    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    /// Sets the tool ID
314    pub fn with_tool_id(mut self, tool_id: String) -> Self {
315        self.tool_id = Some(tool_id);
316        self
317    }
318
319    /// Sets the server ID
320    pub fn with_server_id(mut self, server_id: String) -> Self {
321        self.server_id = Some(server_id);
322        self
323    }
324
325    /// Sets the parameters
326    pub fn with_parameters(mut self, parameters: String) -> Self {
327        self.parameters = Some(parameters);
328        self
329    }
330
331    /// Sets the stack trace
332    pub fn with_stack_trace(mut self, stack_trace: String) -> Self {
333        self.stack_trace = Some(stack_trace);
334        self
335    }
336
337    /// Sets whether the error is recoverable
338    pub fn with_recoverable(mut self, recoverable: bool) -> Self {
339        self.is_recoverable = recoverable;
340        self
341    }
342
343    /// Sets the retry count
344    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}