1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(into = "i32", from = "i32")]
13pub enum McpErrorCode {
14 ParseError,
16 InvalidRequest,
18 MethodNotFound,
20 InvalidParams,
22 InternalError,
24 ToolExecutionError,
26 ResourceNotFound,
28 ResourceForbidden,
30 PromptNotFound,
32 RequestCancelled,
34 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct McpError {
81 pub code: McpErrorCode,
83 pub message: String,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub data: Option<serde_json::Value>,
88}
89
90impl McpError {
91 #[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 #[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 #[must_use]
117 pub fn parse_error(message: impl Into<String>) -> Self {
118 Self::new(McpErrorCode::ParseError, message)
119 }
120
121 #[must_use]
123 pub fn invalid_request(message: impl Into<String>) -> Self {
124 Self::new(McpErrorCode::InvalidRequest, message)
125 }
126
127 #[must_use]
129 pub fn method_not_found(_method: &str) -> Self {
130 Self::new(McpErrorCode::MethodNotFound, "Method not found")
131 }
132
133 #[must_use]
135 pub fn invalid_params(message: impl Into<String>) -> Self {
136 Self::new(McpErrorCode::InvalidParams, message)
137 }
138
139 #[must_use]
141 pub fn internal_error(message: impl Into<String>) -> Self {
142 Self::new(McpErrorCode::InternalError, message)
143 }
144
145 #[must_use]
147 pub fn tool_error(message: impl Into<String>) -> Self {
148 Self::new(McpErrorCode::ToolExecutionError, message)
149 }
150
151 #[must_use]
153 pub fn resource_not_found(uri: &str) -> Self {
154 Self::new(
155 McpErrorCode::ResourceNotFound,
156 format!("Resource not found: {uri}"),
157 )
158 }
159
160 #[must_use]
162 pub fn request_cancelled() -> Self {
163 Self::new(McpErrorCode::RequestCancelled, "Request cancelled")
164 }
165
166 #[must_use]
199 pub fn masked(&self, mask_enabled: bool) -> McpError {
200 if !mask_enabled {
201 return self.clone();
202 }
203
204 match self.code {
205 McpErrorCode::ParseError
207 | McpErrorCode::InvalidRequest
208 | McpErrorCode::MethodNotFound
209 | McpErrorCode::InvalidParams
210 | McpErrorCode::ResourceNotFound
211 | McpErrorCode::ResourceForbidden
212 | McpErrorCode::PromptNotFound
213 | McpErrorCode::RequestCancelled => self.clone(),
214
215 McpErrorCode::InternalError
217 | McpErrorCode::ToolExecutionError
218 | McpErrorCode::Custom(_) => McpError {
219 code: self.code,
220 message: "Internal server error".to_string(),
221 data: None,
222 },
223 }
224 }
225
226 #[must_use]
228 pub fn is_internal(&self) -> bool {
229 matches!(
230 self.code,
231 McpErrorCode::InternalError
232 | McpErrorCode::ToolExecutionError
233 | McpErrorCode::Custom(_)
234 )
235 }
236}
237
238impl std::fmt::Display for McpError {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 write!(f, "[{}] {}", i32::from(self.code), self.message)
241 }
242}
243
244impl std::error::Error for McpError {}
245
246impl Default for McpError {
247 fn default() -> Self {
248 Self::internal_error("Unknown error")
249 }
250}
251
252impl From<crate::CancelledError> for McpError {
253 fn from(_: crate::CancelledError) -> Self {
254 Self::request_cancelled()
255 }
256}
257
258impl From<serde_json::Error> for McpError {
259 fn from(err: serde_json::Error) -> Self {
260 Self::parse_error(err.to_string())
261 }
262}
263
264pub type McpResult<T> = Result<T, McpError>;
266
267pub type McpOutcome<T> = Outcome<T, McpError>;
281
282use asupersync::Outcome;
285use asupersync::types::CancelReason;
286
287pub trait OutcomeExt<T> {
289 fn into_mcp_result(self) -> McpResult<T>;
291
292 fn map_ok<U>(self, f: impl FnOnce(T) -> U) -> Outcome<U, McpError>;
294}
295
296impl<T> OutcomeExt<T> for Outcome<T, McpError> {
297 fn into_mcp_result(self) -> McpResult<T> {
298 match self {
299 Outcome::Ok(v) => Ok(v),
300 Outcome::Err(e) => Err(e),
301 Outcome::Cancelled(_) => Err(McpError::request_cancelled()),
302 Outcome::Panicked(_payload) => Err(McpError::internal_error("Internal server error")),
303 }
304 }
305
306 fn map_ok<U>(self, f: impl FnOnce(T) -> U) -> Outcome<U, McpError> {
307 match self {
308 Outcome::Ok(v) => Outcome::Ok(f(v)),
309 Outcome::Err(e) => Outcome::Err(e),
310 Outcome::Cancelled(r) => Outcome::Cancelled(r),
311 Outcome::Panicked(p) => Outcome::Panicked(p),
312 }
313 }
314}
315
316pub trait ResultExt<T, E> {
318 fn into_outcome(self) -> Outcome<T, E>;
320}
321
322impl<T, E> ResultExt<T, E> for Result<T, E> {
323 fn into_outcome(self) -> Outcome<T, E> {
324 match self {
325 Ok(v) => Outcome::Ok(v),
326 Err(e) => Outcome::Err(e),
327 }
328 }
329}
330
331impl<T> ResultExt<T, McpError> for Result<T, crate::CancelledError> {
332 fn into_outcome(self) -> Outcome<T, McpError> {
333 match self {
334 Ok(v) => Outcome::Ok(v),
335 Err(_) => Outcome::Cancelled(CancelReason::user("request cancelled")),
336 }
337 }
338}
339
340#[must_use]
341pub fn cancelled<T>() -> Outcome<T, McpError> {
343 Outcome::Cancelled(CancelReason::user("request cancelled"))
344}
345
346#[must_use]
347pub fn err<T>(error: McpError) -> Outcome<T, McpError> {
349 Outcome::Err(error)
350}
351
352#[must_use]
353pub fn ok<T>(value: T) -> Outcome<T, McpError> {
355 Outcome::Ok(value)
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use asupersync::types::PanicPayload;
362
363 #[test]
368 fn test_error_code_serialization() {
369 let code = McpErrorCode::MethodNotFound;
370 let value: i32 = code.into();
371 assert_eq!(value, -32601);
372 }
373
374 #[test]
375 fn test_all_standard_error_codes() {
376 assert_eq!(i32::from(McpErrorCode::ParseError), -32700);
377 assert_eq!(i32::from(McpErrorCode::InvalidRequest), -32600);
378 assert_eq!(i32::from(McpErrorCode::MethodNotFound), -32601);
379 assert_eq!(i32::from(McpErrorCode::InvalidParams), -32602);
380 assert_eq!(i32::from(McpErrorCode::InternalError), -32603);
381 assert_eq!(i32::from(McpErrorCode::ToolExecutionError), -32000);
382 assert_eq!(i32::from(McpErrorCode::ResourceNotFound), -32001);
383 assert_eq!(i32::from(McpErrorCode::ResourceForbidden), -32002);
384 assert_eq!(i32::from(McpErrorCode::PromptNotFound), -32003);
385 assert_eq!(i32::from(McpErrorCode::RequestCancelled), -32004);
386 }
387
388 #[test]
389 fn test_error_code_roundtrip() {
390 let codes = vec![
391 McpErrorCode::ParseError,
392 McpErrorCode::InvalidRequest,
393 McpErrorCode::MethodNotFound,
394 McpErrorCode::InvalidParams,
395 McpErrorCode::InternalError,
396 McpErrorCode::ToolExecutionError,
397 McpErrorCode::ResourceNotFound,
398 McpErrorCode::ResourceForbidden,
399 McpErrorCode::PromptNotFound,
400 McpErrorCode::RequestCancelled,
401 ];
402
403 for code in codes {
404 let value: i32 = code.into();
405 let roundtrip: McpErrorCode = value.into();
406 assert_eq!(code, roundtrip);
407 }
408 }
409
410 #[test]
411 fn test_custom_error_code() {
412 let custom = McpErrorCode::Custom(-99999);
413 let value: i32 = custom.into();
414 assert_eq!(value, -99999);
415
416 let from_int: McpErrorCode = (-99999).into();
417 assert!(matches!(from_int, McpErrorCode::Custom(-99999)));
418 }
419
420 #[test]
425 fn test_error_display() {
426 let err = McpError::method_not_found("tools/call");
427 assert!(err.to_string().contains("-32601"));
428 assert!(!err.to_string().contains("tools/call"));
429 }
430
431 #[test]
432 fn test_error_factory_methods() {
433 let parse = McpError::parse_error("invalid json");
434 assert_eq!(parse.code, McpErrorCode::ParseError);
435
436 let invalid_req = McpError::invalid_request("bad request");
437 assert_eq!(invalid_req.code, McpErrorCode::InvalidRequest);
438
439 let method = McpError::method_not_found("foo/bar");
440 assert_eq!(method.code, McpErrorCode::MethodNotFound);
441
442 let params = McpError::invalid_params("missing field");
443 assert_eq!(params.code, McpErrorCode::InvalidParams);
444
445 let internal = McpError::internal_error("panic");
446 assert_eq!(internal.code, McpErrorCode::InternalError);
447
448 let tool = McpError::tool_error("execution failed");
449 assert_eq!(tool.code, McpErrorCode::ToolExecutionError);
450
451 let resource = McpError::resource_not_found("file://test");
452 assert_eq!(resource.code, McpErrorCode::ResourceNotFound);
453
454 let cancelled = McpError::request_cancelled();
455 assert_eq!(cancelled.code, McpErrorCode::RequestCancelled);
456 }
457
458 #[test]
459 fn test_error_with_data() {
460 let data = serde_json::json!({"details": "more info"});
461 let err = McpError::with_data(McpErrorCode::InternalError, "error", data.clone());
462
463 assert_eq!(err.code, McpErrorCode::InternalError);
464 assert_eq!(err.message, "error");
465 assert_eq!(err.data, Some(data));
466 }
467
468 #[test]
469 fn test_error_default() {
470 let err = McpError::default();
471 assert_eq!(err.code, McpErrorCode::InternalError);
472 }
473
474 #[test]
475 fn test_error_from_cancelled() {
476 let cancelled_err = crate::CancelledError;
477 let mcp_err: McpError = cancelled_err.into();
478 assert_eq!(mcp_err.code, McpErrorCode::RequestCancelled);
479 }
480
481 #[test]
482 fn test_error_serialization() {
483 let err = McpError::method_not_found("test");
484 let json = serde_json::to_string(&err).unwrap();
485 assert!(json.contains("-32601"));
486 assert!(json.contains("Method not found"));
487 assert!(!json.contains("test"));
488 }
489
490 #[test]
495 fn test_outcome_into_mcp_result_ok() {
496 let outcome: Outcome<i32, McpError> = Outcome::Ok(42);
497 let result = outcome.into_mcp_result();
498 assert!(matches!(result, Ok(42)));
499 }
500
501 #[test]
502 fn test_outcome_into_mcp_result_err() {
503 let outcome: Outcome<i32, McpError> = Outcome::Err(McpError::internal_error("test"));
504 let result = outcome.into_mcp_result();
505 assert!(result.is_err());
506 }
507
508 #[test]
509 fn test_outcome_into_mcp_result_cancelled() {
510 let outcome: Outcome<i32, McpError> = Outcome::Cancelled(CancelReason::user("user cancel"));
511 let result = outcome.into_mcp_result();
512 assert!(result.is_err());
513 assert_eq!(result.unwrap_err().code, McpErrorCode::RequestCancelled);
514 }
515
516 #[test]
517 fn test_outcome_into_mcp_result_panicked() {
518 let outcome: Outcome<i32, McpError> = Outcome::Panicked(PanicPayload::new(
519 "PANIC_SECRET_CANARY Bearer should-not-cross-boundary",
520 ));
521 let result = outcome.into_mcp_result();
522 assert!(result.is_err());
523 let error = result.unwrap_err();
524 assert_eq!(error.code, McpErrorCode::InternalError);
525 assert_eq!(error.message, "Internal server error");
526 assert!(!error.message.contains("PANIC_SECRET_CANARY"));
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 #[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 #[test]
574 fn test_masked_preserves_client_errors() {
575 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_eq!(masked.message, "Method not found");
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 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 let masked = internal.masked(true);
627 assert_eq!(masked.message, "Internal server error");
628 assert!(masked.data.is_none());
629
630 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 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 #[test]
665 fn from_serde_json_error() {
666 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}