airsprotocols_mcp/integration/
error.rs

1//! Integration Layer Error Types
2//!
3//! This module defines error types for both generic integration operations
4//! and MCP-specific operations, providing comprehensive error handling.
5
6use thiserror::Error;
7
8use crate::protocol::errors::ProtocolError;
9use crate::transport::TransportError;
10
11/// Result type for integration layer operations
12pub type IntegrationResult<T> = Result<T, IntegrationError>;
13
14/// Result type for MCP operations
15pub type McpResult<T> = Result<T, McpError>;
16
17/// Integration layer error types
18#[derive(Error, Debug)]
19pub enum IntegrationError {
20    /// Transport layer error
21    #[error("Transport error: {0}")]
22    Transport(#[from] TransportError),
23
24    /// JSON serialization/deserialization error
25    #[error("JSON error: {0}")]
26    Json(#[from] serde_json::Error),
27
28    /// Client is already shutdown
29    #[error("Client has been shutdown")]
30    Shutdown,
31
32    /// Invalid method name
33    #[error("Invalid method name: {method}")]
34    InvalidMethod { method: String },
35
36    /// Handler registration error
37    #[error("Handler registration failed: {reason}")]
38    HandlerRegistration { reason: String },
39
40    /// Message routing error
41    #[error("Message routing failed: {reason}")]
42    Routing { reason: String },
43
44    /// Response timeout
45    #[error("Response timeout after {timeout_ms}ms")]
46    Timeout { timeout_ms: u64 },
47
48    /// Unexpected response format
49    #[error("Unexpected response format: {details}")]
50    UnexpectedResponse { details: String },
51
52    /// Generic error for catch-all cases
53    #[error("Integration error: {message}")]
54    Other { message: String },
55}
56
57impl IntegrationError {
58    /// Create a timeout error
59    pub fn timeout(timeout_ms: u64) -> Self {
60        Self::Timeout { timeout_ms }
61    }
62
63    /// Create an unexpected response error
64    pub fn unexpected_response(details: impl Into<String>) -> Self {
65        Self::UnexpectedResponse {
66            details: details.into(),
67        }
68    }
69
70    /// Create a generic error
71    pub fn other(message: impl Into<String>) -> Self {
72        Self::Other {
73            message: message.into(),
74        }
75    }
76
77    /// Create a handler registration error
78    pub fn handler_registration(reason: impl Into<String>) -> Self {
79        Self::HandlerRegistration {
80            reason: reason.into(),
81        }
82    }
83
84    /// Create a routing error
85    pub fn routing(reason: impl Into<String>) -> Self {
86        Self::Routing {
87            reason: reason.into(),
88        }
89    }
90}
91
92/// MCP-specific error types
93#[derive(Debug, Error)]
94pub enum McpError {
95    /// Integration layer error (JSON-RPC, transport)
96    #[error("Integration error: {0}")]
97    Integration(#[from] IntegrationError),
98
99    /// Protocol-specific error (validation, format)
100    #[error("Protocol error: {0}")]
101    Protocol(#[from] ProtocolError),
102
103    /// Connection not established or lost
104    #[error("Not connected to MCP server")]
105    NotConnected,
106
107    /// Capability negotiation failed
108    #[error("Capability negotiation failed: {reason}")]
109    CapabilityNegotiationFailed { reason: String },
110
111    /// Server does not support requested capability
112    #[error("Server does not support {capability}")]
113    UnsupportedCapability { capability: String },
114
115    /// Resource not found
116    #[error("Resource not found: {uri}")]
117    ResourceNotFound { uri: String },
118
119    /// Tool not found
120    #[error("Tool not found: {name}")]
121    ToolNotFound { name: String },
122
123    /// Tool execution failed
124    #[error("Tool execution failed: {name} - {reason}")]
125    ToolExecutionFailed { name: String, reason: String },
126
127    /// Prompt not found
128    #[error("Prompt not found: {name}")]
129    PromptNotFound { name: String },
130
131    /// Invalid prompt arguments
132    #[error("Invalid prompt arguments for {prompt}: {reason}")]
133    InvalidPromptArguments { prompt: String, reason: String },
134
135    /// Subscription failed
136    #[error("Failed to subscribe to {uri}: {reason}")]
137    SubscriptionFailed { uri: String, reason: String },
138
139    /// Server error response
140    #[error("Server error: {message}")]
141    ServerError { message: String },
142
143    /// Timeout waiting for response
144    #[error("Operation timed out after {seconds} seconds")]
145    Timeout { seconds: u64 },
146
147    /// Invalid server response format
148    #[error("Invalid server response: {reason}")]
149    InvalidResponse { reason: String },
150
151    /// Connection already established
152    #[error("Already connected to MCP server")]
153    AlreadyConnected,
154
155    /// Operation not allowed in current state
156    #[error("Operation not allowed in current state: {state}")]
157    InvalidState { state: String },
158
159    /// Custom error for user-defined errors
160    #[error("Custom error: {message}")]
161    Custom { message: String },
162}
163
164impl McpError {
165    /// Create a new custom error
166    pub fn custom(message: impl Into<String>) -> Self {
167        Self::Custom {
168            message: message.into(),
169        }
170    }
171
172    /// Create a capability negotiation failed error
173    pub fn capability_negotiation_failed(reason: impl Into<String>) -> Self {
174        Self::CapabilityNegotiationFailed {
175            reason: reason.into(),
176        }
177    }
178
179    /// Create an unsupported capability error
180    pub fn unsupported_capability(capability: impl Into<String>) -> Self {
181        Self::UnsupportedCapability {
182            capability: capability.into(),
183        }
184    }
185
186    /// Create a resource not found error
187    pub fn resource_not_found(uri: impl Into<String>) -> Self {
188        Self::ResourceNotFound { uri: uri.into() }
189    }
190
191    /// Create a tool not found error
192    pub fn tool_not_found(name: impl Into<String>) -> Self {
193        Self::ToolNotFound { name: name.into() }
194    }
195
196    /// Create a tool execution failed error
197    pub fn tool_execution_failed(name: impl Into<String>, reason: impl Into<String>) -> Self {
198        Self::ToolExecutionFailed {
199            name: name.into(),
200            reason: reason.into(),
201        }
202    }
203
204    /// Create a prompt not found error
205    pub fn prompt_not_found(name: impl Into<String>) -> Self {
206        Self::PromptNotFound { name: name.into() }
207    }
208
209    /// Create an invalid response error
210    pub fn invalid_response(reason: impl Into<String>) -> Self {
211        Self::InvalidResponse {
212            reason: reason.into(),
213        }
214    }
215
216    /// Create an invalid request error
217    pub fn invalid_request(details: impl Into<String>) -> Self {
218        Self::InvalidResponse {
219            reason: format!("Invalid request: {}", details.into()),
220        }
221    }
222
223    /// Create a method not found error
224    pub fn method_not_found(method: impl Into<String>) -> Self {
225        Self::InvalidResponse {
226            reason: format!("Method not found: {}", method.into()),
227        }
228    }
229
230    /// Create a server error
231    pub fn server_error(message: impl Into<String>) -> Self {
232        Self::ServerError {
233            message: message.into(),
234        }
235    }
236
237    /// Create an already connected error
238    pub fn already_connected() -> Self {
239        Self::AlreadyConnected
240    }
241
242    /// Create an invalid state error
243    pub fn invalid_state(state: impl Into<String>) -> Self {
244        Self::InvalidState {
245            state: state.into(),
246        }
247    }
248
249    /// Create an internal error (alias for server_error)
250    pub fn internal_error(message: impl Into<String>) -> Self {
251        Self::ServerError {
252            message: message.into(),
253        }
254    }
255
256    /// Create an invalid prompt arguments error
257    pub fn invalid_prompt_arguments(prompt: impl Into<String>, reason: impl Into<String>) -> Self {
258        Self::InvalidPromptArguments {
259            prompt: prompt.into(),
260            reason: reason.into(),
261        }
262    }
263
264    /// Create a subscription failed error
265    pub fn subscription_failed(uri: impl Into<String>, reason: impl Into<String>) -> Self {
266        Self::SubscriptionFailed {
267            uri: uri.into(),
268            reason: reason.into(),
269        }
270    }
271
272    /// Create a timeout error
273    pub fn timeout(seconds: u64) -> Self {
274        Self::Timeout { seconds }
275    }
276
277    /// Check if this error is recoverable (can retry)
278    #[must_use]
279    pub fn is_recoverable(&self) -> bool {
280        match self {
281            McpError::Integration(_) => true, // Integration errors might be recoverable
282            McpError::Protocol(_) => false,
283            McpError::NotConnected => true, // Can reconnect
284            McpError::CapabilityNegotiationFailed { .. } => false,
285            McpError::UnsupportedCapability { .. } => false,
286            McpError::ResourceNotFound { .. } => false,
287            McpError::ToolNotFound { .. } => false,
288            McpError::ToolExecutionFailed { .. } => true, // Can retry tool
289            McpError::PromptNotFound { .. } => false,
290            McpError::InvalidPromptArguments { .. } => false,
291            McpError::SubscriptionFailed { .. } => true, // Can retry subscription
292            McpError::ServerError { .. } => true,        // Might be transient
293            McpError::Timeout { .. } => true,            // Can retry
294            McpError::InvalidResponse { .. } => false,
295            McpError::AlreadyConnected => false,
296            McpError::InvalidState { .. } => false,
297            McpError::Custom { .. } => false, // Conservative default
298        }
299    }
300
301    /// Check if this error indicates a connection problem
302    #[must_use]
303    pub fn is_connection_error(&self) -> bool {
304        match self {
305            McpError::Integration(_) => true, // Assume integration errors could be connection-related
306            McpError::NotConnected => true,
307            McpError::Timeout { .. } => true, // Could be connection timeout
308            _ => false,
309        }
310    }
311
312    /// Get the error category for telemetry/logging
313    #[must_use]
314    pub fn category(&self) -> &'static str {
315        match self {
316            McpError::Integration(_) => "integration",
317            McpError::Protocol(_) => "protocol",
318            McpError::NotConnected => "connection",
319            McpError::CapabilityNegotiationFailed { .. } => "capability",
320            McpError::UnsupportedCapability { .. } => "capability",
321            McpError::ResourceNotFound { .. } => "resource",
322            McpError::ToolNotFound { .. } => "tool",
323            McpError::ToolExecutionFailed { .. } => "tool",
324            McpError::PromptNotFound { .. } => "prompt",
325            McpError::InvalidPromptArguments { .. } => "prompt",
326            McpError::SubscriptionFailed { .. } => "subscription",
327            McpError::ServerError { .. } => "server",
328            McpError::Timeout { .. } => "timeout",
329            McpError::InvalidResponse { .. } => "response",
330            McpError::AlreadyConnected => "connection",
331            McpError::InvalidState { .. } => "state",
332            McpError::Custom { .. } => "custom",
333        }
334    }
335}
336
337// Allow direct conversion from TransportError to McpError so that `?` works
338// in examples and integration code that return McpResult<_>.
339impl From<TransportError> for McpError {
340    fn from(err: TransportError) -> Self {
341        // First convert into IntegrationError via its #[from] impl,
342        // then wrap as McpError::Integration via its #[from] impl.
343        let integration: IntegrationError = err.into();
344        McpError::from(integration)
345    }
346}
347
348// Helper trait to add MCP-specific context to integration errors
349pub trait McpErrorExt {
350    /// Convert to MCP error with additional context
351    fn mcp_context(self, context: &str) -> McpError;
352}
353
354impl McpErrorExt for IntegrationError {
355    fn mcp_context(self, context: &str) -> McpError {
356        McpError::custom(format!("{context}: {self}"))
357    }
358}
359
360// Support `?` conversions from the protocol-layer TransportError used by
361// transport adapters (alias: ProtocolTransportError at crate root).
362impl From<crate::protocol::transport::TransportError> for McpError {
363    fn from(err: crate::protocol::transport::TransportError) -> Self {
364        // Convert protocol transport error into ProtocolError first (has From impl),
365        // then leverage McpError's From<ProtocolError> implementation.
366        let perr: ProtocolError = err.into();
367        McpError::from(perr)
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_error_creation() {
377        let error = McpError::resource_not_found("file:///test.txt");
378        assert_eq!(error.to_string(), "Resource not found: file:///test.txt");
379        assert_eq!(error.category(), "resource");
380        assert!(!error.is_recoverable());
381    }
382
383    #[test]
384    fn test_tool_execution_error() {
385        let error = McpError::tool_execution_failed("grep", "Invalid regex pattern");
386        assert!(error.to_string().contains("grep"));
387        assert!(error.to_string().contains("Invalid regex pattern"));
388        assert!(error.is_recoverable());
389        assert_eq!(error.category(), "tool");
390    }
391
392    #[test]
393    fn test_timeout_error() {
394        let error = McpError::timeout(30);
395        assert!(error.to_string().contains("30 seconds"));
396        assert!(error.is_recoverable());
397        assert!(error.is_connection_error());
398        assert_eq!(error.category(), "timeout");
399    }
400
401    #[test]
402    fn test_capability_error() {
403        let error = McpError::unsupported_capability("sampling");
404        assert!(error.to_string().contains("sampling"));
405        assert!(!error.is_recoverable());
406        assert_eq!(error.category(), "capability");
407    }
408
409    #[test]
410    fn test_custom_error() {
411        let error = McpError::custom("Something went wrong");
412        assert_eq!(error.to_string(), "Custom error: Something went wrong");
413        assert!(!error.is_recoverable());
414        assert_eq!(error.category(), "custom");
415    }
416
417    #[test]
418    fn test_error_categories() {
419        assert_eq!(McpError::NotConnected.category(), "connection");
420        assert_eq!(McpError::server_error("test").category(), "server");
421        assert_eq!(McpError::invalid_response("test").category(), "response");
422    }
423
424    #[test]
425    fn test_recoverable_classification() {
426        // Recoverable errors
427        assert!(McpError::NotConnected.is_recoverable());
428        assert!(McpError::tool_execution_failed("test", "reason").is_recoverable());
429        assert!(McpError::subscription_failed("uri", "reason").is_recoverable());
430        assert!(McpError::server_error("message").is_recoverable());
431        assert!(McpError::timeout(30).is_recoverable());
432
433        // Non-recoverable errors
434        assert!(!McpError::unsupported_capability("test").is_recoverable());
435        assert!(!McpError::resource_not_found("test").is_recoverable());
436        assert!(!McpError::tool_not_found("test").is_recoverable());
437        assert!(!McpError::prompt_not_found("test").is_recoverable());
438        assert!(!McpError::invalid_prompt_arguments("test", "reason").is_recoverable());
439        assert!(!McpError::invalid_response("reason").is_recoverable());
440        assert!(!McpError::AlreadyConnected.is_recoverable());
441        assert!(!McpError::invalid_state("state").is_recoverable());
442    }
443
444    #[test]
445    fn test_connection_error_classification() {
446        // Connection errors
447        assert!(McpError::NotConnected.is_connection_error());
448        assert!(McpError::timeout(30).is_connection_error());
449
450        // Non-connection errors
451        assert!(!McpError::resource_not_found("test").is_connection_error());
452        assert!(!McpError::tool_execution_failed("test", "reason").is_connection_error());
453        assert!(!McpError::server_error("message").is_connection_error());
454        assert!(!McpError::invalid_response("reason").is_connection_error());
455    }
456}