Skip to main content

fastmcp_core/
error.rs

1//! Error types for MCP operations.
2//!
3//! Defines the standard MCP error codes and error types used throughout
4//! the FastMCP framework.
5
6use serde::{Deserialize, Serialize};
7
8/// Standard MCP/JSON-RPC error codes.
9///
10/// These follow the JSON-RPC 2.0 specification and MCP protocol.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(into = "i32", from = "i32")]
13pub enum McpErrorCode {
14    /// Invalid JSON was received.
15    ParseError,
16    /// The JSON sent is not a valid Request object.
17    InvalidRequest,
18    /// The method does not exist or is not available.
19    MethodNotFound,
20    /// Invalid method parameters.
21    InvalidParams,
22    /// Internal JSON-RPC error.
23    InternalError,
24    /// Tool execution failed.
25    ToolExecutionError,
26    /// Resource not found.
27    ResourceNotFound,
28    /// Resource access forbidden.
29    ResourceForbidden,
30    /// Prompt not found.
31    PromptNotFound,
32    /// Request was cancelled.
33    RequestCancelled,
34    /// Custom error code (server-defined).
35    Custom(i32),
36}
37
38impl From<McpErrorCode> for i32 {
39    fn from(code: McpErrorCode) -> Self {
40        match code {
41            McpErrorCode::ParseError => -32700,
42            McpErrorCode::InvalidRequest => -32600,
43            McpErrorCode::MethodNotFound => -32601,
44            McpErrorCode::InvalidParams => -32602,
45            McpErrorCode::InternalError => -32603,
46            // MCP-specific codes (use server error range -32000 to -32099)
47            McpErrorCode::ToolExecutionError => -32000,
48            McpErrorCode::ResourceNotFound => -32001,
49            McpErrorCode::ResourceForbidden => -32002,
50            McpErrorCode::PromptNotFound => -32003,
51            McpErrorCode::RequestCancelled => -32004,
52            McpErrorCode::Custom(code) => code,
53        }
54    }
55}
56
57impl From<i32> for McpErrorCode {
58    fn from(code: i32) -> Self {
59        match code {
60            -32700 => McpErrorCode::ParseError,
61            -32600 => McpErrorCode::InvalidRequest,
62            -32601 => McpErrorCode::MethodNotFound,
63            -32602 => McpErrorCode::InvalidParams,
64            -32603 => McpErrorCode::InternalError,
65            -32000 => McpErrorCode::ToolExecutionError,
66            -32001 => McpErrorCode::ResourceNotFound,
67            -32002 => McpErrorCode::ResourceForbidden,
68            -32003 => McpErrorCode::PromptNotFound,
69            -32004 => McpErrorCode::RequestCancelled,
70            code => McpErrorCode::Custom(code),
71        }
72    }
73}
74
75/// An MCP error response.
76///
77/// This maps directly to the JSON-RPC error object and can be serialized
78/// for transport.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct McpError {
81    /// The error code.
82    pub code: McpErrorCode,
83    /// A short description of the error.
84    pub message: String,
85    /// Additional error data (optional).
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub data: Option<serde_json::Value>,
88}
89
90impl McpError {
91    /// Creates a new MCP error.
92    #[must_use]
93    pub fn new(code: McpErrorCode, message: impl Into<String>) -> Self {
94        Self {
95            code,
96            message: message.into(),
97            data: None,
98        }
99    }
100
101    /// Creates a new MCP error with additional data.
102    #[must_use]
103    pub fn with_data(
104        code: McpErrorCode,
105        message: impl Into<String>,
106        data: serde_json::Value,
107    ) -> Self {
108        Self {
109            code,
110            message: message.into(),
111            data: Some(data),
112        }
113    }
114
115    /// Creates a parse error.
116    #[must_use]
117    pub fn parse_error(message: impl Into<String>) -> Self {
118        Self::new(McpErrorCode::ParseError, message)
119    }
120
121    /// Creates an invalid request error.
122    #[must_use]
123    pub fn invalid_request(message: impl Into<String>) -> Self {
124        Self::new(McpErrorCode::InvalidRequest, message)
125    }
126
127    /// Creates a method not found error.
128    #[must_use]
129    pub fn method_not_found(method: &str) -> Self {
130        Self::new(
131            McpErrorCode::MethodNotFound,
132            format!("Method not found: {method}"),
133        )
134    }
135
136    /// Creates an invalid params error.
137    #[must_use]
138    pub fn invalid_params(message: impl Into<String>) -> Self {
139        Self::new(McpErrorCode::InvalidParams, message)
140    }
141
142    /// Creates an internal error.
143    #[must_use]
144    pub fn internal_error(message: impl Into<String>) -> Self {
145        Self::new(McpErrorCode::InternalError, message)
146    }
147
148    /// Creates a tool execution error.
149    #[must_use]
150    pub fn tool_error(message: impl Into<String>) -> Self {
151        Self::new(McpErrorCode::ToolExecutionError, message)
152    }
153
154    /// Creates a resource not found error.
155    #[must_use]
156    pub fn resource_not_found(uri: &str) -> Self {
157        Self::new(
158            McpErrorCode::ResourceNotFound,
159            format!("Resource not found: {uri}"),
160        )
161    }
162
163    /// Creates a request cancelled error.
164    #[must_use]
165    pub fn request_cancelled() -> Self {
166        Self::new(McpErrorCode::RequestCancelled, "Request was cancelled")
167    }
168
169    /// Returns a masked version of this error for client responses.
170    ///
171    /// When masking is enabled, internal error details are hidden to prevent
172    /// leaking sensitive information (file paths, stack traces, internal state).
173    ///
174    /// # What gets masked
175    ///
176    /// - `InternalError`, `ToolExecutionError`, `Custom` codes: message replaced
177    ///   with "Internal server error" and data removed
178    ///
179    /// # What's preserved
180    ///
181    /// - Error code (for programmatic handling)
182    /// - Client errors (`ParseError`, `InvalidRequest`, `MethodNotFound`,
183    ///   `InvalidParams`, `ResourceNotFound`, `ResourceForbidden`, `PromptNotFound`,
184    ///   `RequestCancelled`): preserved as-is since they don't contain internal details
185    ///
186    /// # Example
187    ///
188    /// ```
189    /// use fastmcp_core::{McpError, McpErrorCode};
190    ///
191    /// let internal = McpError::internal_error("Connection failed at /etc/secrets/db.conf");
192    /// let masked = internal.masked(true);
193    /// assert_eq!(masked.message, "Internal server error");
194    /// assert!(masked.data.is_none());
195    ///
196    /// // Client errors are preserved
197    /// let client = McpError::invalid_params("Missing field 'name'");
198    /// let masked_client = client.masked(true);
199    /// assert!(masked_client.message.contains("name"));
200    /// ```
201    #[must_use]
202    pub fn masked(&self, mask_enabled: bool) -> McpError {
203        if !mask_enabled {
204            return self.clone();
205        }
206
207        match self.code {
208            // Client errors are preserved - they don't contain internal details
209            McpErrorCode::ParseError
210            | McpErrorCode::InvalidRequest
211            | McpErrorCode::MethodNotFound
212            | McpErrorCode::InvalidParams
213            | McpErrorCode::ResourceNotFound
214            | McpErrorCode::ResourceForbidden
215            | McpErrorCode::PromptNotFound
216            | McpErrorCode::RequestCancelled => self.clone(),
217
218            // Internal errors are masked
219            McpErrorCode::InternalError
220            | McpErrorCode::ToolExecutionError
221            | McpErrorCode::Custom(_) => McpError {
222                code: self.code,
223                message: "Internal server error".to_string(),
224                data: None,
225            },
226        }
227    }
228
229    /// Returns whether this error contains internal details that should be masked.
230    #[must_use]
231    pub fn is_internal(&self) -> bool {
232        matches!(
233            self.code,
234            McpErrorCode::InternalError
235                | McpErrorCode::ToolExecutionError
236                | McpErrorCode::Custom(_)
237        )
238    }
239}
240
241impl std::fmt::Display for McpError {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        write!(f, "[{}] {}", i32::from(self.code), self.message)
244    }
245}
246
247impl std::error::Error for McpError {}
248
249impl Default for McpError {
250    fn default() -> Self {
251        Self::internal_error("Unknown error")
252    }
253}
254
255impl From<crate::CancelledError> for McpError {
256    fn from(_: crate::CancelledError) -> Self {
257        Self::request_cancelled()
258    }
259}
260
261impl From<serde_json::Error> for McpError {
262    fn from(err: serde_json::Error) -> Self {
263        Self::parse_error(err.to_string())
264    }
265}
266
267/// Result type alias for MCP operations.
268pub type McpResult<T> = Result<T, McpError>;
269
270/// Outcome type alias for MCP operations with 4-valued returns.
271///
272/// This is the preferred return type for handler functions as it properly
273/// represents all possible states: success, error, cancellation, and panic.
274///
275/// # Mapping to JSON-RPC
276///
277/// | Outcome | JSON-RPC Response |
278/// |---------|-------------------|
279/// | `Ok(value)` | `{"result": value}` |
280/// | `Err(McpError)` | `{"error": {"code": ..., "message": ...}}` |
281/// | `Cancelled` | `{"error": {"code": -32004, "message": "Request cancelled"}}` |
282/// | `Panicked` | `{"error": {"code": -32603, "message": "Internal error"}}` |
283pub type McpOutcome<T> = Outcome<T, McpError>;
284
285// === Outcome Integration ===
286
287use asupersync::Outcome;
288use asupersync::types::CancelReason;
289
290/// Extension trait for converting Outcomes to MCP-friendly forms.
291pub trait OutcomeExt<T> {
292    /// Convert an Outcome to a McpResult, mapping Cancelled to RequestCancelled error.
293    fn into_mcp_result(self) -> McpResult<T>;
294
295    /// Map the success value, preserving cancellation and panic states.
296    fn map_ok<U>(self, f: impl FnOnce(T) -> U) -> Outcome<U, McpError>;
297}
298
299impl<T> OutcomeExt<T> for Outcome<T, McpError> {
300    fn into_mcp_result(self) -> McpResult<T> {
301        match self {
302            Outcome::Ok(v) => Ok(v),
303            Outcome::Err(e) => Err(e),
304            Outcome::Cancelled(_) => Err(McpError::request_cancelled()),
305            Outcome::Panicked(payload) => Err(McpError::internal_error(format!(
306                "Internal panic: {}",
307                payload.message()
308            ))),
309        }
310    }
311
312    fn map_ok<U>(self, f: impl FnOnce(T) -> U) -> Outcome<U, McpError> {
313        match self {
314            Outcome::Ok(v) => Outcome::Ok(f(v)),
315            Outcome::Err(e) => Outcome::Err(e),
316            Outcome::Cancelled(r) => Outcome::Cancelled(r),
317            Outcome::Panicked(p) => Outcome::Panicked(p),
318        }
319    }
320}
321
322/// Extension trait for converting Results to Outcomes.
323pub trait ResultExt<T, E> {
324    /// Convert a Result to an Outcome.
325    fn into_outcome(self) -> Outcome<T, E>;
326}
327
328impl<T, E> ResultExt<T, E> for Result<T, E> {
329    fn into_outcome(self) -> Outcome<T, E> {
330        match self {
331            Ok(v) => Outcome::Ok(v),
332            Err(e) => Outcome::Err(e),
333        }
334    }
335}
336
337impl<T> ResultExt<T, McpError> for Result<T, crate::CancelledError> {
338    fn into_outcome(self) -> Outcome<T, McpError> {
339        match self {
340            Ok(v) => Outcome::Ok(v),
341            Err(_) => Outcome::Cancelled(CancelReason::user("request cancelled")),
342        }
343    }
344}
345
346#[must_use]
347/// Creates an MCP error Outcome from a cancellation.
348pub fn cancelled<T>() -> Outcome<T, McpError> {
349    Outcome::Cancelled(CancelReason::user("request cancelled"))
350}
351
352#[must_use]
353/// Creates an MCP error Outcome with the given error.
354pub fn err<T>(error: McpError) -> Outcome<T, McpError> {
355    Outcome::Err(error)
356}
357
358#[must_use]
359/// Creates an MCP success Outcome.
360pub fn ok<T>(value: T) -> Outcome<T, McpError> {
361    Outcome::Ok(value)
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use asupersync::types::PanicPayload;
368
369    // ========================================
370    // McpErrorCode tests
371    // ========================================
372
373    #[test]
374    fn test_error_code_serialization() {
375        let code = McpErrorCode::MethodNotFound;
376        let value: i32 = code.into();
377        assert_eq!(value, -32601);
378    }
379
380    #[test]
381    fn test_all_standard_error_codes() {
382        assert_eq!(i32::from(McpErrorCode::ParseError), -32700);
383        assert_eq!(i32::from(McpErrorCode::InvalidRequest), -32600);
384        assert_eq!(i32::from(McpErrorCode::MethodNotFound), -32601);
385        assert_eq!(i32::from(McpErrorCode::InvalidParams), -32602);
386        assert_eq!(i32::from(McpErrorCode::InternalError), -32603);
387        assert_eq!(i32::from(McpErrorCode::ToolExecutionError), -32000);
388        assert_eq!(i32::from(McpErrorCode::ResourceNotFound), -32001);
389        assert_eq!(i32::from(McpErrorCode::ResourceForbidden), -32002);
390        assert_eq!(i32::from(McpErrorCode::PromptNotFound), -32003);
391        assert_eq!(i32::from(McpErrorCode::RequestCancelled), -32004);
392    }
393
394    #[test]
395    fn test_error_code_roundtrip() {
396        let codes = vec![
397            McpErrorCode::ParseError,
398            McpErrorCode::InvalidRequest,
399            McpErrorCode::MethodNotFound,
400            McpErrorCode::InvalidParams,
401            McpErrorCode::InternalError,
402            McpErrorCode::ToolExecutionError,
403            McpErrorCode::ResourceNotFound,
404            McpErrorCode::ResourceForbidden,
405            McpErrorCode::PromptNotFound,
406            McpErrorCode::RequestCancelled,
407        ];
408
409        for code in codes {
410            let value: i32 = code.into();
411            let roundtrip: McpErrorCode = value.into();
412            assert_eq!(code, roundtrip);
413        }
414    }
415
416    #[test]
417    fn test_custom_error_code() {
418        let custom = McpErrorCode::Custom(-99999);
419        let value: i32 = custom.into();
420        assert_eq!(value, -99999);
421
422        let from_int: McpErrorCode = (-99999).into();
423        assert!(matches!(from_int, McpErrorCode::Custom(-99999)));
424    }
425
426    // ========================================
427    // McpError tests
428    // ========================================
429
430    #[test]
431    fn test_error_display() {
432        let err = McpError::method_not_found("tools/call");
433        assert!(err.to_string().contains("-32601"));
434        assert!(err.to_string().contains("tools/call"));
435    }
436
437    #[test]
438    fn test_error_factory_methods() {
439        let parse = McpError::parse_error("invalid json");
440        assert_eq!(parse.code, McpErrorCode::ParseError);
441
442        let invalid_req = McpError::invalid_request("bad request");
443        assert_eq!(invalid_req.code, McpErrorCode::InvalidRequest);
444
445        let method = McpError::method_not_found("foo/bar");
446        assert_eq!(method.code, McpErrorCode::MethodNotFound);
447
448        let params = McpError::invalid_params("missing field");
449        assert_eq!(params.code, McpErrorCode::InvalidParams);
450
451        let internal = McpError::internal_error("panic");
452        assert_eq!(internal.code, McpErrorCode::InternalError);
453
454        let tool = McpError::tool_error("execution failed");
455        assert_eq!(tool.code, McpErrorCode::ToolExecutionError);
456
457        let resource = McpError::resource_not_found("file://test");
458        assert_eq!(resource.code, McpErrorCode::ResourceNotFound);
459
460        let cancelled = McpError::request_cancelled();
461        assert_eq!(cancelled.code, McpErrorCode::RequestCancelled);
462    }
463
464    #[test]
465    fn test_error_with_data() {
466        let data = serde_json::json!({"details": "more info"});
467        let err = McpError::with_data(McpErrorCode::InternalError, "error", data.clone());
468
469        assert_eq!(err.code, McpErrorCode::InternalError);
470        assert_eq!(err.message, "error");
471        assert_eq!(err.data, Some(data));
472    }
473
474    #[test]
475    fn test_error_default() {
476        let err = McpError::default();
477        assert_eq!(err.code, McpErrorCode::InternalError);
478    }
479
480    #[test]
481    fn test_error_from_cancelled() {
482        let cancelled_err = crate::CancelledError;
483        let mcp_err: McpError = cancelled_err.into();
484        assert_eq!(mcp_err.code, McpErrorCode::RequestCancelled);
485    }
486
487    #[test]
488    fn test_error_serialization() {
489        let err = McpError::method_not_found("test");
490        let json = serde_json::to_string(&err).unwrap();
491        assert!(json.contains("-32601"));
492        assert!(json.contains("Method not found: test"));
493    }
494
495    // ========================================
496    // Outcome extension tests
497    // ========================================
498
499    #[test]
500    fn test_outcome_into_mcp_result_ok() {
501        let outcome: Outcome<i32, McpError> = Outcome::Ok(42);
502        let result = outcome.into_mcp_result();
503        assert!(matches!(result, Ok(42)));
504    }
505
506    #[test]
507    fn test_outcome_into_mcp_result_err() {
508        let outcome: Outcome<i32, McpError> = Outcome::Err(McpError::internal_error("test"));
509        let result = outcome.into_mcp_result();
510        assert!(result.is_err());
511    }
512
513    #[test]
514    fn test_outcome_into_mcp_result_cancelled() {
515        let outcome: Outcome<i32, McpError> = Outcome::Cancelled(CancelReason::user("user cancel"));
516        let result = outcome.into_mcp_result();
517        assert!(result.is_err());
518        assert_eq!(result.unwrap_err().code, McpErrorCode::RequestCancelled);
519    }
520
521    #[test]
522    fn test_outcome_into_mcp_result_panicked() {
523        let outcome: Outcome<i32, McpError> = Outcome::Panicked(PanicPayload::new("test panic"));
524        let result = outcome.into_mcp_result();
525        assert!(result.is_err());
526        assert_eq!(result.unwrap_err().code, McpErrorCode::InternalError);
527    }
528
529    #[test]
530    fn test_outcome_map_ok() {
531        let outcome: Outcome<i32, McpError> = Outcome::Ok(21);
532        let mapped = outcome.map_ok(|x| x * 2);
533        assert!(matches!(mapped, Outcome::Ok(42)));
534    }
535
536    #[test]
537    fn test_result_ext_into_outcome() {
538        let result: Result<i32, McpError> = Ok(42);
539        let outcome = result.into_outcome();
540        assert!(matches!(outcome, Outcome::Ok(42)));
541
542        let err_result: Result<i32, McpError> = Err(McpError::internal_error("test"));
543        let outcome = err_result.into_outcome();
544        assert!(matches!(outcome, Outcome::Err(_)));
545    }
546
547    // ========================================
548    // Helper function tests
549    // ========================================
550
551    #[test]
552    fn test_helper_ok() {
553        let outcome: Outcome<i32, McpError> = ok(42);
554        assert!(matches!(outcome, Outcome::Ok(42)));
555    }
556
557    #[test]
558    fn test_helper_err() {
559        let outcome: Outcome<i32, McpError> = err(McpError::internal_error("test"));
560        assert!(matches!(outcome, Outcome::Err(_)));
561    }
562
563    #[test]
564    fn test_helper_cancelled() {
565        let outcome: Outcome<i32, McpError> = cancelled();
566        assert!(matches!(outcome, Outcome::Cancelled(_)));
567    }
568
569    // ========================================
570    // Error masking tests
571    // ========================================
572
573    #[test]
574    fn test_masked_preserves_client_errors() {
575        // Client errors should be preserved
576        let parse = McpError::parse_error("invalid json");
577        let masked = parse.masked(true);
578        assert_eq!(masked.message, "invalid json");
579
580        let invalid = McpError::invalid_request("bad request");
581        let masked = invalid.masked(true);
582        assert!(masked.message.contains("bad request"));
583
584        let method = McpError::method_not_found("unknown");
585        let masked = method.masked(true);
586        assert!(masked.message.contains("unknown"));
587
588        let params = McpError::invalid_params("missing field");
589        let masked = params.masked(true);
590        assert!(masked.message.contains("missing field"));
591
592        let resource = McpError::resource_not_found("file://test");
593        let masked = resource.masked(true);
594        assert!(masked.message.contains("file://test"));
595
596        let cancelled = McpError::request_cancelled();
597        let masked = cancelled.masked(true);
598        assert!(masked.message.contains("cancelled"));
599    }
600
601    #[test]
602    fn test_masked_hides_internal_errors() {
603        // Internal errors should be masked
604        let internal = McpError::internal_error("Connection failed at /etc/secrets/db.conf");
605        let masked = internal.masked(true);
606        assert_eq!(masked.message, "Internal server error");
607        assert!(masked.data.is_none());
608        assert_eq!(masked.code, McpErrorCode::InternalError);
609
610        let tool = McpError::tool_error("Failed: /home/user/secret.txt");
611        let masked = tool.masked(true);
612        assert_eq!(masked.message, "Internal server error");
613        assert!(masked.data.is_none());
614
615        let custom = McpError::new(McpErrorCode::Custom(-99999), "Stack trace: ...");
616        let masked = custom.masked(true);
617        assert_eq!(masked.message, "Internal server error");
618    }
619
620    #[test]
621    fn test_masked_with_data_removed() {
622        let data = serde_json::json!({"internal": "secret", "path": "/etc/passwd"});
623        let internal = McpError::with_data(McpErrorCode::InternalError, "Failure", data);
624
625        // With masking enabled
626        let masked = internal.masked(true);
627        assert_eq!(masked.message, "Internal server error");
628        assert!(masked.data.is_none());
629
630        // With masking disabled
631        let unmasked = internal.masked(false);
632        assert_eq!(unmasked.message, "Failure");
633        assert!(unmasked.data.is_some());
634    }
635
636    #[test]
637    fn test_masked_disabled() {
638        // When masking is disabled, all errors should be preserved
639        let internal = McpError::internal_error("Full details here");
640        let masked = internal.masked(false);
641        assert_eq!(masked.message, "Full details here");
642    }
643
644    #[test]
645    fn test_is_internal() {
646        assert!(McpError::internal_error("test").is_internal());
647        assert!(McpError::tool_error("test").is_internal());
648        assert!(McpError::new(McpErrorCode::Custom(-99999), "test").is_internal());
649
650        assert!(!McpError::parse_error("test").is_internal());
651        assert!(!McpError::invalid_request("test").is_internal());
652        assert!(!McpError::method_not_found("test").is_internal());
653        assert!(!McpError::invalid_params("test").is_internal());
654        assert!(!McpError::resource_not_found("test").is_internal());
655        assert!(!McpError::request_cancelled().is_internal());
656        assert!(!McpError::new(McpErrorCode::ResourceForbidden, "forbidden").is_internal());
657        assert!(!McpError::new(McpErrorCode::PromptNotFound, "not found").is_internal());
658    }
659
660    // =========================================================================
661    // Additional coverage tests (bd-2qlj)
662    // =========================================================================
663
664    #[test]
665    fn from_serde_json_error() {
666        // Trigger a real serde_json::Error via invalid JSON parse
667        let serde_err: serde_json::Error =
668            serde_json::from_str::<serde_json::Value>("{{bad json").unwrap_err();
669        let mcp_err: McpError = serde_err.into();
670        assert_eq!(mcp_err.code, McpErrorCode::ParseError);
671        assert!(!mcp_err.message.is_empty());
672    }
673
674    #[test]
675    fn map_ok_err_variant() {
676        let outcome: Outcome<i32, McpError> = Outcome::Err(McpError::internal_error("oops"));
677        let mapped = outcome.map_ok(|x| x * 2);
678        match mapped {
679            Outcome::Err(e) => assert_eq!(e.code, McpErrorCode::InternalError),
680            other => panic!("expected Err, got {other:?}"),
681        }
682    }
683
684    #[test]
685    fn map_ok_cancelled_variant() {
686        let outcome: Outcome<i32, McpError> = Outcome::Cancelled(CancelReason::user("test cancel"));
687        let mapped = outcome.map_ok(|x| x * 2);
688        assert!(matches!(mapped, Outcome::Cancelled(_)));
689    }
690
691    #[test]
692    fn map_ok_panicked_variant() {
693        let outcome: Outcome<i32, McpError> = Outcome::Panicked(PanicPayload::new("boom"));
694        let mapped = outcome.map_ok(|x| x * 2);
695        assert!(matches!(mapped, Outcome::Panicked(_)));
696    }
697
698    #[test]
699    fn result_ext_cancelled_error_ok() {
700        let result: Result<i32, crate::CancelledError> = Ok(42);
701        let outcome: Outcome<i32, McpError> = result.into_outcome();
702        assert!(matches!(outcome, Outcome::Ok(42)));
703    }
704
705    #[test]
706    fn result_ext_cancelled_error_err() {
707        let result: Result<i32, crate::CancelledError> = Err(crate::CancelledError);
708        let outcome: Outcome<i32, McpError> = result.into_outcome();
709        assert!(matches!(outcome, Outcome::Cancelled(_)));
710    }
711
712    #[test]
713    fn error_code_json_serde_roundtrip() {
714        let codes = [
715            McpErrorCode::ParseError,
716            McpErrorCode::InvalidRequest,
717            McpErrorCode::MethodNotFound,
718            McpErrorCode::InvalidParams,
719            McpErrorCode::InternalError,
720            McpErrorCode::ToolExecutionError,
721            McpErrorCode::ResourceNotFound,
722            McpErrorCode::ResourceForbidden,
723            McpErrorCode::PromptNotFound,
724            McpErrorCode::RequestCancelled,
725            McpErrorCode::Custom(-12345),
726        ];
727        for code in codes {
728            let json = serde_json::to_string(&code).unwrap();
729            let deserialized: McpErrorCode = serde_json::from_str(&json).unwrap();
730            assert_eq!(code, deserialized, "roundtrip failed for {code:?}");
731        }
732    }
733
734    #[test]
735    fn mcp_error_json_deserialization() {
736        let json = r#"{"code":-32601,"message":"Method not found: test","data":{"key":"val"}}"#;
737        let err: McpError = serde_json::from_str(json).unwrap();
738        assert_eq!(err.code, McpErrorCode::MethodNotFound);
739        assert!(err.message.contains("test"));
740        assert!(err.data.is_some());
741        assert_eq!(err.data.unwrap()["key"], "val");
742    }
743
744    #[test]
745    fn mcp_error_json_deserialization_no_data() {
746        let json = r#"{"code":-32603,"message":"Internal error"}"#;
747        let err: McpError = serde_json::from_str(json).unwrap();
748        assert_eq!(err.code, McpErrorCode::InternalError);
749        assert!(err.data.is_none());
750    }
751
752    #[test]
753    fn masked_preserves_resource_forbidden() {
754        let err = McpError::new(McpErrorCode::ResourceForbidden, "access denied");
755        let masked = err.masked(true);
756        assert_eq!(masked.message, "access denied");
757        assert_eq!(masked.code, McpErrorCode::ResourceForbidden);
758    }
759
760    #[test]
761    fn masked_preserves_prompt_not_found() {
762        let err = McpError::new(McpErrorCode::PromptNotFound, "no such prompt");
763        let masked = err.masked(true);
764        assert_eq!(masked.message, "no such prompt");
765        assert_eq!(masked.code, McpErrorCode::PromptNotFound);
766    }
767
768    #[test]
769    fn mcp_error_is_std_error() {
770        let err = McpError::internal_error("test");
771        let _: &dyn std::error::Error = &err;
772    }
773
774    #[test]
775    fn mcp_error_debug_and_clone() {
776        let err = McpError::with_data(
777            McpErrorCode::ToolExecutionError,
778            "fail",
779            serde_json::json!({"x": 1}),
780        );
781        let debug = format!("{err:?}");
782        assert!(debug.contains("McpError"));
783        assert!(debug.contains("fail"));
784        let cloned = err.clone();
785        assert_eq!(cloned.code, err.code);
786        assert_eq!(cloned.message, err.message);
787        assert_eq!(cloned.data, err.data);
788    }
789
790    #[test]
791    fn error_code_debug_clone_copy() {
792        let code = McpErrorCode::ResourceForbidden;
793        let debug = format!("{code:?}");
794        assert!(debug.contains("ResourceForbidden"));
795        let cloned = code;
796        assert_eq!(code, cloned);
797    }
798}