airsprotocols_mcp/integration/
error.rs1use thiserror::Error;
7
8use crate::protocol::errors::ProtocolError;
9use crate::transport::TransportError;
10
11pub type IntegrationResult<T> = Result<T, IntegrationError>;
13
14pub type McpResult<T> = Result<T, McpError>;
16
17#[derive(Error, Debug)]
19pub enum IntegrationError {
20 #[error("Transport error: {0}")]
22 Transport(#[from] TransportError),
23
24 #[error("JSON error: {0}")]
26 Json(#[from] serde_json::Error),
27
28 #[error("Client has been shutdown")]
30 Shutdown,
31
32 #[error("Invalid method name: {method}")]
34 InvalidMethod { method: String },
35
36 #[error("Handler registration failed: {reason}")]
38 HandlerRegistration { reason: String },
39
40 #[error("Message routing failed: {reason}")]
42 Routing { reason: String },
43
44 #[error("Response timeout after {timeout_ms}ms")]
46 Timeout { timeout_ms: u64 },
47
48 #[error("Unexpected response format: {details}")]
50 UnexpectedResponse { details: String },
51
52 #[error("Integration error: {message}")]
54 Other { message: String },
55}
56
57impl IntegrationError {
58 pub fn timeout(timeout_ms: u64) -> Self {
60 Self::Timeout { timeout_ms }
61 }
62
63 pub fn unexpected_response(details: impl Into<String>) -> Self {
65 Self::UnexpectedResponse {
66 details: details.into(),
67 }
68 }
69
70 pub fn other(message: impl Into<String>) -> Self {
72 Self::Other {
73 message: message.into(),
74 }
75 }
76
77 pub fn handler_registration(reason: impl Into<String>) -> Self {
79 Self::HandlerRegistration {
80 reason: reason.into(),
81 }
82 }
83
84 pub fn routing(reason: impl Into<String>) -> Self {
86 Self::Routing {
87 reason: reason.into(),
88 }
89 }
90}
91
92#[derive(Debug, Error)]
94pub enum McpError {
95 #[error("Integration error: {0}")]
97 Integration(#[from] IntegrationError),
98
99 #[error("Protocol error: {0}")]
101 Protocol(#[from] ProtocolError),
102
103 #[error("Not connected to MCP server")]
105 NotConnected,
106
107 #[error("Capability negotiation failed: {reason}")]
109 CapabilityNegotiationFailed { reason: String },
110
111 #[error("Server does not support {capability}")]
113 UnsupportedCapability { capability: String },
114
115 #[error("Resource not found: {uri}")]
117 ResourceNotFound { uri: String },
118
119 #[error("Tool not found: {name}")]
121 ToolNotFound { name: String },
122
123 #[error("Tool execution failed: {name} - {reason}")]
125 ToolExecutionFailed { name: String, reason: String },
126
127 #[error("Prompt not found: {name}")]
129 PromptNotFound { name: String },
130
131 #[error("Invalid prompt arguments for {prompt}: {reason}")]
133 InvalidPromptArguments { prompt: String, reason: String },
134
135 #[error("Failed to subscribe to {uri}: {reason}")]
137 SubscriptionFailed { uri: String, reason: String },
138
139 #[error("Server error: {message}")]
141 ServerError { message: String },
142
143 #[error("Operation timed out after {seconds} seconds")]
145 Timeout { seconds: u64 },
146
147 #[error("Invalid server response: {reason}")]
149 InvalidResponse { reason: String },
150
151 #[error("Already connected to MCP server")]
153 AlreadyConnected,
154
155 #[error("Operation not allowed in current state: {state}")]
157 InvalidState { state: String },
158
159 #[error("Custom error: {message}")]
161 Custom { message: String },
162}
163
164impl McpError {
165 pub fn custom(message: impl Into<String>) -> Self {
167 Self::Custom {
168 message: message.into(),
169 }
170 }
171
172 pub fn capability_negotiation_failed(reason: impl Into<String>) -> Self {
174 Self::CapabilityNegotiationFailed {
175 reason: reason.into(),
176 }
177 }
178
179 pub fn unsupported_capability(capability: impl Into<String>) -> Self {
181 Self::UnsupportedCapability {
182 capability: capability.into(),
183 }
184 }
185
186 pub fn resource_not_found(uri: impl Into<String>) -> Self {
188 Self::ResourceNotFound { uri: uri.into() }
189 }
190
191 pub fn tool_not_found(name: impl Into<String>) -> Self {
193 Self::ToolNotFound { name: name.into() }
194 }
195
196 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 pub fn prompt_not_found(name: impl Into<String>) -> Self {
206 Self::PromptNotFound { name: name.into() }
207 }
208
209 pub fn invalid_response(reason: impl Into<String>) -> Self {
211 Self::InvalidResponse {
212 reason: reason.into(),
213 }
214 }
215
216 pub fn invalid_request(details: impl Into<String>) -> Self {
218 Self::InvalidResponse {
219 reason: format!("Invalid request: {}", details.into()),
220 }
221 }
222
223 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 pub fn server_error(message: impl Into<String>) -> Self {
232 Self::ServerError {
233 message: message.into(),
234 }
235 }
236
237 pub fn already_connected() -> Self {
239 Self::AlreadyConnected
240 }
241
242 pub fn invalid_state(state: impl Into<String>) -> Self {
244 Self::InvalidState {
245 state: state.into(),
246 }
247 }
248
249 pub fn internal_error(message: impl Into<String>) -> Self {
251 Self::ServerError {
252 message: message.into(),
253 }
254 }
255
256 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 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 pub fn timeout(seconds: u64) -> Self {
274 Self::Timeout { seconds }
275 }
276
277 #[must_use]
279 pub fn is_recoverable(&self) -> bool {
280 match self {
281 McpError::Integration(_) => true, McpError::Protocol(_) => false,
283 McpError::NotConnected => true, McpError::CapabilityNegotiationFailed { .. } => false,
285 McpError::UnsupportedCapability { .. } => false,
286 McpError::ResourceNotFound { .. } => false,
287 McpError::ToolNotFound { .. } => false,
288 McpError::ToolExecutionFailed { .. } => true, McpError::PromptNotFound { .. } => false,
290 McpError::InvalidPromptArguments { .. } => false,
291 McpError::SubscriptionFailed { .. } => true, McpError::ServerError { .. } => true, McpError::Timeout { .. } => true, McpError::InvalidResponse { .. } => false,
295 McpError::AlreadyConnected => false,
296 McpError::InvalidState { .. } => false,
297 McpError::Custom { .. } => false, }
299 }
300
301 #[must_use]
303 pub fn is_connection_error(&self) -> bool {
304 match self {
305 McpError::Integration(_) => true, McpError::NotConnected => true,
307 McpError::Timeout { .. } => true, _ => false,
309 }
310 }
311
312 #[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
337impl From<TransportError> for McpError {
340 fn from(err: TransportError) -> Self {
341 let integration: IntegrationError = err.into();
344 McpError::from(integration)
345 }
346}
347
348pub trait McpErrorExt {
350 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
360impl From<crate::protocol::transport::TransportError> for McpError {
363 fn from(err: crate::protocol::transport::TransportError) -> Self {
364 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 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 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 assert!(McpError::NotConnected.is_connection_error());
448 assert!(McpError::timeout(30).is_connection_error());
449
450 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}