Skip to main content

aster/mcp/
error.rs

1//! MCP Error types
2//!
3//! This module defines structured error types for MCP operations,
4//! ensuring all errors contain a code and message as per Requirements 8.1.
5
6use std::time::Duration;
7use thiserror::Error;
8
9/// MCP error codes following the JSON-RPC 2.0 specification
10/// and MCP protocol extensions.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum McpErrorCode {
13    // Standard JSON-RPC errors
14    /// Invalid JSON was received
15    ParseError = -32700,
16    /// The JSON sent is not a valid Request object
17    InvalidRequest = -32600,
18    /// The method does not exist / is not available
19    MethodNotFound = -32601,
20    /// Invalid method parameter(s)
21    InvalidParams = -32602,
22    /// Internal JSON-RPC error
23    InternalError = -32603,
24
25    // MCP-specific errors (-32000 to -32099)
26    /// Connection-related errors
27    ConnectionError = -32000,
28    /// Transport layer errors
29    TransportError = -32001,
30    /// Protocol negotiation or handshake errors
31    ProtocolError = -32002,
32    /// Request timeout
33    TimeoutError = -32003,
34    /// Operation was cancelled
35    CancelledError = -32004,
36    /// Configuration validation errors
37    ValidationError = -32005,
38    /// Configuration loading/saving errors
39    ConfigError = -32006,
40    /// Server lifecycle errors
41    LifecycleError = -32007,
42    /// Tool execution errors
43    ToolError = -32008,
44    /// Resource access errors
45    ResourceError = -32009,
46    /// Permission denied errors
47    PermissionDenied = -32010,
48}
49
50impl McpErrorCode {
51    /// Returns the numeric code value
52    pub fn code(&self) -> i32 {
53        *self as i32
54    }
55
56    /// Returns a human-readable description of the error code
57    pub fn description(&self) -> &'static str {
58        match self {
59            Self::ParseError => "Parse error",
60            Self::InvalidRequest => "Invalid request",
61            Self::MethodNotFound => "Method not found",
62            Self::InvalidParams => "Invalid params",
63            Self::InternalError => "Internal error",
64            Self::ConnectionError => "Connection error",
65            Self::TransportError => "Transport error",
66            Self::ProtocolError => "Protocol error",
67            Self::TimeoutError => "Timeout error",
68            Self::CancelledError => "Cancelled",
69            Self::ValidationError => "Validation error",
70            Self::ConfigError => "Configuration error",
71            Self::LifecycleError => "Lifecycle error",
72            Self::ToolError => "Tool error",
73            Self::ResourceError => "Resource error",
74            Self::PermissionDenied => "Permission denied",
75        }
76    }
77}
78
79/// MCP Error type with structured code and message.
80///
81/// All MCP operation failures return this error type, which contains:
82/// - A numeric error code (following JSON-RPC 2.0 conventions)
83/// - A human-readable error message
84/// - Optional additional data
85///
86/// This satisfies Requirement 8.1: "WHEN an MCP operation fails THEN the System
87/// SHALL return a structured error with code and message"
88#[derive(Debug, Error)]
89pub enum McpError {
90    /// Connection-related errors (establishing, maintaining connections)
91    #[error("Connection error: {message}")]
92    Connection {
93        /// Error code
94        code: i32,
95        /// Human-readable error message
96        message: String,
97        /// Optional source error
98        #[source]
99        source: Option<Box<dyn std::error::Error + Send + Sync>>,
100    },
101
102    /// Transport layer errors (stdio, HTTP, WebSocket communication)
103    #[error("Transport error: {message}")]
104    Transport {
105        /// Error code
106        code: i32,
107        /// Human-readable error message
108        message: String,
109        /// Optional source error
110        #[source]
111        source: Option<Box<dyn std::error::Error + Send + Sync>>,
112    },
113
114    /// Protocol errors (handshake, version negotiation, message format)
115    #[error("Protocol error: {message}")]
116    Protocol {
117        /// Error code
118        code: i32,
119        /// Human-readable error message
120        message: String,
121    },
122
123    /// Request timeout
124    #[error("Timeout after {duration:?}: {message}")]
125    Timeout {
126        /// Error code
127        code: i32,
128        /// Human-readable error message
129        message: String,
130        /// Duration before timeout occurred
131        duration: Duration,
132    },
133
134    /// Operation was cancelled
135    #[error("Cancelled: {message}")]
136    Cancelled {
137        /// Error code
138        code: i32,
139        /// Human-readable error message
140        message: String,
141        /// Optional reason for cancellation
142        reason: Option<String>,
143    },
144
145    /// Server returned an error
146    #[error("Server error: code={code}, message={message}")]
147    Server {
148        /// Error code from server
149        code: i32,
150        /// Error message from server
151        message: String,
152        /// Optional additional data from server
153        data: Option<serde_json::Value>,
154    },
155
156    /// Validation errors (config, arguments, schema)
157    #[error("Validation error: {message}")]
158    Validation {
159        /// Error code
160        code: i32,
161        /// Human-readable error message
162        message: String,
163        /// Validation error details
164        errors: Vec<String>,
165    },
166
167    /// Configuration errors (loading, saving, parsing)
168    #[error("Configuration error: {message}")]
169    Config {
170        /// Error code
171        code: i32,
172        /// Human-readable error message
173        message: String,
174        /// Optional source error
175        #[source]
176        source: Option<Box<dyn std::error::Error + Send + Sync>>,
177    },
178
179    /// IO errors
180    #[error("IO error: {message}")]
181    Io {
182        /// Error code
183        code: i32,
184        /// Human-readable error message
185        message: String,
186        /// Source IO error
187        #[source]
188        source: std::io::Error,
189    },
190
191    /// Serialization/deserialization errors
192    #[error("Serialization error: {message}")]
193    Serialization {
194        /// Error code
195        code: i32,
196        /// Human-readable error message
197        message: String,
198        /// Source error
199        #[source]
200        source: serde_json::Error,
201    },
202
203    /// Lifecycle management errors
204    #[error("Lifecycle error: {message}")]
205    Lifecycle {
206        /// Error code
207        code: i32,
208        /// Human-readable error message
209        message: String,
210        /// Server name if applicable
211        server_name: Option<String>,
212    },
213
214    /// Tool execution errors
215    #[error("Tool error: {message}")]
216    Tool {
217        /// Error code
218        code: i32,
219        /// Human-readable error message
220        message: String,
221        /// Tool name
222        tool_name: Option<String>,
223    },
224
225    /// Permission denied errors
226    #[error("Permission denied: {message}")]
227    PermissionDenied {
228        /// Error code
229        code: i32,
230        /// Human-readable error message
231        message: String,
232        /// Tool name if applicable
233        tool_name: Option<String>,
234    },
235}
236
237impl McpError {
238    /// Returns the error code
239    pub fn code(&self) -> i32 {
240        match self {
241            Self::Connection { code, .. } => *code,
242            Self::Transport { code, .. } => *code,
243            Self::Protocol { code, .. } => *code,
244            Self::Timeout { code, .. } => *code,
245            Self::Cancelled { code, .. } => *code,
246            Self::Server { code, .. } => *code,
247            Self::Validation { code, .. } => *code,
248            Self::Config { code, .. } => *code,
249            Self::Io { code, .. } => *code,
250            Self::Serialization { code, .. } => *code,
251            Self::Lifecycle { code, .. } => *code,
252            Self::Tool { code, .. } => *code,
253            Self::PermissionDenied { code, .. } => *code,
254        }
255    }
256
257    /// Returns the error message
258    pub fn message(&self) -> &str {
259        match self {
260            Self::Connection { message, .. } => message,
261            Self::Transport { message, .. } => message,
262            Self::Protocol { message, .. } => message,
263            Self::Timeout { message, .. } => message,
264            Self::Cancelled { message, .. } => message,
265            Self::Server { message, .. } => message,
266            Self::Validation { message, .. } => message,
267            Self::Config { message, .. } => message,
268            Self::Io { message, .. } => message,
269            Self::Serialization { message, .. } => message,
270            Self::Lifecycle { message, .. } => message,
271            Self::Tool { message, .. } => message,
272            Self::PermissionDenied { message, .. } => message,
273        }
274    }
275
276    // Constructor helpers
277
278    /// Create a connection error
279    pub fn connection(message: impl Into<String>) -> Self {
280        Self::Connection {
281            code: McpErrorCode::ConnectionError.code(),
282            message: message.into(),
283            source: None,
284        }
285    }
286
287    /// Create a connection error with source
288    pub fn connection_with_source(
289        message: impl Into<String>,
290        source: impl std::error::Error + Send + Sync + 'static,
291    ) -> Self {
292        Self::Connection {
293            code: McpErrorCode::ConnectionError.code(),
294            message: message.into(),
295            source: Some(Box::new(source)),
296        }
297    }
298
299    /// Create a transport error
300    pub fn transport(message: impl Into<String>) -> Self {
301        Self::Transport {
302            code: McpErrorCode::TransportError.code(),
303            message: message.into(),
304            source: None,
305        }
306    }
307
308    /// Create a transport error with source
309    pub fn transport_with_source(
310        message: impl Into<String>,
311        source: impl std::error::Error + Send + Sync + 'static,
312    ) -> Self {
313        Self::Transport {
314            code: McpErrorCode::TransportError.code(),
315            message: message.into(),
316            source: Some(Box::new(source)),
317        }
318    }
319
320    /// Create a protocol error
321    pub fn protocol(message: impl Into<String>) -> Self {
322        Self::Protocol {
323            code: McpErrorCode::ProtocolError.code(),
324            message: message.into(),
325        }
326    }
327
328    /// Create a timeout error
329    pub fn timeout(message: impl Into<String>, duration: Duration) -> Self {
330        Self::Timeout {
331            code: McpErrorCode::TimeoutError.code(),
332            message: message.into(),
333            duration,
334        }
335    }
336
337    /// Create a cancelled error
338    pub fn cancelled(message: impl Into<String>, reason: Option<String>) -> Self {
339        Self::Cancelled {
340            code: McpErrorCode::CancelledError.code(),
341            message: message.into(),
342            reason,
343        }
344    }
345
346    /// Create a server error
347    pub fn server(code: i32, message: impl Into<String>, data: Option<serde_json::Value>) -> Self {
348        Self::Server {
349            code,
350            message: message.into(),
351            data,
352        }
353    }
354
355    /// Create a validation error
356    pub fn validation(message: impl Into<String>, errors: Vec<String>) -> Self {
357        Self::Validation {
358            code: McpErrorCode::ValidationError.code(),
359            message: message.into(),
360            errors,
361        }
362    }
363
364    /// Create a config error
365    pub fn config(message: impl Into<String>) -> Self {
366        Self::Config {
367            code: McpErrorCode::ConfigError.code(),
368            message: message.into(),
369            source: None,
370        }
371    }
372
373    /// Create a config error with source
374    pub fn config_with_source(
375        message: impl Into<String>,
376        source: impl std::error::Error + Send + Sync + 'static,
377    ) -> Self {
378        Self::Config {
379            code: McpErrorCode::ConfigError.code(),
380            message: message.into(),
381            source: Some(Box::new(source)),
382        }
383    }
384
385    /// Create a lifecycle error
386    pub fn lifecycle(message: impl Into<String>, server_name: Option<String>) -> Self {
387        Self::Lifecycle {
388            code: McpErrorCode::LifecycleError.code(),
389            message: message.into(),
390            server_name,
391        }
392    }
393
394    /// Create a tool error
395    pub fn tool(message: impl Into<String>, tool_name: Option<String>) -> Self {
396        Self::Tool {
397            code: McpErrorCode::ToolError.code(),
398            message: message.into(),
399            tool_name,
400        }
401    }
402
403    /// Create a permission denied error
404    pub fn permission_denied(message: impl Into<String>) -> Self {
405        Self::PermissionDenied {
406            code: McpErrorCode::PermissionDenied.code(),
407            message: message.into(),
408            tool_name: None,
409        }
410    }
411
412    /// Create a permission denied error with tool name
413    pub fn permission_denied_for_tool(
414        message: impl Into<String>,
415        tool_name: impl Into<String>,
416    ) -> Self {
417        Self::PermissionDenied {
418            code: McpErrorCode::PermissionDenied.code(),
419            message: message.into(),
420            tool_name: Some(tool_name.into()),
421        }
422    }
423}
424
425impl From<std::io::Error> for McpError {
426    fn from(err: std::io::Error) -> Self {
427        Self::Io {
428            code: McpErrorCode::InternalError.code(),
429            message: err.to_string(),
430            source: err,
431        }
432    }
433}
434
435impl From<serde_json::Error> for McpError {
436    fn from(err: serde_json::Error) -> Self {
437        Self::Serialization {
438            code: McpErrorCode::ParseError.code(),
439            message: err.to_string(),
440            source: err,
441        }
442    }
443}
444
445/// Convert from rmcp::ErrorData to McpError
446impl From<rmcp::ErrorData> for McpError {
447    fn from(err: rmcp::ErrorData) -> Self {
448        Self::Server {
449            code: err.code.0,
450            message: err.message.to_string(),
451            data: err.data,
452        }
453    }
454}
455
456/// Result type alias for MCP operations
457pub type McpResult<T> = Result<T, McpError>;
458
459/// Structured error representation for serialization
460///
461/// This struct provides a JSON-serializable representation of MCP errors
462/// that always includes code and message fields as required by Requirements 8.1.
463#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct StructuredError {
465    /// Error code (following JSON-RPC 2.0 conventions)
466    pub code: i32,
467    /// Human-readable error message
468    pub message: String,
469    /// Optional additional data
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub data: Option<serde_json::Value>,
472}
473
474impl StructuredError {
475    /// Create a new structured error
476    pub fn new(code: i32, message: impl Into<String>) -> Self {
477        Self {
478            code,
479            message: message.into(),
480            data: None,
481        }
482    }
483
484    /// Create a structured error with additional data
485    pub fn with_data(code: i32, message: impl Into<String>, data: serde_json::Value) -> Self {
486        Self {
487            code,
488            message: message.into(),
489            data: Some(data),
490        }
491    }
492}
493
494impl From<&McpError> for StructuredError {
495    fn from(err: &McpError) -> Self {
496        let data = match err {
497            McpError::Validation { errors, .. } => Some(serde_json::json!({ "errors": errors })),
498            McpError::Server { data, .. } => data.clone(),
499            McpError::Timeout { duration, .. } => {
500                Some(serde_json::json!({ "duration_ms": duration.as_millis() }))
501            }
502            McpError::Cancelled { reason, .. } => {
503                reason.as_ref().map(|r| serde_json::json!({ "reason": r }))
504            }
505            McpError::Lifecycle { server_name, .. } => server_name
506                .as_ref()
507                .map(|n| serde_json::json!({ "server_name": n })),
508            McpError::Tool { tool_name, .. } => tool_name
509                .as_ref()
510                .map(|n| serde_json::json!({ "tool_name": n })),
511            McpError::PermissionDenied { tool_name, .. } => tool_name
512                .as_ref()
513                .map(|n| serde_json::json!({ "tool_name": n })),
514            _ => None,
515        };
516
517        Self {
518            code: err.code(),
519            message: err.message().to_string(),
520            data,
521        }
522    }
523}
524
525impl From<McpError> for StructuredError {
526    fn from(err: McpError) -> Self {
527        StructuredError::from(&err)
528    }
529}
530
531use serde::{Deserialize, Serialize};
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    #[test]
538    fn test_error_code_values() {
539        assert_eq!(McpErrorCode::ParseError.code(), -32700);
540        assert_eq!(McpErrorCode::InvalidRequest.code(), -32600);
541        assert_eq!(McpErrorCode::ConnectionError.code(), -32000);
542    }
543
544    #[test]
545    fn test_error_has_code_and_message() {
546        let err = McpError::connection("test connection error");
547        assert_eq!(err.code(), McpErrorCode::ConnectionError.code());
548        assert_eq!(err.message(), "test connection error");
549    }
550
551    #[test]
552    fn test_timeout_error() {
553        let err = McpError::timeout("request timed out", Duration::from_secs(30));
554        assert_eq!(err.code(), McpErrorCode::TimeoutError.code());
555        assert!(err.message().contains("request timed out"));
556    }
557
558    #[test]
559    fn test_validation_error_with_details() {
560        let err = McpError::validation(
561            "invalid configuration",
562            vec!["missing field: command".to_string()],
563        );
564        assert_eq!(err.code(), McpErrorCode::ValidationError.code());
565
566        if let McpError::Validation { errors, .. } = err {
567            assert_eq!(errors.len(), 1);
568            assert!(errors[0].contains("missing field"));
569        } else {
570            panic!("Expected Validation error");
571        }
572    }
573
574    #[test]
575    fn test_server_error() {
576        let err = McpError::server(-32001, "server unavailable", None);
577        assert_eq!(err.code(), -32001);
578        assert_eq!(err.message(), "server unavailable");
579    }
580
581    #[test]
582    fn test_io_error_conversion() {
583        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
584        let mcp_err: McpError = io_err.into();
585        assert_eq!(mcp_err.code(), McpErrorCode::InternalError.code());
586        assert!(mcp_err.message().contains("file not found"));
587    }
588
589    #[test]
590    fn test_error_display() {
591        let err = McpError::connection("failed to connect");
592        let display = format!("{}", err);
593        assert!(display.contains("Connection error"));
594        assert!(display.contains("failed to connect"));
595    }
596
597    #[test]
598    fn test_structured_error_new() {
599        let err = StructuredError::new(-32000, "test error");
600        assert_eq!(err.code, -32000);
601        assert_eq!(err.message, "test error");
602        assert!(err.data.is_none());
603    }
604
605    #[test]
606    fn test_structured_error_with_data() {
607        let data = serde_json::json!({"key": "value"});
608        let err = StructuredError::with_data(-32000, "test error", data.clone());
609        assert_eq!(err.code, -32000);
610        assert_eq!(err.message, "test error");
611        assert_eq!(err.data, Some(data));
612    }
613
614    #[test]
615    fn test_structured_error_from_mcp_error() {
616        let mcp_err = McpError::connection("connection failed");
617        let structured: StructuredError = (&mcp_err).into();
618
619        assert_eq!(structured.code, McpErrorCode::ConnectionError.code());
620        assert_eq!(structured.message, "connection failed");
621    }
622
623    #[test]
624    fn test_structured_error_from_validation_error() {
625        let mcp_err = McpError::validation(
626            "invalid config",
627            vec!["missing field".to_string(), "invalid value".to_string()],
628        );
629        let structured: StructuredError = (&mcp_err).into();
630
631        assert_eq!(structured.code, McpErrorCode::ValidationError.code());
632        assert_eq!(structured.message, "invalid config");
633        assert!(structured.data.is_some());
634
635        let data = structured.data.unwrap();
636        let errors = data.get("errors").unwrap().as_array().unwrap();
637        assert_eq!(errors.len(), 2);
638    }
639
640    #[test]
641    fn test_structured_error_from_timeout_error() {
642        let mcp_err = McpError::timeout("request timed out", Duration::from_secs(30));
643        let structured: StructuredError = (&mcp_err).into();
644
645        assert_eq!(structured.code, McpErrorCode::TimeoutError.code());
646        assert!(structured.data.is_some());
647
648        let data = structured.data.unwrap();
649        assert_eq!(data.get("duration_ms").unwrap().as_u64().unwrap(), 30000);
650    }
651
652    #[test]
653    fn test_structured_error_serialization() {
654        let err = StructuredError::new(-32000, "test error");
655        let json = serde_json::to_string(&err).unwrap();
656
657        assert!(json.contains("\"code\":-32000"));
658        assert!(json.contains("\"message\":\"test error\""));
659        // data should not be present when None
660        assert!(!json.contains("\"data\""));
661    }
662
663    #[test]
664    fn test_all_error_variants_have_code_and_message() {
665        // Test that all error variants can be converted to structured errors
666        // with valid code and message (Requirements 8.1)
667
668        let errors: Vec<McpError> = vec![
669            McpError::connection("test"),
670            McpError::transport("test"),
671            McpError::protocol("test"),
672            McpError::timeout("test", Duration::from_secs(1)),
673            McpError::cancelled("test", None),
674            McpError::server(-32000, "test", None),
675            McpError::validation("test", vec![]),
676            McpError::config("test"),
677            McpError::lifecycle("test", None),
678            McpError::tool("test", None),
679            McpError::permission_denied("test"),
680        ];
681
682        for err in errors {
683            let structured: StructuredError = (&err).into();
684            assert!(structured.code != 0, "Error code should not be 0");
685            assert!(
686                !structured.message.is_empty(),
687                "Error message should not be empty"
688            );
689        }
690    }
691}