Skip to main content

turbomcp_core/
error.rs

1//! Unified MCP error handling - no_std compatible.
2//!
3//! This module provides a single error type [`McpError`] for all MCP operations,
4//! replacing the previous dual error types (`ServerError` + `protocol::Error`).
5//!
6//! ## Design Goals
7//!
8//! 1. **Single Error Type**: One `McpError` across all crates
9//! 2. **no_std Compatible**: Core error works without std
10//! 3. **Rich Context**: Optional detailed context when `rich-errors` feature enabled
11//! 4. **MCP Compliant**: Maps to JSON-RPC error codes per MCP spec
12//!
13//! ## Features
14//!
15//! - **Default (no_std)**: Lightweight error with kind, message, and basic context
16//! - **`rich-errors`**: Adds UUID tracking and timestamp for observability
17//!
18//! ## Example
19//!
20//! ```rust
21//! use turbomcp_core::error::{McpError, ErrorKind, McpResult};
22//!
23//! fn my_tool() -> McpResult<String> {
24//!     Err(McpError::new(ErrorKind::ToolNotFound, "calculator"))
25//! }
26//! ```
27
28use alloc::boxed::Box;
29use alloc::string::String;
30use core::fmt;
31use serde::{Deserialize, Serialize};
32
33/// Result type alias for MCP operations
34pub type McpResult<T> = core::result::Result<T, McpError>;
35
36/// Unified MCP error type
37///
38/// This is the single error type used across all TurboMCP crates in v3.
39/// It is `no_std` compatible and maps to JSON-RPC error codes per MCP spec.
40///
41/// With `rich-errors` feature enabled, includes UUID tracking and timestamps.
42///
43/// The `context` field is boxed to keep error size small for efficient Result<T, McpError> usage.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct McpError {
46    /// Unique error ID for tracing (only with `rich-errors` feature)
47    #[cfg(feature = "rich-errors")]
48    pub id: uuid::Uuid,
49    /// Error classification
50    pub kind: ErrorKind,
51    /// Human-readable error message
52    pub message: String,
53    /// Source location (file:line for debugging)
54    /// Note: Never serialized to clients to prevent information leakage
55    #[serde(skip_serializing)]
56    pub source_location: Option<String>,
57    /// Additional context (boxed to keep McpError small)
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub context: Option<alloc::boxed::Box<ErrorContext>>,
60    /// Timestamp when error occurred (only with `rich-errors` feature)
61    #[cfg(feature = "rich-errors")]
62    pub timestamp: chrono::DateTime<chrono::Utc>,
63}
64
65/// Additional error context
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67pub struct ErrorContext {
68    /// Operation being performed
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub operation: Option<String>,
71    /// Component where error occurred
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub component: Option<String>,
74    /// Request ID for tracing
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub request_id: Option<String>,
77}
78
79/// Error classification for programmatic handling.
80///
81/// This enum is `#[non_exhaustive]` — new variants may be added in future
82/// minor releases without a breaking change.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85#[non_exhaustive]
86pub enum ErrorKind {
87    // === MCP-Specific Errors ===
88    /// Tool not found (MCP -32001)
89    ToolNotFound,
90    /// Tool execution failed (MCP -32002)
91    ToolExecutionFailed,
92    /// Prompt not found (MCP -32003)
93    PromptNotFound,
94    /// Resource not found (MCP -32004)
95    ResourceNotFound,
96    /// Resource access denied (MCP -32005)
97    ResourceAccessDenied,
98    /// Capability not supported (MCP -32006)
99    CapabilityNotSupported,
100    /// Protocol version mismatch (MCP -32007)
101    ProtocolVersionMismatch,
102    /// User rejected the request (MCP -1)
103    UserRejected,
104
105    // === JSON-RPC Standard Errors ===
106    /// Parse error (-32700)
107    ParseError,
108    /// Invalid request (-32600)
109    InvalidRequest,
110    /// Method not found (-32601)
111    MethodNotFound,
112    /// Invalid params (-32602)
113    InvalidParams,
114    /// Internal error (-32603)
115    Internal,
116
117    // === General Application Errors ===
118    /// Authentication failed
119    Authentication,
120    /// Permission denied
121    PermissionDenied,
122    /// Transport/network error
123    Transport,
124    /// Operation timed out
125    Timeout,
126    /// Service unavailable
127    Unavailable,
128    /// Rate limited (-32009)
129    RateLimited,
130    /// Server overloaded (-32010)
131    ServerOverloaded,
132    /// Configuration error
133    Configuration,
134    /// External service failed
135    ExternalService,
136    /// Operation cancelled
137    Cancelled,
138    /// Security violation
139    Security,
140    /// Serialization error
141    Serialization,
142}
143
144impl McpError {
145    /// Create a new error with kind and message
146    #[must_use]
147    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
148        Self {
149            #[cfg(feature = "rich-errors")]
150            id: uuid::Uuid::new_v4(),
151            kind,
152            message: message.into(),
153            source_location: None,
154            context: None,
155            #[cfg(feature = "rich-errors")]
156            timestamp: chrono::Utc::now(),
157        }
158    }
159
160    /// Get the error ID (only available with `rich-errors` feature)
161    #[cfg(feature = "rich-errors")]
162    #[must_use]
163    pub const fn id(&self) -> uuid::Uuid {
164        self.id
165    }
166
167    /// Get the error timestamp (only available with `rich-errors` feature)
168    #[cfg(feature = "rich-errors")]
169    #[must_use]
170    pub const fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
171        self.timestamp
172    }
173
174    /// Create a validation/invalid params error
175    #[must_use]
176    pub fn invalid_params(message: impl Into<String>) -> Self {
177        Self::new(ErrorKind::InvalidParams, message)
178    }
179
180    /// Create an internal error
181    #[must_use]
182    pub fn internal(message: impl Into<String>) -> Self {
183        Self::new(ErrorKind::Internal, message)
184    }
185
186    /// Create a safe internal error with sanitized message.
187    ///
188    /// Use this for errors that may contain sensitive information (file paths,
189    /// IP addresses, connection strings, etc.). The message is automatically
190    /// sanitized to prevent information leakage per OWASP guidelines.
191    ///
192    /// # Example
193    ///
194    /// ```rust
195    /// use turbomcp_core::error::McpError;
196    ///
197    /// let err = McpError::safe_internal("Failed: postgres://admin:secret@192.168.1.1/db");
198    /// assert!(!err.message.contains("secret"));
199    /// assert!(!err.message.contains("192.168.1.1"));
200    /// ```
201    #[must_use]
202    pub fn safe_internal(message: impl Into<String>) -> Self {
203        let sanitized = crate::security::sanitize_error_message(&message.into());
204        Self::new(ErrorKind::Internal, sanitized)
205    }
206
207    /// Create a safe tool execution error with sanitized message.
208    ///
209    /// Like [`safe_internal`](Self::safe_internal), but specifically for tool execution failures.
210    #[must_use]
211    pub fn safe_tool_execution_failed(
212        tool_name: impl Into<String>,
213        reason: impl Into<String>,
214    ) -> Self {
215        let name = tool_name.into();
216        let sanitized_reason = crate::security::sanitize_error_message(&reason.into());
217        Self::new(
218            ErrorKind::ToolExecutionFailed,
219            alloc::format!("Tool '{}' failed: {}", name, sanitized_reason),
220        )
221        .with_operation("tool_execution")
222    }
223
224    /// Sanitize this error's message in-place.
225    ///
226    /// Call this before returning errors to clients in production to ensure
227    /// no sensitive information is leaked.
228    #[must_use]
229    pub fn sanitized(mut self) -> Self {
230        self.message = crate::security::sanitize_error_message(&self.message);
231        self
232    }
233
234    /// Create a parse error
235    #[must_use]
236    pub fn parse_error(message: impl Into<String>) -> Self {
237        Self::new(ErrorKind::ParseError, message)
238    }
239
240    /// Create an invalid request error
241    #[must_use]
242    pub fn invalid_request(message: impl Into<String>) -> Self {
243        Self::new(ErrorKind::InvalidRequest, message)
244    }
245
246    /// Create a method not found error
247    #[must_use]
248    pub fn method_not_found(method: impl Into<String>) -> Self {
249        let method = method.into();
250        Self::new(
251            ErrorKind::MethodNotFound,
252            alloc::format!("Method not found: {}", method),
253        )
254    }
255
256    /// Create a tool not found error
257    #[must_use]
258    pub fn tool_not_found(tool_name: impl Into<String>) -> Self {
259        let name = tool_name.into();
260        Self::new(
261            ErrorKind::ToolNotFound,
262            alloc::format!("Tool not found: {}", name),
263        )
264        .with_operation("tool_lookup")
265        .with_component("tool_registry")
266    }
267
268    /// Create a tool execution failed error
269    #[must_use]
270    pub fn tool_execution_failed(tool_name: impl Into<String>, reason: impl Into<String>) -> Self {
271        let name = tool_name.into();
272        let reason = reason.into();
273        Self::new(
274            ErrorKind::ToolExecutionFailed,
275            alloc::format!("Tool '{}' failed: {}", name, reason),
276        )
277        .with_operation("tool_execution")
278    }
279
280    /// Create a prompt not found error
281    #[must_use]
282    pub fn prompt_not_found(prompt_name: impl Into<String>) -> Self {
283        let name = prompt_name.into();
284        Self::new(
285            ErrorKind::PromptNotFound,
286            alloc::format!("Prompt not found: {}", name),
287        )
288        .with_operation("prompt_lookup")
289        .with_component("prompt_registry")
290    }
291
292    /// Create a resource not found error
293    #[must_use]
294    pub fn resource_not_found(uri: impl Into<String>) -> Self {
295        let uri = uri.into();
296        Self::new(
297            ErrorKind::ResourceNotFound,
298            alloc::format!("Resource not found: {}", uri),
299        )
300        .with_operation("resource_lookup")
301        .with_component("resource_provider")
302    }
303
304    /// Create a resource access denied error
305    #[must_use]
306    pub fn resource_access_denied(uri: impl Into<String>, reason: impl Into<String>) -> Self {
307        let uri = uri.into();
308        let reason = reason.into();
309        Self::new(
310            ErrorKind::ResourceAccessDenied,
311            alloc::format!("Access denied to '{}': {}", uri, reason),
312        )
313        .with_operation("resource_access")
314        .with_component("resource_security")
315    }
316
317    /// Create a capability not supported error
318    #[must_use]
319    pub fn capability_not_supported(capability: impl Into<String>) -> Self {
320        let cap = capability.into();
321        Self::new(
322            ErrorKind::CapabilityNotSupported,
323            alloc::format!("Capability not supported: {}", cap),
324        )
325    }
326
327    /// Create a protocol version mismatch error
328    #[must_use]
329    pub fn protocol_version_mismatch(
330        client_version: impl Into<String>,
331        server_version: impl Into<String>,
332    ) -> Self {
333        let client = client_version.into();
334        let server = server_version.into();
335        Self::new(
336            ErrorKind::ProtocolVersionMismatch,
337            alloc::format!(
338                "Protocol version mismatch: client={}, server={}",
339                client,
340                server
341            ),
342        )
343    }
344
345    /// Create a timeout error
346    #[must_use]
347    pub fn timeout(message: impl Into<String>) -> Self {
348        Self::new(ErrorKind::Timeout, message)
349    }
350
351    /// Create a transport error
352    #[must_use]
353    pub fn transport(message: impl Into<String>) -> Self {
354        Self::new(ErrorKind::Transport, message)
355    }
356
357    /// Create an authentication error
358    #[must_use]
359    pub fn authentication(message: impl Into<String>) -> Self {
360        Self::new(ErrorKind::Authentication, message)
361    }
362
363    /// Create a permission denied error
364    #[must_use]
365    pub fn permission_denied(message: impl Into<String>) -> Self {
366        Self::new(ErrorKind::PermissionDenied, message)
367    }
368
369    /// Create a rate limited error
370    #[must_use]
371    pub fn rate_limited(message: impl Into<String>) -> Self {
372        Self::new(ErrorKind::RateLimited, message)
373    }
374
375    /// Create a cancelled error
376    #[must_use]
377    pub fn cancelled(message: impl Into<String>) -> Self {
378        Self::new(ErrorKind::Cancelled, message)
379    }
380
381    /// Create a user rejected error
382    #[must_use]
383    pub fn user_rejected(message: impl Into<String>) -> Self {
384        Self::new(ErrorKind::UserRejected, message)
385    }
386
387    /// Create a serialization error
388    #[must_use]
389    pub fn serialization(message: impl Into<String>) -> Self {
390        Self::new(ErrorKind::Serialization, message)
391    }
392
393    /// Create a security error
394    #[must_use]
395    pub fn security(message: impl Into<String>) -> Self {
396        Self::new(ErrorKind::Security, message)
397    }
398
399    /// Create an unavailable error
400    #[must_use]
401    pub fn unavailable(message: impl Into<String>) -> Self {
402        Self::new(ErrorKind::Unavailable, message)
403    }
404
405    /// Create a configuration error
406    #[must_use]
407    pub fn configuration(message: impl Into<String>) -> Self {
408        Self::new(ErrorKind::Configuration, message)
409    }
410
411    /// Create an external service error
412    #[must_use]
413    pub fn external_service(message: impl Into<String>) -> Self {
414        Self::new(ErrorKind::ExternalService, message)
415    }
416
417    /// Create a server overloaded error
418    #[must_use]
419    pub fn server_overloaded() -> Self {
420        Self::new(
421            ErrorKind::ServerOverloaded,
422            "Server is currently overloaded",
423        )
424    }
425
426    /// Create an error from a JSON-RPC error code
427    #[must_use]
428    pub fn from_rpc_code(code: i32, message: impl Into<String>) -> Self {
429        let kind = match code {
430            -1 => ErrorKind::UserRejected,
431            -32001 => ErrorKind::ToolNotFound,
432            -32002 => ErrorKind::ToolExecutionFailed,
433            -32003 => ErrorKind::PromptNotFound,
434            -32004 => ErrorKind::ResourceNotFound,
435            -32005 => ErrorKind::ResourceAccessDenied,
436            -32006 => ErrorKind::CapabilityNotSupported,
437            -32007 => ErrorKind::ProtocolVersionMismatch,
438            -32008 => ErrorKind::Authentication,
439            -32009 => ErrorKind::RateLimited,
440            -32010 => ErrorKind::ServerOverloaded,
441            -32600 => ErrorKind::InvalidRequest,
442            -32601 => ErrorKind::MethodNotFound,
443            -32602 => ErrorKind::InvalidParams,
444            -32603 => ErrorKind::Internal,
445            -32700 => ErrorKind::ParseError,
446            _ => ErrorKind::Internal,
447        };
448        Self::new(kind, message)
449    }
450
451    /// Set the operation context
452    #[must_use]
453    pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
454        let ctx = self
455            .context
456            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
457        ctx.operation = Some(operation.into());
458        self
459    }
460
461    /// Set the component context
462    #[must_use]
463    pub fn with_component(mut self, component: impl Into<String>) -> Self {
464        let ctx = self
465            .context
466            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
467        ctx.component = Some(component.into());
468        self
469    }
470
471    /// Set the request ID context
472    #[must_use]
473    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
474        let ctx = self
475            .context
476            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
477        ctx.request_id = Some(request_id.into());
478        self
479    }
480
481    /// Set the source location (typically file:line)
482    #[must_use]
483    pub fn with_source_location(mut self, location: impl Into<String>) -> Self {
484        self.source_location = Some(location.into());
485        self
486    }
487
488    /// Check if this error is retryable
489    #[must_use]
490    pub const fn is_retryable(&self) -> bool {
491        matches!(
492            self.kind,
493            ErrorKind::Timeout
494                | ErrorKind::Unavailable
495                | ErrorKind::Transport
496                | ErrorKind::ExternalService
497                | ErrorKind::RateLimited
498        )
499    }
500
501    /// Check if this error is temporary
502    #[must_use]
503    pub const fn is_temporary(&self) -> bool {
504        matches!(
505            self.kind,
506            ErrorKind::Timeout
507                | ErrorKind::Unavailable
508                | ErrorKind::RateLimited
509                | ErrorKind::ExternalService
510                | ErrorKind::ServerOverloaded
511        )
512    }
513
514    /// Get the JSON-RPC error code for this error
515    #[must_use]
516    pub const fn jsonrpc_code(&self) -> i32 {
517        self.jsonrpc_error_code()
518    }
519
520    /// Get the JSON-RPC error code (canonical name)
521    #[must_use]
522    pub const fn jsonrpc_error_code(&self) -> i32 {
523        match self.kind {
524            // JSON-RPC standard
525            ErrorKind::ParseError => -32700,
526            ErrorKind::InvalidRequest => -32600,
527            ErrorKind::MethodNotFound => -32601,
528            ErrorKind::InvalidParams | ErrorKind::Serialization => -32602,
529            ErrorKind::Internal => -32603,
530            // MCP specific
531            ErrorKind::UserRejected => -1,
532            ErrorKind::ToolNotFound => -32001,
533            ErrorKind::ToolExecutionFailed => -32002,
534            ErrorKind::PromptNotFound => -32003,
535            ErrorKind::ResourceNotFound => -32004,
536            ErrorKind::ResourceAccessDenied => -32005,
537            ErrorKind::CapabilityNotSupported => -32006,
538            ErrorKind::ProtocolVersionMismatch => -32007,
539            ErrorKind::Authentication => -32008,
540            ErrorKind::RateLimited => -32009,
541            ErrorKind::ServerOverloaded => -32010,
542            // Application specific
543            ErrorKind::PermissionDenied => -32011,
544            ErrorKind::Timeout => -32012,
545            ErrorKind::Unavailable => -32013,
546            ErrorKind::Transport => -32014,
547            ErrorKind::Configuration => -32015,
548            ErrorKind::ExternalService => -32016,
549            ErrorKind::Cancelled => -32017,
550            ErrorKind::Security => -32018,
551        }
552    }
553
554    /// Get the HTTP status code equivalent
555    #[must_use]
556    pub const fn http_status(&self) -> u16 {
557        match self.kind {
558            // 4xx Client errors
559            ErrorKind::InvalidParams
560            | ErrorKind::InvalidRequest
561            | ErrorKind::UserRejected
562            | ErrorKind::ParseError => 400,
563            ErrorKind::Authentication => 401,
564            ErrorKind::PermissionDenied | ErrorKind::Security | ErrorKind::ResourceAccessDenied => {
565                403
566            }
567            ErrorKind::ToolNotFound
568            | ErrorKind::PromptNotFound
569            | ErrorKind::ResourceNotFound
570            | ErrorKind::MethodNotFound => 404,
571            ErrorKind::Timeout => 408,
572            ErrorKind::RateLimited => 429,
573            ErrorKind::Cancelled => 499,
574            // 5xx Server errors
575            ErrorKind::Internal
576            | ErrorKind::Configuration
577            | ErrorKind::Serialization
578            | ErrorKind::ToolExecutionFailed
579            | ErrorKind::CapabilityNotSupported
580            | ErrorKind::ProtocolVersionMismatch => 500,
581            ErrorKind::Transport
582            | ErrorKind::ExternalService
583            | ErrorKind::Unavailable
584            | ErrorKind::ServerOverloaded => 503,
585        }
586    }
587}
588
589impl ErrorKind {
590    /// Create ErrorKind from a JSON-RPC error code.
591    ///
592    /// Includes standard JSON-RPC codes and MCP-specific codes per 2025-11-25 spec.
593    #[must_use]
594    pub fn from_i32(code: i32) -> Self {
595        match code {
596            // MCP-specific
597            -1 => Self::UserRejected,
598            -32001 => Self::ToolNotFound,
599            -32002 => Self::ToolExecutionFailed,
600            -32003 => Self::PromptNotFound,
601            -32004 => Self::ResourceNotFound,
602            -32005 => Self::ResourceAccessDenied,
603            -32006 => Self::CapabilityNotSupported,
604            -32007 => Self::ProtocolVersionMismatch,
605            -32008 => Self::Authentication,
606            -32009 => Self::RateLimited,
607            -32010 => Self::ServerOverloaded,
608            // MCP 2025-11-25: URL elicitation required
609            -32042 => Self::CapabilityNotSupported,
610            // Standard JSON-RPC
611            -32600 => Self::InvalidRequest,
612            -32601 => Self::MethodNotFound,
613            -32602 => Self::InvalidParams,
614            -32603 => Self::Internal,
615            -32700 => Self::ParseError,
616            _ => Self::Internal,
617        }
618    }
619
620    /// Get a human-readable description
621    #[must_use]
622    pub const fn description(self) -> &'static str {
623        match self {
624            Self::ToolNotFound => "Tool not found",
625            Self::ToolExecutionFailed => "Tool execution failed",
626            Self::PromptNotFound => "Prompt not found",
627            Self::ResourceNotFound => "Resource not found",
628            Self::ResourceAccessDenied => "Resource access denied",
629            Self::CapabilityNotSupported => "Capability not supported",
630            Self::ProtocolVersionMismatch => "Protocol version mismatch",
631            Self::UserRejected => "User rejected request",
632            Self::ParseError => "Parse error",
633            Self::InvalidRequest => "Invalid request",
634            Self::MethodNotFound => "Method not found",
635            Self::InvalidParams => "Invalid parameters",
636            Self::Internal => "Internal error",
637            Self::Authentication => "Authentication failed",
638            Self::PermissionDenied => "Permission denied",
639            Self::Transport => "Transport error",
640            Self::Timeout => "Operation timed out",
641            Self::Unavailable => "Service unavailable",
642            Self::RateLimited => "Rate limit exceeded",
643            Self::ServerOverloaded => "Server overloaded",
644            Self::Configuration => "Configuration error",
645            Self::ExternalService => "External service error",
646            Self::Cancelled => "Operation cancelled",
647            Self::Security => "Security violation",
648            Self::Serialization => "Serialization error",
649        }
650    }
651}
652
653impl fmt::Display for McpError {
654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655        write!(f, "{}", self.message)?;
656        if let Some(ctx) = &self.context {
657            if let Some(op) = &ctx.operation {
658                write!(f, " (operation: {})", op)?;
659            }
660            if let Some(comp) = &ctx.component {
661                write!(f, " (component: {})", comp)?;
662            }
663        }
664        Ok(())
665    }
666}
667
668impl fmt::Display for ErrorKind {
669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670        write!(f, "{}", self.description())
671    }
672}
673
674#[cfg(feature = "std")]
675impl std::error::Error for McpError {}
676
677// =========================================================================
678// From implementations for common error types
679// =========================================================================
680
681impl From<Box<McpError>> for McpError {
682    fn from(boxed: Box<McpError>) -> Self {
683        *boxed
684    }
685}
686
687impl From<serde_json::Error> for McpError {
688    fn from(err: serde_json::Error) -> Self {
689        // Categorize serde_json errors
690        let kind = if err.is_syntax() || err.is_eof() {
691            ErrorKind::ParseError
692        } else if err.is_data() {
693            ErrorKind::InvalidParams
694        } else {
695            ErrorKind::Serialization
696        };
697        Self::new(kind, alloc::format!("JSON error: {}", err))
698    }
699}
700
701#[cfg(feature = "std")]
702impl From<std::io::Error> for McpError {
703    fn from(err: std::io::Error) -> Self {
704        use std::io::ErrorKind as IoKind;
705        let kind = match err.kind() {
706            IoKind::NotFound => ErrorKind::ResourceNotFound,
707            IoKind::PermissionDenied => ErrorKind::PermissionDenied,
708            IoKind::ConnectionRefused
709            | IoKind::ConnectionReset
710            | IoKind::ConnectionAborted
711            | IoKind::NotConnected
712            | IoKind::BrokenPipe => ErrorKind::Transport,
713            IoKind::TimedOut => ErrorKind::Timeout,
714            _ => ErrorKind::Internal,
715        };
716        Self::new(kind, alloc::format!("IO error: {}", err))
717    }
718}
719
720/// Convenience macro for creating errors with location
721#[macro_export]
722macro_rules! mcp_err {
723    ($kind:expr, $msg:expr) => {
724        $crate::error::McpError::new($kind, $msg)
725            .with_source_location(concat!(file!(), ":", line!()))
726    };
727    ($kind:expr, $fmt:expr, $($arg:tt)*) => {
728        $crate::error::McpError::new($kind, alloc::format!($fmt, $($arg)*))
729            .with_source_location(concat!(file!(), ":", line!()))
730    };
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736    use alloc::string::ToString;
737
738    #[test]
739    fn test_error_creation() {
740        let err = McpError::invalid_params("missing field");
741        assert_eq!(err.kind, ErrorKind::InvalidParams);
742        assert!(err.message.contains("missing field"));
743    }
744
745    #[test]
746    fn test_error_context() {
747        let err = McpError::internal("test")
748            .with_operation("test_op")
749            .with_component("test_comp")
750            .with_request_id("req-123");
751
752        let ctx = err.context.unwrap();
753        assert_eq!(ctx.operation, Some("test_op".to_string()));
754        assert_eq!(ctx.component, Some("test_comp".to_string()));
755        assert_eq!(ctx.request_id, Some("req-123".to_string()));
756    }
757
758    #[test]
759    fn test_jsonrpc_codes() {
760        assert_eq!(McpError::tool_not_found("x").jsonrpc_code(), -32001);
761        assert_eq!(McpError::invalid_params("x").jsonrpc_code(), -32602);
762        assert_eq!(McpError::internal("x").jsonrpc_code(), -32603);
763    }
764
765    #[test]
766    fn test_retryable() {
767        assert!(McpError::timeout("x").is_retryable());
768        assert!(McpError::rate_limited("x").is_retryable());
769        assert!(!McpError::invalid_params("x").is_retryable());
770    }
771
772    #[test]
773    fn test_http_status() {
774        assert_eq!(McpError::tool_not_found("x").http_status(), 404);
775        assert_eq!(McpError::authentication("x").http_status(), 401);
776        assert_eq!(McpError::internal("x").http_status(), 500);
777    }
778
779    #[test]
780    fn test_error_size_reasonable() {
781        // McpError should fit in 2 cache lines (128 bytes) for efficient Result<T, E>
782        assert!(
783            core::mem::size_of::<McpError>() <= 128,
784            "McpError size: {} bytes (should be ≤128)",
785            core::mem::size_of::<McpError>()
786        );
787    }
788
789    // H-15: ErrorKind::from_i32 maps all known codes
790    #[test]
791    fn test_error_kind_from_i32() {
792        // MCP-specific codes
793        assert_eq!(ErrorKind::from_i32(-32001), ErrorKind::ToolNotFound);
794        assert_eq!(ErrorKind::from_i32(-32002), ErrorKind::ToolExecutionFailed);
795        assert_eq!(ErrorKind::from_i32(-32003), ErrorKind::PromptNotFound);
796        assert_eq!(ErrorKind::from_i32(-32004), ErrorKind::ResourceNotFound);
797        assert_eq!(ErrorKind::from_i32(-32005), ErrorKind::ResourceAccessDenied);
798        assert_eq!(
799            ErrorKind::from_i32(-32006),
800            ErrorKind::CapabilityNotSupported
801        );
802        assert_eq!(
803            ErrorKind::from_i32(-32007),
804            ErrorKind::ProtocolVersionMismatch
805        );
806        assert_eq!(ErrorKind::from_i32(-32008), ErrorKind::Authentication);
807        assert_eq!(ErrorKind::from_i32(-32009), ErrorKind::RateLimited);
808        assert_eq!(ErrorKind::from_i32(-32010), ErrorKind::ServerOverloaded);
809        // MCP 2025-11-25: URL elicitation required maps to CapabilityNotSupported
810        assert_eq!(
811            ErrorKind::from_i32(-32042),
812            ErrorKind::CapabilityNotSupported
813        );
814        // Standard JSON-RPC codes
815        assert_eq!(ErrorKind::from_i32(-32600), ErrorKind::InvalidRequest);
816        assert_eq!(ErrorKind::from_i32(-32601), ErrorKind::MethodNotFound);
817        assert_eq!(ErrorKind::from_i32(-32602), ErrorKind::InvalidParams);
818        assert_eq!(ErrorKind::from_i32(-32603), ErrorKind::Internal);
819        assert_eq!(ErrorKind::from_i32(-32700), ErrorKind::ParseError);
820        // Unknown codes fall back to Internal
821        assert_eq!(ErrorKind::from_i32(-99999), ErrorKind::Internal);
822        assert_eq!(ErrorKind::from_i32(0), ErrorKind::Internal);
823    }
824}