1use miette::Diagnostic;
111use serde::{Deserialize, Serialize};
112use std::fmt;
113use thiserror::Error;
114
115pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
117
118#[derive(Debug)]
124pub struct InvalidParamsDetails {
125 pub method: String,
127 pub message: String,
129 pub param_path: Option<String>,
131 pub expected: Option<String>,
133 pub actual: Option<String>,
135 pub source: Option<BoxError>,
137}
138
139impl fmt::Display for InvalidParamsDetails {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 write!(f, "Invalid params for '{}': {}", self.method, self.message)
142 }
143}
144
145impl std::error::Error for InvalidParamsDetails {
146 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
147 self.source
148 .as_ref()
149 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
150 }
151}
152
153#[derive(Debug)]
155pub struct TransportDetails {
156 pub kind: TransportErrorKind,
158 pub message: String,
160 pub context: TransportContext,
162 pub source: Option<BoxError>,
164}
165
166impl fmt::Display for TransportDetails {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 write!(f, "Transport error ({}): {}", self.kind, self.message)
169 }
170}
171
172impl std::error::Error for TransportDetails {
173 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
174 self.source
175 .as_ref()
176 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
177 }
178}
179
180#[derive(Debug)]
182pub struct ToolExecutionDetails {
183 pub tool: String,
185 pub message: String,
187 pub is_recoverable: bool,
189 pub data: Option<serde_json::Value>,
191 pub source: Option<BoxError>,
193}
194
195impl fmt::Display for ToolExecutionDetails {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 write!(f, "Tool '{}' failed: {}", self.tool, self.message)
198 }
199}
200
201impl std::error::Error for ToolExecutionDetails {
202 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
203 self.source
204 .as_ref()
205 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
206 }
207}
208
209#[derive(Debug)]
211pub struct HandshakeDetails {
212 pub message: String,
214 pub client_version: Option<String>,
216 pub server_version: Option<String>,
218 pub source: Option<BoxError>,
220}
221
222impl fmt::Display for HandshakeDetails {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 write!(f, "Handshake failed: {}", self.message)
225 }
226}
227
228impl std::error::Error for HandshakeDetails {
229 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
230 self.source
231 .as_ref()
232 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
233 }
234}
235
236#[derive(Error, Diagnostic, Debug)]
245#[allow(clippy::large_enum_variant)] pub enum McpError {
247 #[error("Parse error: {message}")]
252 #[diagnostic(
253 code(mcp::protocol::parse_error),
254 help("Ensure the message is valid JSON-RPC 2.0 format")
255 )]
256 Parse {
257 message: String,
259 #[source]
261 source: Option<BoxError>,
262 },
263
264 #[error("Invalid request: {message}")]
266 #[diagnostic(code(mcp::protocol::invalid_request))]
267 InvalidRequest {
268 message: String,
270 #[source]
272 source: Option<BoxError>,
273 },
274
275 #[error("Method not found: {method}")]
277 #[diagnostic(code(mcp::protocol::method_not_found))]
278 MethodNotFound {
279 method: String,
281 available: Box<[String]>,
283 },
284
285 #[error("Invalid params for '{}': {}", .0.method, .0.message)]
287 #[diagnostic(code(mcp::protocol::invalid_params))]
288 InvalidParams(#[source] Box<InvalidParamsDetails>),
289
290 #[error("Internal error: {message}")]
292 #[diagnostic(code(mcp::protocol::internal_error), severity(error))]
293 Internal {
294 message: String,
296 #[source]
298 source: Option<BoxError>,
299 },
300
301 #[error("Transport error ({}): {}", .0.kind, .0.message)]
306 #[diagnostic(code(mcp::transport::error))]
307 Transport(#[source] Box<TransportDetails>),
308
309 #[error("Tool '{}' failed: {}", .0.tool, .0.message)]
314 #[diagnostic(code(mcp::tool::execution_error))]
315 ToolExecution(#[source] Box<ToolExecutionDetails>),
316
317 #[error("Resource not found: {uri}")]
322 #[diagnostic(
323 code(mcp::resource::not_found),
324 help("Verify the URI is correct and the resource exists")
325 )]
326 ResourceNotFound {
327 uri: String,
329 },
330
331 #[error("Resource access denied: {uri}")]
333 #[diagnostic(code(mcp::resource::access_denied))]
334 ResourceAccessDenied {
335 uri: String,
337 reason: Option<String>,
339 },
340
341 #[error("Connection failed: {message}")]
346 #[diagnostic(code(mcp::connection::failed))]
347 ConnectionFailed {
348 message: String,
350 #[source]
352 source: Option<BoxError>,
353 },
354
355 #[error("Session expired: {session_id}")]
357 #[diagnostic(
358 code(mcp::session::expired),
359 help("Re-initialize the connection to continue")
360 )]
361 SessionExpired {
362 session_id: String,
364 },
365
366 #[error("Handshake failed: {}", .0.message)]
368 #[diagnostic(code(mcp::handshake::failed))]
369 HandshakeFailed(#[source] Box<HandshakeDetails>),
370
371 #[error("Capability not supported: {capability}")]
376 #[diagnostic(code(mcp::capability::not_supported))]
377 CapabilityNotSupported {
378 capability: String,
380 available: Box<[String]>,
382 },
383
384 #[error("User rejected: {message}")]
389 #[diagnostic(code(mcp::user::rejected))]
390 UserRejected {
391 message: String,
393 operation: String,
395 },
396
397 #[error("Timeout after {duration:?}: {operation}")]
402 #[diagnostic(
403 code(mcp::timeout),
404 help("Consider increasing the timeout or checking connectivity")
405 )]
406 Timeout {
407 operation: String,
409 duration: std::time::Duration,
411 },
412
413 #[error("Operation cancelled: {operation}")]
418 #[diagnostic(code(mcp::cancelled))]
419 Cancelled {
420 operation: String,
422 reason: Option<String>,
424 },
425
426 #[error("{context}: {source}")]
431 #[diagnostic(code(mcp::context))]
432 WithContext {
433 context: String,
435 #[source]
437 source: Box<McpError>,
438 },
439
440 #[error("Internal error: {message}")]
445 #[diagnostic(code(mcp::internal))]
446 InternalMessage {
447 message: String,
449 },
450}
451
452#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
454#[serde(rename_all = "snake_case")]
455pub enum TransportErrorKind {
456 ConnectionFailed,
458 ConnectionClosed,
460 ReadFailed,
462 WriteFailed,
464 TlsError,
466 DnsResolutionFailed,
468 Timeout,
470 InvalidMessage,
472 ProtocolViolation,
474 ResourceExhausted,
476 RateLimited,
478}
479
480impl fmt::Display for TransportErrorKind {
481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
482 match self {
483 Self::ConnectionFailed => write!(f, "connection failed"),
484 Self::ConnectionClosed => write!(f, "connection closed"),
485 Self::ReadFailed => write!(f, "read failed"),
486 Self::WriteFailed => write!(f, "write failed"),
487 Self::TlsError => write!(f, "TLS error"),
488 Self::DnsResolutionFailed => write!(f, "DNS resolution failed"),
489 Self::Timeout => write!(f, "timeout"),
490 Self::InvalidMessage => write!(f, "invalid message"),
491 Self::ProtocolViolation => write!(f, "protocol violation"),
492 Self::ResourceExhausted => write!(f, "resource exhausted"),
493 Self::RateLimited => write!(f, "rate limited"),
494 }
495 }
496}
497
498#[derive(Debug, Clone, Default, Serialize, Deserialize)]
500pub struct TransportContext {
501 #[serde(skip_serializing_if = "Option::is_none")]
503 pub transport_type: Option<String>,
504 #[serde(skip_serializing_if = "Option::is_none")]
506 pub remote_addr: Option<String>,
507 #[serde(skip_serializing_if = "Option::is_none")]
509 pub local_addr: Option<String>,
510 #[serde(skip_serializing_if = "Option::is_none")]
512 pub bytes_sent: Option<u64>,
513 #[serde(skip_serializing_if = "Option::is_none")]
515 pub bytes_received: Option<u64>,
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub connection_duration_ms: Option<u64>,
519}
520
521impl TransportContext {
522 #[must_use]
524 pub fn new(transport_type: impl Into<String>) -> Self {
525 Self {
526 transport_type: Some(transport_type.into()),
527 ..Default::default()
528 }
529 }
530
531 #[must_use]
533 pub fn with_remote_addr(mut self, addr: impl Into<String>) -> Self {
534 self.remote_addr = Some(addr.into());
535 self
536 }
537
538 #[must_use]
540 pub fn with_local_addr(mut self, addr: impl Into<String>) -> Self {
541 self.local_addr = Some(addr.into());
542 self
543 }
544}
545
546impl McpError {
551 pub fn parse(message: impl Into<String>) -> Self {
553 Self::Parse {
554 message: message.into(),
555 source: None,
556 }
557 }
558
559 pub fn parse_with_source<E: std::error::Error + Send + Sync + 'static>(
561 message: impl Into<String>,
562 source: E,
563 ) -> Self {
564 Self::Parse {
565 message: message.into(),
566 source: Some(Box::new(source)),
567 }
568 }
569
570 pub fn invalid_request(message: impl Into<String>) -> Self {
572 Self::InvalidRequest {
573 message: message.into(),
574 source: None,
575 }
576 }
577
578 pub fn method_not_found(method: impl Into<String>) -> Self {
580 Self::MethodNotFound {
581 method: method.into(),
582 available: Box::new([]),
583 }
584 }
585
586 pub fn method_not_found_with_suggestions(
588 method: impl Into<String>,
589 available: Vec<String>,
590 ) -> Self {
591 Self::MethodNotFound {
592 method: method.into(),
593 available: available.into_boxed_slice(),
594 }
595 }
596
597 pub fn invalid_params(method: impl Into<String>, message: impl Into<String>) -> Self {
599 Self::InvalidParams(Box::new(InvalidParamsDetails {
600 method: method.into(),
601 message: message.into(),
602 param_path: None,
603 expected: None,
604 actual: None,
605 source: None,
606 }))
607 }
608
609 pub fn invalid_params_detailed(
611 method: impl Into<String>,
612 message: impl Into<String>,
613 param_path: Option<String>,
614 expected: Option<String>,
615 actual: Option<String>,
616 ) -> Self {
617 Self::InvalidParams(Box::new(InvalidParamsDetails {
618 method: method.into(),
619 message: message.into(),
620 param_path,
621 expected,
622 actual,
623 source: None,
624 }))
625 }
626
627 pub fn internal(message: impl Into<String>) -> Self {
629 Self::Internal {
630 message: message.into(),
631 source: None,
632 }
633 }
634
635 pub fn internal_with_source<E: std::error::Error + Send + Sync + 'static>(
637 message: impl Into<String>,
638 source: E,
639 ) -> Self {
640 Self::Internal {
641 message: message.into(),
642 source: Some(Box::new(source)),
643 }
644 }
645
646 pub fn transport(kind: TransportErrorKind, message: impl Into<String>) -> Self {
648 Self::Transport(Box::new(TransportDetails {
649 kind,
650 message: message.into(),
651 context: TransportContext::default(),
652 source: None,
653 }))
654 }
655
656 pub fn transport_with_context(
658 kind: TransportErrorKind,
659 message: impl Into<String>,
660 context: TransportContext,
661 ) -> Self {
662 Self::Transport(Box::new(TransportDetails {
663 kind,
664 message: message.into(),
665 context,
666 source: None,
667 }))
668 }
669
670 pub fn tool_error(tool: impl Into<String>, message: impl Into<String>) -> Self {
672 Self::ToolExecution(Box::new(ToolExecutionDetails {
673 tool: tool.into(),
674 message: message.into(),
675 is_recoverable: true,
676 data: None,
677 source: None,
678 }))
679 }
680
681 pub fn tool_error_detailed(
683 tool: impl Into<String>,
684 message: impl Into<String>,
685 is_recoverable: bool,
686 data: Option<serde_json::Value>,
687 ) -> Self {
688 Self::ToolExecution(Box::new(ToolExecutionDetails {
689 tool: tool.into(),
690 message: message.into(),
691 is_recoverable,
692 data,
693 source: None,
694 }))
695 }
696
697 pub fn resource_not_found(uri: impl Into<String>) -> Self {
699 Self::ResourceNotFound { uri: uri.into() }
700 }
701
702 pub fn handshake_failed(message: impl Into<String>) -> Self {
704 Self::HandshakeFailed(Box::new(HandshakeDetails {
705 message: message.into(),
706 client_version: None,
707 server_version: None,
708 source: None,
709 }))
710 }
711
712 pub fn handshake_failed_with_versions(
714 message: impl Into<String>,
715 client_version: Option<String>,
716 server_version: Option<String>,
717 ) -> Self {
718 Self::HandshakeFailed(Box::new(HandshakeDetails {
719 message: message.into(),
720 client_version,
721 server_version,
722 source: None,
723 }))
724 }
725
726 pub fn capability_not_supported(capability: impl Into<String>) -> Self {
728 Self::CapabilityNotSupported {
729 capability: capability.into(),
730 available: Box::new([]),
731 }
732 }
733
734 pub fn capability_not_supported_with_available(
736 capability: impl Into<String>,
737 available: Vec<String>,
738 ) -> Self {
739 Self::CapabilityNotSupported {
740 capability: capability.into(),
741 available: available.into_boxed_slice(),
742 }
743 }
744
745 pub fn timeout(operation: impl Into<String>, duration: std::time::Duration) -> Self {
747 Self::Timeout {
748 operation: operation.into(),
749 duration,
750 }
751 }
752
753 pub fn cancelled(operation: impl Into<String>) -> Self {
755 Self::Cancelled {
756 operation: operation.into(),
757 reason: None,
758 }
759 }
760
761 pub fn cancelled_with_reason(operation: impl Into<String>, reason: impl Into<String>) -> Self {
763 Self::Cancelled {
764 operation: operation.into(),
765 reason: Some(reason.into()),
766 }
767 }
768}
769
770pub mod codes {
776 pub const PARSE_ERROR: i32 = -32700;
778 pub const INVALID_REQUEST: i32 = -32600;
780 pub const METHOD_NOT_FOUND: i32 = -32601;
782 pub const INVALID_PARAMS: i32 = -32602;
784 pub const INTERNAL_ERROR: i32 = -32603;
786
787 pub const SERVER_ERROR_START: i32 = -32000;
789 pub const SERVER_ERROR_END: i32 = -32099;
791
792 pub const USER_REJECTED: i32 = -1;
795 pub const RESOURCE_NOT_FOUND: i32 = -32002;
797}
798
799impl McpError {
800 #[must_use]
802 pub fn code(&self) -> i32 {
803 match self {
804 Self::Parse { .. } => codes::PARSE_ERROR,
805 Self::InvalidRequest { .. } => codes::INVALID_REQUEST,
806 Self::MethodNotFound { .. } => codes::METHOD_NOT_FOUND,
807 Self::InvalidParams(_) => codes::INVALID_PARAMS,
808 Self::Internal { .. } => codes::INTERNAL_ERROR,
809 Self::Transport(_) => codes::SERVER_ERROR_START,
810 Self::ToolExecution(_) => codes::SERVER_ERROR_START - 1,
811 Self::ResourceNotFound { .. } => codes::RESOURCE_NOT_FOUND,
812 Self::ResourceAccessDenied { .. } => codes::SERVER_ERROR_START - 2,
813 Self::ConnectionFailed { .. } => codes::SERVER_ERROR_START - 3,
814 Self::SessionExpired { .. } => codes::SERVER_ERROR_START - 4,
815 Self::HandshakeFailed(_) => codes::SERVER_ERROR_START - 5,
816 Self::CapabilityNotSupported { .. } => codes::SERVER_ERROR_START - 6,
817 Self::UserRejected { .. } => codes::USER_REJECTED,
818 Self::Timeout { .. } => codes::SERVER_ERROR_START - 7,
819 Self::Cancelled { .. } => codes::SERVER_ERROR_START - 8,
820 Self::WithContext { source, .. } => source.code(),
821 Self::InternalMessage { .. } => codes::INTERNAL_ERROR,
822 }
823 }
824
825 #[must_use]
827 pub fn is_recoverable(&self) -> bool {
828 match self {
829 Self::ToolExecution(details) => details.is_recoverable,
830 Self::InvalidParams(_) => true,
831 Self::ResourceNotFound { .. } => true,
832 Self::Timeout { .. } => true,
833 Self::WithContext { source, .. } => source.is_recoverable(),
834 Self::InternalMessage { .. } => false,
835 _ => false,
836 }
837 }
838}
839
840#[derive(Debug, Clone, Serialize, Deserialize)]
846pub struct JsonRpcError {
847 pub code: i32,
849 pub message: String,
851 #[serde(skip_serializing_if = "Option::is_none")]
853 pub data: Option<serde_json::Value>,
854}
855
856impl JsonRpcError {
857 pub fn invalid_params(message: impl Into<String>) -> Self {
859 Self {
860 code: -32602,
861 message: message.into(),
862 data: None,
863 }
864 }
865
866 pub fn internal_error(message: impl Into<String>) -> Self {
868 Self {
869 code: -32603,
870 message: message.into(),
871 data: None,
872 }
873 }
874
875 pub fn method_not_found(message: impl Into<String>) -> Self {
877 Self {
878 code: -32601,
879 message: message.into(),
880 data: None,
881 }
882 }
883
884 pub fn parse_error(message: impl Into<String>) -> Self {
886 Self {
887 code: -32700,
888 message: message.into(),
889 data: None,
890 }
891 }
892
893 pub fn invalid_request(message: impl Into<String>) -> Self {
895 Self {
896 code: -32600,
897 message: message.into(),
898 data: None,
899 }
900 }
901}
902
903impl From<&McpError> for JsonRpcError {
904 fn from(err: &McpError) -> Self {
905 let code = err.code();
906 let message = err.to_string();
907 let data = match err {
908 McpError::MethodNotFound {
909 method, available, ..
910 } => Some(serde_json::json!({
911 "method": method,
912 "available": available,
913 })),
914 McpError::InvalidParams(details) => Some(serde_json::json!({
915 "method": details.method,
916 "param_path": details.param_path,
917 "expected": details.expected,
918 "actual": details.actual,
919 })),
920 McpError::Transport(details) => Some(serde_json::json!({
921 "kind": format!("{:?}", details.kind),
922 "context": details.context,
923 })),
924 McpError::ToolExecution(details) => details
925 .data
926 .clone()
927 .or_else(|| Some(serde_json::json!({ "tool": details.tool }))),
928 McpError::HandshakeFailed(details) => Some(serde_json::json!({
929 "client_version": details.client_version,
930 "server_version": details.server_version,
931 })),
932 McpError::WithContext { source, .. } => {
933 let inner: Self = source.as_ref().into();
934 inner.data
935 }
936 _ => None,
937 };
938
939 Self {
940 code,
941 message,
942 data,
943 }
944 }
945}
946
947impl From<McpError> for JsonRpcError {
948 fn from(err: McpError) -> Self {
949 Self::from(&err)
950 }
951}
952
953pub trait McpResultExt<T> {
974 fn context<C: Into<String>>(self, context: C) -> Result<T, McpError>;
976
977 fn with_context<C, F>(self, f: F) -> Result<T, McpError>
979 where
980 C: Into<String>,
981 F: FnOnce() -> C;
982}
983
984impl<T> McpResultExt<T> for Result<T, McpError> {
985 fn context<C: Into<String>>(self, context: C) -> Self {
986 self.map_err(|e| McpError::WithContext {
987 context: context.into(),
988 source: Box::new(e),
989 })
990 }
991
992 fn with_context<C, F>(self, f: F) -> Self
993 where
994 C: Into<String>,
995 F: FnOnce() -> C,
996 {
997 self.map_err(|e| McpError::WithContext {
998 context: f().into(),
999 source: Box::new(e),
1000 })
1001 }
1002}
1003
1004impl From<serde_json::Error> for McpError {
1009 fn from(err: serde_json::Error) -> Self {
1010 Self::parse_with_source("JSON serialization/deserialization error", err)
1011 }
1012}
1013
1014impl From<std::io::Error> for McpError {
1015 fn from(err: std::io::Error) -> Self {
1016 let kind = match err.kind() {
1017 std::io::ErrorKind::NotFound => TransportErrorKind::ConnectionFailed,
1018 std::io::ErrorKind::ConnectionRefused => TransportErrorKind::ConnectionFailed,
1019 std::io::ErrorKind::ConnectionReset => TransportErrorKind::ConnectionClosed,
1020 std::io::ErrorKind::ConnectionAborted => TransportErrorKind::ConnectionClosed,
1021 std::io::ErrorKind::TimedOut => TransportErrorKind::Timeout,
1022 std::io::ErrorKind::WriteZero => TransportErrorKind::WriteFailed,
1023 std::io::ErrorKind::UnexpectedEof => TransportErrorKind::ReadFailed,
1024 _ => TransportErrorKind::ReadFailed,
1025 };
1026 let message = err.to_string();
1027 Self::Transport(Box::new(TransportDetails {
1028 kind,
1029 message,
1030 context: TransportContext::default(),
1031 source: Some(Box::new(err)),
1032 }))
1033 }
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038 use super::*;
1039
1040 #[test]
1041 fn test_error_size_is_small() {
1042 let size = std::mem::size_of::<McpError>();
1045 assert!(
1046 size <= 64,
1047 "McpError is {size} bytes, should be <= 64 bytes. Consider boxing more variants."
1048 );
1049
1050 let result_size = std::mem::size_of::<Result<(), McpError>>();
1052 assert!(
1053 result_size <= 72,
1054 "Result<(), McpError> is {result_size} bytes, should be <= 72 bytes."
1055 );
1056 }
1057
1058 #[test]
1059 fn test_error_codes() {
1060 assert_eq!(McpError::parse("test").code(), codes::PARSE_ERROR);
1061 assert_eq!(
1062 McpError::invalid_request("test").code(),
1063 codes::INVALID_REQUEST
1064 );
1065 assert_eq!(
1066 McpError::method_not_found("test").code(),
1067 codes::METHOD_NOT_FOUND
1068 );
1069 assert_eq!(
1070 McpError::invalid_params("m", "test").code(),
1071 codes::INVALID_PARAMS
1072 );
1073 assert_eq!(McpError::internal("test").code(), codes::INTERNAL_ERROR);
1074 assert_eq!(
1075 McpError::transport(TransportErrorKind::ConnectionFailed, "test").code(),
1076 codes::SERVER_ERROR_START
1077 );
1078 assert_eq!(
1079 McpError::tool_error("tool", "test").code(),
1080 codes::SERVER_ERROR_START - 1
1081 );
1082 assert_eq!(
1083 McpError::handshake_failed("test").code(),
1084 codes::SERVER_ERROR_START - 5
1085 );
1086 }
1087
1088 #[test]
1089 fn test_context_chaining() {
1090 fn inner() -> Result<(), McpError> {
1091 Err(McpError::resource_not_found("test://resource"))
1092 }
1093
1094 fn outer() -> Result<(), McpError> {
1095 inner().context("Failed in outer")?;
1096 Ok(())
1097 }
1098
1099 let err = outer().unwrap_err();
1100 assert!(err.to_string().contains("Failed in outer"));
1101
1102 assert_eq!(err.code(), codes::RESOURCE_NOT_FOUND);
1104 }
1105
1106 #[test]
1107 fn test_json_rpc_error_conversion() {
1108 let err = McpError::method_not_found_with_suggestions(
1109 "unknown_method",
1110 vec!["tools/list".to_string(), "resources/list".to_string()],
1111 );
1112
1113 let json_err: JsonRpcError = (&err).into();
1114 assert_eq!(json_err.code, codes::METHOD_NOT_FOUND);
1115 assert!(json_err.message.contains("unknown_method"));
1116 assert!(json_err.data.is_some());
1117 }
1118
1119 #[test]
1120 fn test_json_rpc_error_conversion_boxed_variants() {
1121 let err = McpError::invalid_params_detailed(
1123 "test_method",
1124 "invalid value",
1125 Some("args.count".to_string()),
1126 Some("number".to_string()),
1127 Some("string".to_string()),
1128 );
1129 let json_err: JsonRpcError = (&err).into();
1130 assert_eq!(json_err.code, codes::INVALID_PARAMS);
1131 let data = json_err.data.unwrap();
1132 assert_eq!(data["method"], "test_method");
1133 assert_eq!(data["param_path"], "args.count");
1134
1135 let err = McpError::transport_with_context(
1137 TransportErrorKind::ConnectionFailed,
1138 "connection refused",
1139 TransportContext::new("websocket").with_remote_addr("ws://localhost:8080"),
1140 );
1141 let json_err: JsonRpcError = (&err).into();
1142 assert_eq!(json_err.code, codes::SERVER_ERROR_START);
1143 assert!(json_err.data.is_some());
1144
1145 let err = McpError::tool_error_detailed(
1147 "calculator",
1148 "division by zero",
1149 true,
1150 Some(serde_json::json!({"operation": "divide"})),
1151 );
1152 let json_err: JsonRpcError = (&err).into();
1153 assert!(json_err.data.is_some());
1154 let data = json_err.data.unwrap();
1155 assert_eq!(data["operation"], "divide");
1156
1157 let err = McpError::handshake_failed_with_versions(
1159 "version mismatch",
1160 Some("2024-11-05".to_string()),
1161 Some("2025-11-25".to_string()),
1162 );
1163 let json_err: JsonRpcError = (&err).into();
1164 assert!(json_err.data.is_some());
1165 let data = json_err.data.unwrap();
1166 assert_eq!(data["client_version"], "2024-11-05");
1167 assert_eq!(data["server_version"], "2025-11-25");
1168 }
1169
1170 #[test]
1171 fn test_recoverable_errors() {
1172 assert!(McpError::invalid_params("m", "test").is_recoverable());
1173 assert!(McpError::resource_not_found("uri").is_recoverable());
1174 assert!(!McpError::internal("test").is_recoverable());
1175
1176 let recoverable_tool = McpError::tool_error_detailed("tool", "error", true, None);
1178 assert!(recoverable_tool.is_recoverable());
1179
1180 let non_recoverable_tool = McpError::tool_error_detailed("tool", "error", false, None);
1181 assert!(!non_recoverable_tool.is_recoverable());
1182 }
1183
1184 #[test]
1185 fn test_boxed_error_display() {
1186 let err = McpError::invalid_params("method", "bad params");
1188 assert!(err.to_string().contains("method"));
1189 assert!(err.to_string().contains("bad params"));
1190
1191 let err = McpError::transport(TransportErrorKind::Timeout, "connection timed out");
1192 assert!(err.to_string().contains("timeout"));
1193 assert!(err.to_string().contains("connection timed out"));
1194
1195 let err = McpError::tool_error("my_tool", "tool failed");
1196 assert!(err.to_string().contains("my_tool"));
1197 assert!(err.to_string().contains("tool failed"));
1198
1199 let err = McpError::handshake_failed("protocol mismatch");
1200 assert!(err.to_string().contains("protocol mismatch"));
1201 }
1202
1203 #[test]
1204 fn test_io_error_conversion() {
1205 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
1206 let mcp_err: McpError = io_err.into();
1207
1208 if let McpError::Transport(details) = mcp_err {
1210 assert_eq!(details.kind, TransportErrorKind::ConnectionFailed);
1211 assert!(details.message.contains("refused"));
1212 } else {
1213 panic!("Expected Transport error variant");
1214 }
1215 }
1216}