mcp_probe_core/
error.rs

1//! Error types for MCP (Model Context Protocol) operations.
2//!
3//! This module provides comprehensive error handling for all MCP operations,
4//! including transport-specific errors, protocol errors, and validation errors.
5//!
6//! # Design Philosophy
7//!
8//! The error system is designed to be:
9//! - **Informative**: Provide clear, actionable error messages
10//! - **Structured**: Use strongly-typed error variants for programmatic handling  
11//! - **Transport-aware**: Include transport-specific error context
12//! - **Debuggable**: Include sufficient context for debugging
13//! - **User-friendly**: Format appropriately for end-user display
14
15use std::time::Duration;
16use thiserror::Error;
17
18/// The main error type for all MCP operations.
19///
20/// This enum covers all possible error conditions that can occur during
21/// MCP client operations, from transport failures to protocol violations.
22///
23/// # Examples
24///
25/// ```rust
26/// use mcp_probe_core::error::{McpError, TransportError};
27///
28/// let error = McpError::Transport(TransportError::ConnectionFailed {
29///     transport_type: "stdio".to_string(),
30///     reason: "Process exited unexpectedly".to_string(),
31/// });
32///
33/// println!("Error: {}", error);
34/// ```
35#[derive(Error, Debug)]
36pub enum McpError {
37    /// Transport-related errors (connection, communication, etc.)
38    #[error("Transport error: {0}")]
39    Transport(#[from] TransportError),
40
41    /// Protocol-level errors (invalid messages, unsupported versions, etc.)
42    #[error("Protocol error: {0}")]
43    Protocol(#[from] ProtocolError),
44
45    /// Validation errors (schema validation, capability mismatches, etc.)
46    #[error("Validation error: {0}")]
47    Validation(#[from] ValidationError),
48
49    /// Authentication and authorization errors
50    #[error("Authentication error: {0}")]
51    Auth(#[from] AuthError),
52
53    /// Timeout errors for operations that exceed time limits
54    #[error("Operation timed out after {duration_ms}ms: {operation}")]
55    Timeout {
56        /// The operation that timed out
57        operation: String,
58        /// The timeout duration in milliseconds
59        duration_ms: u64,
60    },
61
62    /// Configuration errors (invalid config files, missing parameters, etc.)
63    #[error("Configuration error: {0}")]
64    Config(#[from] ConfigError),
65
66    /// Serialization/deserialization errors
67    #[error("Serialization error: {source}")]
68    Serialization {
69        #[from]
70        /// The underlying serde_json error
71        source: serde_json::Error,
72    },
73
74    /// IO errors (file operations, network errors, etc.)
75    #[error("IO error: {source}")]
76    Io {
77        #[from]
78        /// The underlying IO error
79        source: std::io::Error,
80    },
81
82    /// Generic errors for cases not covered by specific variants
83    #[error("Internal error: {message}")]
84    Internal {
85        /// Error message
86        message: String,
87    },
88}
89
90/// Transport-specific errors for different MCP transport mechanisms.
91///
92/// Each transport type (stdio, HTTP+SSE, HTTP streaming) can have
93/// specific failure modes that need to be handled appropriately.
94#[derive(Error, Debug, Clone)]
95#[allow(missing_docs)]
96pub enum TransportError {
97    /// Failed to establish connection to the MCP server
98    #[error("Failed to connect to {transport_type} server: {reason}")]
99    ConnectionFailed {
100        transport_type: String,
101        reason: String,
102    },
103
104    /// Connection was lost during operation
105    #[error("Connection lost to {transport_type} server: {reason}")]
106    ConnectionLost {
107        transport_type: String,
108        reason: String,
109    },
110
111    /// Failed to send message to server
112    #[error("Failed to send message via {transport_type}: {reason}")]
113    SendFailed {
114        transport_type: String,
115        reason: String,
116    },
117
118    /// Failed to receive message from server
119    #[error("Failed to receive message via {transport_type}: {reason}")]
120    ReceiveFailed {
121        transport_type: String,
122        reason: String,
123    },
124
125    /// Transport-specific configuration is invalid
126    #[error("Invalid {transport_type} configuration: {reason}")]
127    InvalidConfig {
128        transport_type: String,
129        reason: String,
130    },
131
132    /// Process-related errors for stdio transport
133    #[error("Process error: {reason}")]
134    ProcessError { reason: String },
135
136    /// HTTP-specific errors for HTTP transports
137    #[error("HTTP error: {status_code} - {reason}")]
138    HttpError { status_code: u16, reason: String },
139
140    /// Server-Sent Events specific errors
141    #[error("SSE error: {reason}")]
142    SseError { reason: String },
143
144    /// Streaming protocol errors
145    #[error("Streaming error: {reason}")]
146    StreamingError { reason: String },
147
148    /// Transport is not connected
149    #[error("Transport not connected ({transport_type}): {reason}")]
150    NotConnected {
151        transport_type: String,
152        reason: String,
153    },
154
155    /// Generic network error
156    #[error("Network error ({transport_type}): {reason}")]
157    NetworkError {
158        transport_type: String,
159        reason: String,
160    },
161
162    /// Message serialization/deserialization error
163    #[error("Serialization error ({transport_type}): {reason}")]
164    SerializationError {
165        transport_type: String,
166        reason: String,
167    },
168
169    /// Operation timed out
170    #[error("Operation timed out ({transport_type}): {reason}")]
171    TimeoutError {
172        transport_type: String,
173        reason: String,
174    },
175
176    /// Transport unexpectedly disconnected
177    #[error("Transport disconnected ({transport_type}): {reason}")]
178    DisconnectedError {
179        transport_type: String,
180        reason: String,
181    },
182
183    /// Connection error (alias for ConnectionFailed for compatibility)
184    #[error("Connection error ({transport_type}): {reason}")]
185    ConnectionError {
186        transport_type: String,
187        reason: String,
188    },
189}
190
191/// Protocol-level errors related to MCP message handling.
192///
193/// These errors occur when messages don't conform to the MCP specification
194/// or when protocol violations are detected.
195#[derive(Error, Debug, Clone)]
196#[allow(missing_docs)]
197pub enum ProtocolError {
198    /// Invalid JSON-RPC message format
199    #[error("Invalid JSON-RPC message: {reason}")]
200    InvalidJsonRpc { reason: String },
201
202    /// Unsupported MCP protocol version
203    #[error("Unsupported protocol version: {version}, supported versions: {supported:?}")]
204    UnsupportedVersion {
205        version: String,
206        supported: Vec<String>,
207    },
208
209    /// Message ID mismatch in request/response correlation
210    #[error("Message ID mismatch: expected {expected}, got {actual}")]
211    MessageIdMismatch { expected: String, actual: String },
212
213    /// Unexpected message type received
214    #[error("Unexpected message type: expected {expected}, got {actual}")]
215    UnexpectedMessageType { expected: String, actual: String },
216
217    /// Required field missing from message
218    #[error("Missing required field '{field}' in {message_type}")]
219    MissingField { field: String, message_type: String },
220
221    /// Invalid method name for MCP operation
222    #[error("Invalid method name: {method}")]
223    InvalidMethod { method: String },
224
225    /// Server returned an error response
226    #[error("Server error {code}: {message}")]
227    ServerError { code: i32, message: String },
228
229    /// Protocol state violation (e.g., calling method before initialization)
230    #[error("Protocol state violation: {reason}")]
231    StateViolation { reason: String },
232
233    /// Protocol initialization failed
234    #[error("Protocol initialization failed: {reason}")]
235    InitializationFailed { reason: String },
236
237    /// Operation attempted before protocol initialization
238    #[error("Protocol not initialized: {reason}")]
239    NotInitialized { reason: String },
240
241    /// Invalid or malformed response
242    #[error("Invalid response: {reason}")]
243    InvalidResponse { reason: String },
244
245    /// Configuration error in protocol settings
246    #[error("Protocol configuration error: {reason}")]
247    InvalidConfig { reason: String },
248
249    /// Operation timeout
250    #[error("Protocol operation '{operation}' timed out after {timeout:?}")]
251    TimeoutError {
252        operation: String,
253        timeout: std::time::Duration,
254    },
255
256    /// Request failed
257    #[error("Request failed: {reason}")]
258    RequestFailed { reason: String },
259
260    /// Request timed out
261    #[error("Request timed out after {timeout:?}")]
262    RequestTimeout { timeout: Duration },
263}
264
265/// Validation errors for MCP capabilities and schemas.
266///
267/// These errors occur during validation of server capabilities,
268/// tool parameters, resource schemas, etc.
269#[derive(Error, Debug, Clone)]
270#[allow(missing_docs)]
271pub enum ValidationError {
272    /// Schema validation failed
273    #[error("Schema validation failed for {object_type}: {reason}")]
274    SchemaValidation { object_type: String, reason: String },
275
276    /// Capability not supported by server
277    #[error("Capability '{capability}' not supported by server")]
278    UnsupportedCapability { capability: String },
279
280    /// Tool parameter validation failed
281    #[error("Invalid parameter '{parameter}' for tool '{tool}': {reason}")]
282    InvalidToolParameter {
283        tool: String,
284        parameter: String,
285        reason: String,
286    },
287
288    /// Resource validation failed
289    #[error("Invalid resource '{resource}': {reason}")]
290    InvalidResource { resource: String, reason: String },
291
292    /// Prompt validation failed
293    #[error("Invalid prompt '{prompt}': {reason}")]
294    InvalidPrompt { prompt: String, reason: String },
295
296    /// Constraint violation (size limits, rate limits, etc.)
297    #[error("Constraint violation: {constraint} - {reason}")]
298    ConstraintViolation { constraint: String, reason: String },
299}
300
301/// Authentication and authorization errors.
302///
303/// These errors cover all aspects of authentication and authorization
304/// for different transport types and authentication schemes.
305#[derive(Error, Debug, Clone)]
306#[allow(missing_docs)]
307pub enum AuthError {
308    /// Missing required authentication credentials
309    #[error("Missing authentication credentials for {auth_type}")]
310    MissingCredentials { auth_type: String },
311
312    /// Invalid authentication credentials
313    #[error("Invalid {auth_type} credentials: {reason}")]
314    InvalidCredentials { auth_type: String, reason: String },
315
316    /// Authentication expired and needs renewal
317    #[error("Authentication expired for {auth_type}")]
318    Expired { auth_type: String },
319
320    /// Access denied for requested operation
321    #[error("Access denied: {reason}")]
322    AccessDenied { reason: String },
323
324    /// OAuth-specific errors
325    #[error("OAuth error: {error_code} - {description}")]
326    OAuth {
327        error_code: String,
328        description: String,
329    },
330
331    /// JWT token errors
332    #[error("JWT error: {reason}")]
333    Jwt { reason: String },
334}
335
336/// Configuration-related errors.
337///
338/// These errors occur when configuration files are invalid,
339/// missing required parameters, or contain conflicting settings.
340#[derive(Error, Debug, Clone)]
341#[allow(missing_docs)]
342pub enum ConfigError {
343    /// Configuration file not found
344    #[error("Configuration file not found: {path}")]
345    FileNotFound { path: String },
346
347    /// Configuration file has invalid format
348    #[error("Invalid configuration format in {path}: {reason}")]
349    InvalidFormat { path: String, reason: String },
350
351    /// Required configuration parameter is missing
352    #[error("Missing required configuration parameter: {parameter}")]
353    MissingParameter { parameter: String },
354
355    /// Configuration parameter has invalid value
356    #[error("Invalid value for parameter '{parameter}': {value} - {reason}")]
357    InvalidValue {
358        parameter: String,
359        value: String,
360        reason: String,
361    },
362
363    /// Conflicting configuration parameters
364    #[error("Conflicting configuration: {reason}")]
365    Conflict { reason: String },
366}
367
368/// Convenience type alias for Results using McpError.
369pub type McpResult<T> = Result<T, McpError>;
370
371impl McpError {
372    /// Create a new internal error with a custom message.
373    ///
374    /// This is useful for creating errors from string messages when
375    /// a more specific error type is not available.
376    ///
377    /// # Examples
378    ///
379    /// ```rust
380    /// use mcp_probe_core::error::McpError;
381    ///
382    /// let error = McpError::internal("Something went wrong");
383    /// ```
384    pub fn internal(message: impl Into<String>) -> Self {
385        Self::Internal {
386            message: message.into(),
387        }
388    }
389
390    /// Create a new timeout error.
391    ///
392    /// # Examples
393    ///
394    /// ```rust
395    /// use mcp_probe_core::error::McpError;
396    /// use std::time::Duration;
397    ///
398    /// let error = McpError::timeout("server connection", Duration::from_secs(30));
399    /// ```
400    pub fn timeout(operation: impl Into<String>, duration: std::time::Duration) -> Self {
401        Self::Timeout {
402            operation: operation.into(),
403            duration_ms: duration.as_millis() as u64,
404        }
405    }
406
407    /// Check if this error is retryable.
408    ///
409    /// Some errors (like network timeouts) may be worth retrying,
410    /// while others (like invalid credentials) are permanent failures.
411    ///
412    /// # Examples
413    ///
414    /// ```rust
415    /// use mcp_probe_core::error::{McpError, TransportError};
416    ///
417    /// let timeout_error = McpError::timeout("connection", std::time::Duration::from_secs(30));
418    /// assert!(timeout_error.is_retryable());
419    ///
420    /// let auth_error = McpError::Auth(
421    ///     mcp_probe_core::error::AuthError::InvalidCredentials {
422    ///         auth_type: "Bearer".to_string(),
423    ///         reason: "Invalid token".to_string(),
424    ///     }
425    /// );
426    /// assert!(!auth_error.is_retryable());
427    /// ```
428    pub fn is_retryable(&self) -> bool {
429        match self {
430            McpError::Transport(transport_err) => transport_err.is_retryable(),
431            McpError::Timeout { .. } => true,
432            McpError::Io { .. } => true,
433            McpError::Auth(_) => false,
434            McpError::Protocol(_) => false,
435            McpError::Validation(_) => false,
436            McpError::Config(_) => false,
437            McpError::Serialization { .. } => false,
438            McpError::Internal { .. } => false,
439        }
440    }
441
442    /// Get the error category for this error.
443    ///
444    /// This is useful for error reporting and metrics collection.
445    pub fn category(&self) -> &'static str {
446        match self {
447            McpError::Transport(_) => "transport",
448            McpError::Protocol(_) => "protocol",
449            McpError::Validation(_) => "validation",
450            McpError::Auth(_) => "auth",
451            McpError::Timeout { .. } => "timeout",
452            McpError::Config(_) => "config",
453            McpError::Serialization { .. } => "serialization",
454            McpError::Io { .. } => "io",
455            McpError::Internal { .. } => "internal",
456        }
457    }
458}
459
460impl TransportError {
461    /// Check if this transport error is retryable.
462    pub fn is_retryable(&self) -> bool {
463        match self {
464            TransportError::ConnectionFailed { .. } => true,
465            TransportError::ConnectionLost { .. } => true,
466            TransportError::ConnectionError { .. } => true,
467            TransportError::SendFailed { .. } => true,
468            TransportError::ReceiveFailed { .. } => true,
469            TransportError::NetworkError { .. } => true,
470            TransportError::TimeoutError { .. } => true,
471            TransportError::DisconnectedError { .. } => true,
472            TransportError::HttpError { status_code, .. } => {
473                // 5xx errors are generally retryable, 4xx are not
474                *status_code >= 500
475            }
476            TransportError::SseError { .. } => true,
477            TransportError::StreamingError { .. } => true,
478            TransportError::ProcessError { .. } => false,
479            TransportError::InvalidConfig { .. } => false,
480            TransportError::NotConnected { .. } => false,
481            TransportError::SerializationError { .. } => false,
482        }
483    }
484}
485
486impl From<reqwest::Error> for McpError {
487    fn from(err: reqwest::Error) -> Self {
488        if err.is_timeout() {
489            McpError::timeout("HTTP request", std::time::Duration::from_secs(30))
490        } else if err.is_connect() {
491            McpError::Transport(TransportError::ConnectionFailed {
492                transport_type: "http".to_string(),
493                reason: err.to_string(),
494            })
495        } else if let Some(status) = err.status() {
496            McpError::Transport(TransportError::HttpError {
497                status_code: status.as_u16(),
498                reason: err.to_string(),
499            })
500        } else {
501            McpError::Transport(TransportError::HttpError {
502                status_code: 0,
503                reason: err.to_string(),
504            })
505        }
506    }
507}
508
509impl From<url::ParseError> for McpError {
510    fn from(err: url::ParseError) -> Self {
511        McpError::Config(ConfigError::InvalidValue {
512            parameter: "url".to_string(),
513            value: err.to_string(),
514            reason: "Invalid URL format".to_string(),
515        })
516    }
517}
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522    use std::time::Duration;
523
524    #[test]
525    fn test_error_display() {
526        let error = McpError::timeout("test operation", Duration::from_secs(30));
527        assert_eq!(
528            error.to_string(),
529            "Operation timed out after 30000ms: test operation"
530        );
531    }
532
533    #[test]
534    fn test_retryable_errors() {
535        let timeout = McpError::timeout("test", Duration::from_secs(30));
536        assert!(timeout.is_retryable());
537
538        let auth_error = McpError::Auth(AuthError::InvalidCredentials {
539            auth_type: "Bearer".to_string(),
540            reason: "Invalid token".to_string(),
541        });
542        assert!(!auth_error.is_retryable());
543    }
544
545    #[test]
546    fn test_error_categories() {
547        let timeout = McpError::timeout("test", Duration::from_secs(30));
548        assert_eq!(timeout.category(), "timeout");
549
550        let transport_error = McpError::Transport(TransportError::ConnectionFailed {
551            transport_type: "stdio".to_string(),
552            reason: "Process failed".to_string(),
553        });
554        assert_eq!(transport_error.category(), "transport");
555    }
556
557    #[test]
558    fn test_transport_error_retryable() {
559        let connection_failed = TransportError::ConnectionFailed {
560            transport_type: "stdio".to_string(),
561            reason: "Process failed".to_string(),
562        };
563        assert!(connection_failed.is_retryable());
564
565        let invalid_config = TransportError::InvalidConfig {
566            transport_type: "stdio".to_string(),
567            reason: "Missing command".to_string(),
568        };
569        assert!(!invalid_config.is_retryable());
570    }
571}