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.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
148 }
149}
150
151#[derive(Debug)]
153pub struct TransportDetails {
154 pub kind: TransportErrorKind,
156 pub message: String,
158 pub context: TransportContext,
160 pub source: Option<BoxError>,
162}
163
164impl fmt::Display for TransportDetails {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 write!(f, "Transport error ({}): {}", self.kind, self.message)
167 }
168}
169
170impl std::error::Error for TransportDetails {
171 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
172 self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
173 }
174}
175
176#[derive(Debug)]
178pub struct ToolExecutionDetails {
179 pub tool: String,
181 pub message: String,
183 pub is_recoverable: bool,
185 pub data: Option<serde_json::Value>,
187 pub source: Option<BoxError>,
189}
190
191impl fmt::Display for ToolExecutionDetails {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 write!(f, "Tool '{}' failed: {}", self.tool, self.message)
194 }
195}
196
197impl std::error::Error for ToolExecutionDetails {
198 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
199 self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
200 }
201}
202
203#[derive(Debug)]
205pub struct HandshakeDetails {
206 pub message: String,
208 pub client_version: Option<String>,
210 pub server_version: Option<String>,
212 pub source: Option<BoxError>,
214}
215
216impl fmt::Display for HandshakeDetails {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 write!(f, "Handshake failed: {}", self.message)
219 }
220}
221
222impl std::error::Error for HandshakeDetails {
223 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
224 self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
225 }
226}
227
228#[derive(Error, Diagnostic, Debug)]
237#[allow(clippy::large_enum_variant)] pub enum McpError {
239 #[error("Parse error: {message}")]
244 #[diagnostic(
245 code(mcp::protocol::parse_error),
246 help("Ensure the message is valid JSON-RPC 2.0 format")
247 )]
248 Parse {
249 message: String,
251 #[source]
253 source: Option<BoxError>,
254 },
255
256 #[error("Invalid request: {message}")]
258 #[diagnostic(code(mcp::protocol::invalid_request))]
259 InvalidRequest {
260 message: String,
262 #[source]
264 source: Option<BoxError>,
265 },
266
267 #[error("Method not found: {method}")]
269 #[diagnostic(code(mcp::protocol::method_not_found))]
270 MethodNotFound {
271 method: String,
273 available: Box<[String]>,
275 },
276
277 #[error("Invalid params for '{}': {}", .0.method, .0.message)]
279 #[diagnostic(code(mcp::protocol::invalid_params))]
280 InvalidParams(#[source] Box<InvalidParamsDetails>),
281
282 #[error("Internal error: {message}")]
284 #[diagnostic(code(mcp::protocol::internal_error), severity(error))]
285 Internal {
286 message: String,
288 #[source]
290 source: Option<BoxError>,
291 },
292
293 #[error("Transport error ({}): {}", .0.kind, .0.message)]
298 #[diagnostic(code(mcp::transport::error))]
299 Transport(#[source] Box<TransportDetails>),
300
301 #[error("Tool '{}' failed: {}", .0.tool, .0.message)]
306 #[diagnostic(code(mcp::tool::execution_error))]
307 ToolExecution(#[source] Box<ToolExecutionDetails>),
308
309 #[error("Resource not found: {uri}")]
314 #[diagnostic(
315 code(mcp::resource::not_found),
316 help("Verify the URI is correct and the resource exists")
317 )]
318 ResourceNotFound {
319 uri: String,
321 },
322
323 #[error("Resource access denied: {uri}")]
325 #[diagnostic(code(mcp::resource::access_denied))]
326 ResourceAccessDenied {
327 uri: String,
329 reason: Option<String>,
331 },
332
333 #[error("Connection failed: {message}")]
338 #[diagnostic(code(mcp::connection::failed))]
339 ConnectionFailed {
340 message: String,
342 #[source]
344 source: Option<BoxError>,
345 },
346
347 #[error("Session expired: {session_id}")]
349 #[diagnostic(
350 code(mcp::session::expired),
351 help("Re-initialize the connection to continue")
352 )]
353 SessionExpired {
354 session_id: String,
356 },
357
358 #[error("Handshake failed: {}", .0.message)]
360 #[diagnostic(code(mcp::handshake::failed))]
361 HandshakeFailed(#[source] Box<HandshakeDetails>),
362
363 #[error("Capability not supported: {capability}")]
368 #[diagnostic(code(mcp::capability::not_supported))]
369 CapabilityNotSupported {
370 capability: String,
372 available: Box<[String]>,
374 },
375
376 #[error("User rejected: {message}")]
381 #[diagnostic(code(mcp::user::rejected))]
382 UserRejected {
383 message: String,
385 operation: String,
387 },
388
389 #[error("Timeout after {duration:?}: {operation}")]
394 #[diagnostic(
395 code(mcp::timeout),
396 help("Consider increasing the timeout or checking connectivity")
397 )]
398 Timeout {
399 operation: String,
401 duration: std::time::Duration,
403 },
404
405 #[error("Operation cancelled: {operation}")]
410 #[diagnostic(code(mcp::cancelled))]
411 Cancelled {
412 operation: String,
414 reason: Option<String>,
416 },
417
418 #[error("{context}: {source}")]
423 #[diagnostic(code(mcp::context))]
424 WithContext {
425 context: String,
427 #[source]
429 source: Box<McpError>,
430 },
431
432 #[error("Internal error: {message}")]
437 #[diagnostic(code(mcp::internal))]
438 InternalMessage {
439 message: String,
441 },
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
446#[serde(rename_all = "snake_case")]
447pub enum TransportErrorKind {
448 ConnectionFailed,
450 ConnectionClosed,
452 ReadFailed,
454 WriteFailed,
456 TlsError,
458 DnsResolutionFailed,
460 Timeout,
462 InvalidMessage,
464 ProtocolViolation,
466 ResourceExhausted,
468 RateLimited,
470}
471
472impl fmt::Display for TransportErrorKind {
473 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474 match self {
475 Self::ConnectionFailed => write!(f, "connection failed"),
476 Self::ConnectionClosed => write!(f, "connection closed"),
477 Self::ReadFailed => write!(f, "read failed"),
478 Self::WriteFailed => write!(f, "write failed"),
479 Self::TlsError => write!(f, "TLS error"),
480 Self::DnsResolutionFailed => write!(f, "DNS resolution failed"),
481 Self::Timeout => write!(f, "timeout"),
482 Self::InvalidMessage => write!(f, "invalid message"),
483 Self::ProtocolViolation => write!(f, "protocol violation"),
484 Self::ResourceExhausted => write!(f, "resource exhausted"),
485 Self::RateLimited => write!(f, "rate limited"),
486 }
487 }
488}
489
490#[derive(Debug, Clone, Default, Serialize, Deserialize)]
492pub struct TransportContext {
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub transport_type: Option<String>,
496 #[serde(skip_serializing_if = "Option::is_none")]
498 pub remote_addr: Option<String>,
499 #[serde(skip_serializing_if = "Option::is_none")]
501 pub local_addr: Option<String>,
502 #[serde(skip_serializing_if = "Option::is_none")]
504 pub bytes_sent: Option<u64>,
505 #[serde(skip_serializing_if = "Option::is_none")]
507 pub bytes_received: Option<u64>,
508 #[serde(skip_serializing_if = "Option::is_none")]
510 pub connection_duration_ms: Option<u64>,
511}
512
513impl TransportContext {
514 #[must_use]
516 pub fn new(transport_type: impl Into<String>) -> Self {
517 Self {
518 transport_type: Some(transport_type.into()),
519 ..Default::default()
520 }
521 }
522
523 #[must_use]
525 pub fn with_remote_addr(mut self, addr: impl Into<String>) -> Self {
526 self.remote_addr = Some(addr.into());
527 self
528 }
529
530 #[must_use]
532 pub fn with_local_addr(mut self, addr: impl Into<String>) -> Self {
533 self.local_addr = Some(addr.into());
534 self
535 }
536}
537
538impl McpError {
543 pub fn parse(message: impl Into<String>) -> Self {
545 Self::Parse {
546 message: message.into(),
547 source: None,
548 }
549 }
550
551 pub fn parse_with_source<E: std::error::Error + Send + Sync + 'static>(
553 message: impl Into<String>,
554 source: E,
555 ) -> Self {
556 Self::Parse {
557 message: message.into(),
558 source: Some(Box::new(source)),
559 }
560 }
561
562 pub fn invalid_request(message: impl Into<String>) -> Self {
564 Self::InvalidRequest {
565 message: message.into(),
566 source: None,
567 }
568 }
569
570 pub fn method_not_found(method: impl Into<String>) -> Self {
572 Self::MethodNotFound {
573 method: method.into(),
574 available: Box::new([]),
575 }
576 }
577
578 pub fn method_not_found_with_suggestions(
580 method: impl Into<String>,
581 available: Vec<String>,
582 ) -> Self {
583 Self::MethodNotFound {
584 method: method.into(),
585 available: available.into_boxed_slice(),
586 }
587 }
588
589 pub fn invalid_params(method: impl Into<String>, message: impl Into<String>) -> Self {
591 Self::InvalidParams(Box::new(InvalidParamsDetails {
592 method: method.into(),
593 message: message.into(),
594 param_path: None,
595 expected: None,
596 actual: None,
597 source: None,
598 }))
599 }
600
601 pub fn invalid_params_detailed(
603 method: impl Into<String>,
604 message: impl Into<String>,
605 param_path: Option<String>,
606 expected: Option<String>,
607 actual: Option<String>,
608 ) -> Self {
609 Self::InvalidParams(Box::new(InvalidParamsDetails {
610 method: method.into(),
611 message: message.into(),
612 param_path,
613 expected,
614 actual,
615 source: None,
616 }))
617 }
618
619 pub fn internal(message: impl Into<String>) -> Self {
621 Self::Internal {
622 message: message.into(),
623 source: None,
624 }
625 }
626
627 pub fn internal_with_source<E: std::error::Error + Send + Sync + 'static>(
629 message: impl Into<String>,
630 source: E,
631 ) -> Self {
632 Self::Internal {
633 message: message.into(),
634 source: Some(Box::new(source)),
635 }
636 }
637
638 pub fn transport(kind: TransportErrorKind, message: impl Into<String>) -> Self {
640 Self::Transport(Box::new(TransportDetails {
641 kind,
642 message: message.into(),
643 context: TransportContext::default(),
644 source: None,
645 }))
646 }
647
648 pub fn transport_with_context(
650 kind: TransportErrorKind,
651 message: impl Into<String>,
652 context: TransportContext,
653 ) -> Self {
654 Self::Transport(Box::new(TransportDetails {
655 kind,
656 message: message.into(),
657 context,
658 source: None,
659 }))
660 }
661
662 pub fn tool_error(tool: impl Into<String>, message: impl Into<String>) -> Self {
664 Self::ToolExecution(Box::new(ToolExecutionDetails {
665 tool: tool.into(),
666 message: message.into(),
667 is_recoverable: true,
668 data: None,
669 source: None,
670 }))
671 }
672
673 pub fn tool_error_detailed(
675 tool: impl Into<String>,
676 message: impl Into<String>,
677 is_recoverable: bool,
678 data: Option<serde_json::Value>,
679 ) -> Self {
680 Self::ToolExecution(Box::new(ToolExecutionDetails {
681 tool: tool.into(),
682 message: message.into(),
683 is_recoverable,
684 data,
685 source: None,
686 }))
687 }
688
689 pub fn resource_not_found(uri: impl Into<String>) -> Self {
691 Self::ResourceNotFound { uri: uri.into() }
692 }
693
694 pub fn handshake_failed(message: impl Into<String>) -> Self {
696 Self::HandshakeFailed(Box::new(HandshakeDetails {
697 message: message.into(),
698 client_version: None,
699 server_version: None,
700 source: None,
701 }))
702 }
703
704 pub fn handshake_failed_with_versions(
706 message: impl Into<String>,
707 client_version: Option<String>,
708 server_version: Option<String>,
709 ) -> Self {
710 Self::HandshakeFailed(Box::new(HandshakeDetails {
711 message: message.into(),
712 client_version,
713 server_version,
714 source: None,
715 }))
716 }
717
718 pub fn capability_not_supported(capability: impl Into<String>) -> Self {
720 Self::CapabilityNotSupported {
721 capability: capability.into(),
722 available: Box::new([]),
723 }
724 }
725
726 pub fn capability_not_supported_with_available(
728 capability: impl Into<String>,
729 available: Vec<String>,
730 ) -> Self {
731 Self::CapabilityNotSupported {
732 capability: capability.into(),
733 available: available.into_boxed_slice(),
734 }
735 }
736
737 pub fn timeout(operation: impl Into<String>, duration: std::time::Duration) -> Self {
739 Self::Timeout {
740 operation: operation.into(),
741 duration,
742 }
743 }
744
745 pub fn cancelled(operation: impl Into<String>) -> Self {
747 Self::Cancelled {
748 operation: operation.into(),
749 reason: None,
750 }
751 }
752
753 pub fn cancelled_with_reason(operation: impl Into<String>, reason: impl Into<String>) -> Self {
755 Self::Cancelled {
756 operation: operation.into(),
757 reason: Some(reason.into()),
758 }
759 }
760}
761
762pub mod codes {
768 pub const PARSE_ERROR: i32 = -32700;
770 pub const INVALID_REQUEST: i32 = -32600;
772 pub const METHOD_NOT_FOUND: i32 = -32601;
774 pub const INVALID_PARAMS: i32 = -32602;
776 pub const INTERNAL_ERROR: i32 = -32603;
778
779 pub const SERVER_ERROR_START: i32 = -32000;
781 pub const SERVER_ERROR_END: i32 = -32099;
783
784 pub const USER_REJECTED: i32 = -1;
787 pub const RESOURCE_NOT_FOUND: i32 = -32002;
789}
790
791impl McpError {
792 #[must_use]
794 pub fn code(&self) -> i32 {
795 match self {
796 Self::Parse { .. } => codes::PARSE_ERROR,
797 Self::InvalidRequest { .. } => codes::INVALID_REQUEST,
798 Self::MethodNotFound { .. } => codes::METHOD_NOT_FOUND,
799 Self::InvalidParams(_) => codes::INVALID_PARAMS,
800 Self::Internal { .. } => codes::INTERNAL_ERROR,
801 Self::Transport(_) => codes::SERVER_ERROR_START,
802 Self::ToolExecution(_) => codes::SERVER_ERROR_START - 1,
803 Self::ResourceNotFound { .. } => codes::RESOURCE_NOT_FOUND,
804 Self::ResourceAccessDenied { .. } => codes::SERVER_ERROR_START - 2,
805 Self::ConnectionFailed { .. } => codes::SERVER_ERROR_START - 3,
806 Self::SessionExpired { .. } => codes::SERVER_ERROR_START - 4,
807 Self::HandshakeFailed(_) => codes::SERVER_ERROR_START - 5,
808 Self::CapabilityNotSupported { .. } => codes::SERVER_ERROR_START - 6,
809 Self::UserRejected { .. } => codes::USER_REJECTED,
810 Self::Timeout { .. } => codes::SERVER_ERROR_START - 7,
811 Self::Cancelled { .. } => codes::SERVER_ERROR_START - 8,
812 Self::WithContext { source, .. } => source.code(),
813 Self::InternalMessage { .. } => codes::INTERNAL_ERROR,
814 }
815 }
816
817 #[must_use]
819 pub fn is_recoverable(&self) -> bool {
820 match self {
821 Self::ToolExecution(details) => details.is_recoverable,
822 Self::InvalidParams(_) => true,
823 Self::ResourceNotFound { .. } => true,
824 Self::Timeout { .. } => true,
825 Self::WithContext { source, .. } => source.is_recoverable(),
826 Self::InternalMessage { .. } => false,
827 _ => false,
828 }
829 }
830}
831
832#[derive(Debug, Clone, Serialize, Deserialize)]
838pub struct JsonRpcError {
839 pub code: i32,
841 pub message: String,
843 #[serde(skip_serializing_if = "Option::is_none")]
845 pub data: Option<serde_json::Value>,
846}
847
848impl JsonRpcError {
849 pub fn invalid_params(message: impl Into<String>) -> Self {
851 Self {
852 code: -32602,
853 message: message.into(),
854 data: None,
855 }
856 }
857
858 pub fn internal_error(message: impl Into<String>) -> Self {
860 Self {
861 code: -32603,
862 message: message.into(),
863 data: None,
864 }
865 }
866
867 pub fn method_not_found(message: impl Into<String>) -> Self {
869 Self {
870 code: -32601,
871 message: message.into(),
872 data: None,
873 }
874 }
875
876 pub fn parse_error(message: impl Into<String>) -> Self {
878 Self {
879 code: -32700,
880 message: message.into(),
881 data: None,
882 }
883 }
884
885 pub fn invalid_request(message: impl Into<String>) -> Self {
887 Self {
888 code: -32600,
889 message: message.into(),
890 data: None,
891 }
892 }
893}
894
895impl From<&McpError> for JsonRpcError {
896 fn from(err: &McpError) -> Self {
897 let code = err.code();
898 let message = err.to_string();
899 let data = match err {
900 McpError::MethodNotFound { method, available, .. } => Some(serde_json::json!({
901 "method": method,
902 "available": available,
903 })),
904 McpError::InvalidParams(details) => Some(serde_json::json!({
905 "method": details.method,
906 "param_path": details.param_path,
907 "expected": details.expected,
908 "actual": details.actual,
909 })),
910 McpError::Transport(details) => Some(serde_json::json!({
911 "kind": format!("{:?}", details.kind),
912 "context": details.context,
913 })),
914 McpError::ToolExecution(details) => {
915 details.data.clone().or_else(|| Some(serde_json::json!({ "tool": details.tool })))
916 }
917 McpError::HandshakeFailed(details) => Some(serde_json::json!({
918 "client_version": details.client_version,
919 "server_version": details.server_version,
920 })),
921 McpError::WithContext { source, .. } => {
922 let inner: JsonRpcError = source.as_ref().into();
923 inner.data
924 }
925 _ => None,
926 };
927
928 Self {
929 code,
930 message,
931 data,
932 }
933 }
934}
935
936impl From<McpError> for JsonRpcError {
937 fn from(err: McpError) -> Self {
938 Self::from(&err)
939 }
940}
941
942pub trait McpResultExt<T> {
963 fn context<C: Into<String>>(self, context: C) -> Result<T, McpError>;
965
966 fn with_context<C, F>(self, f: F) -> Result<T, McpError>
968 where
969 C: Into<String>,
970 F: FnOnce() -> C;
971}
972
973impl<T> McpResultExt<T> for Result<T, McpError> {
974 fn context<C: Into<String>>(self, context: C) -> Result<T, McpError> {
975 self.map_err(|e| McpError::WithContext {
976 context: context.into(),
977 source: Box::new(e),
978 })
979 }
980
981 fn with_context<C, F>(self, f: F) -> Result<T, McpError>
982 where
983 C: Into<String>,
984 F: FnOnce() -> C,
985 {
986 self.map_err(|e| McpError::WithContext {
987 context: f().into(),
988 source: Box::new(e),
989 })
990 }
991}
992
993impl From<serde_json::Error> for McpError {
998 fn from(err: serde_json::Error) -> Self {
999 Self::parse_with_source("JSON serialization/deserialization error", err)
1000 }
1001}
1002
1003impl From<std::io::Error> for McpError {
1004 fn from(err: std::io::Error) -> Self {
1005 let kind = match err.kind() {
1006 std::io::ErrorKind::NotFound => TransportErrorKind::ConnectionFailed,
1007 std::io::ErrorKind::ConnectionRefused => TransportErrorKind::ConnectionFailed,
1008 std::io::ErrorKind::ConnectionReset => TransportErrorKind::ConnectionClosed,
1009 std::io::ErrorKind::ConnectionAborted => TransportErrorKind::ConnectionClosed,
1010 std::io::ErrorKind::TimedOut => TransportErrorKind::Timeout,
1011 std::io::ErrorKind::WriteZero => TransportErrorKind::WriteFailed,
1012 std::io::ErrorKind::UnexpectedEof => TransportErrorKind::ReadFailed,
1013 _ => TransportErrorKind::ReadFailed,
1014 };
1015 let message = err.to_string();
1016 Self::Transport(Box::new(TransportDetails {
1017 kind,
1018 message,
1019 context: TransportContext::default(),
1020 source: Some(Box::new(err)),
1021 }))
1022 }
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027 use super::*;
1028
1029 #[test]
1030 fn test_error_size_is_small() {
1031 let size = std::mem::size_of::<McpError>();
1034 assert!(
1035 size <= 64,
1036 "McpError is {} bytes, should be <= 64 bytes. Consider boxing more variants.",
1037 size
1038 );
1039
1040 let result_size = std::mem::size_of::<Result<(), McpError>>();
1042 assert!(
1043 result_size <= 72,
1044 "Result<(), McpError> is {} bytes, should be <= 72 bytes.",
1045 result_size
1046 );
1047 }
1048
1049 #[test]
1050 fn test_error_codes() {
1051 assert_eq!(McpError::parse("test").code(), codes::PARSE_ERROR);
1052 assert_eq!(
1053 McpError::invalid_request("test").code(),
1054 codes::INVALID_REQUEST
1055 );
1056 assert_eq!(
1057 McpError::method_not_found("test").code(),
1058 codes::METHOD_NOT_FOUND
1059 );
1060 assert_eq!(
1061 McpError::invalid_params("m", "test").code(),
1062 codes::INVALID_PARAMS
1063 );
1064 assert_eq!(McpError::internal("test").code(), codes::INTERNAL_ERROR);
1065 assert_eq!(
1066 McpError::transport(TransportErrorKind::ConnectionFailed, "test").code(),
1067 codes::SERVER_ERROR_START
1068 );
1069 assert_eq!(
1070 McpError::tool_error("tool", "test").code(),
1071 codes::SERVER_ERROR_START - 1
1072 );
1073 assert_eq!(
1074 McpError::handshake_failed("test").code(),
1075 codes::SERVER_ERROR_START - 5
1076 );
1077 }
1078
1079 #[test]
1080 fn test_context_chaining() {
1081 fn inner() -> Result<(), McpError> {
1082 Err(McpError::resource_not_found("test://resource"))
1083 }
1084
1085 fn outer() -> Result<(), McpError> {
1086 inner().context("Failed in outer")?;
1087 Ok(())
1088 }
1089
1090 let err = outer().unwrap_err();
1091 assert!(err.to_string().contains("Failed in outer"));
1092
1093 assert_eq!(err.code(), codes::RESOURCE_NOT_FOUND);
1095 }
1096
1097 #[test]
1098 fn test_json_rpc_error_conversion() {
1099 let err = McpError::method_not_found_with_suggestions(
1100 "unknown_method",
1101 vec!["tools/list".to_string(), "resources/list".to_string()],
1102 );
1103
1104 let json_err: JsonRpcError = (&err).into();
1105 assert_eq!(json_err.code, codes::METHOD_NOT_FOUND);
1106 assert!(json_err.message.contains("unknown_method"));
1107 assert!(json_err.data.is_some());
1108 }
1109
1110 #[test]
1111 fn test_json_rpc_error_conversion_boxed_variants() {
1112 let err = McpError::invalid_params_detailed(
1114 "test_method",
1115 "invalid value",
1116 Some("args.count".to_string()),
1117 Some("number".to_string()),
1118 Some("string".to_string()),
1119 );
1120 let json_err: JsonRpcError = (&err).into();
1121 assert_eq!(json_err.code, codes::INVALID_PARAMS);
1122 let data = json_err.data.unwrap();
1123 assert_eq!(data["method"], "test_method");
1124 assert_eq!(data["param_path"], "args.count");
1125
1126 let err = McpError::transport_with_context(
1128 TransportErrorKind::ConnectionFailed,
1129 "connection refused",
1130 TransportContext::new("websocket").with_remote_addr("ws://localhost:8080"),
1131 );
1132 let json_err: JsonRpcError = (&err).into();
1133 assert_eq!(json_err.code, codes::SERVER_ERROR_START);
1134 assert!(json_err.data.is_some());
1135
1136 let err = McpError::tool_error_detailed(
1138 "calculator",
1139 "division by zero",
1140 true,
1141 Some(serde_json::json!({"operation": "divide"})),
1142 );
1143 let json_err: JsonRpcError = (&err).into();
1144 assert!(json_err.data.is_some());
1145 let data = json_err.data.unwrap();
1146 assert_eq!(data["operation"], "divide");
1147
1148 let err = McpError::handshake_failed_with_versions(
1150 "version mismatch",
1151 Some("2024-11-05".to_string()),
1152 Some("2025-11-25".to_string()),
1153 );
1154 let json_err: JsonRpcError = (&err).into();
1155 assert!(json_err.data.is_some());
1156 let data = json_err.data.unwrap();
1157 assert_eq!(data["client_version"], "2024-11-05");
1158 assert_eq!(data["server_version"], "2025-11-25");
1159 }
1160
1161 #[test]
1162 fn test_recoverable_errors() {
1163 assert!(McpError::invalid_params("m", "test").is_recoverable());
1164 assert!(McpError::resource_not_found("uri").is_recoverable());
1165 assert!(!McpError::internal("test").is_recoverable());
1166
1167 let recoverable_tool = McpError::tool_error_detailed("tool", "error", true, None);
1169 assert!(recoverable_tool.is_recoverable());
1170
1171 let non_recoverable_tool = McpError::tool_error_detailed("tool", "error", false, None);
1172 assert!(!non_recoverable_tool.is_recoverable());
1173 }
1174
1175 #[test]
1176 fn test_boxed_error_display() {
1177 let err = McpError::invalid_params("method", "bad params");
1179 assert!(err.to_string().contains("method"));
1180 assert!(err.to_string().contains("bad params"));
1181
1182 let err = McpError::transport(TransportErrorKind::Timeout, "connection timed out");
1183 assert!(err.to_string().contains("timeout"));
1184 assert!(err.to_string().contains("connection timed out"));
1185
1186 let err = McpError::tool_error("my_tool", "tool failed");
1187 assert!(err.to_string().contains("my_tool"));
1188 assert!(err.to_string().contains("tool failed"));
1189
1190 let err = McpError::handshake_failed("protocol mismatch");
1191 assert!(err.to_string().contains("protocol mismatch"));
1192 }
1193
1194 #[test]
1195 fn test_io_error_conversion() {
1196 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
1197 let mcp_err: McpError = io_err.into();
1198
1199 if let McpError::Transport(details) = mcp_err {
1201 assert_eq!(details.kind, TransportErrorKind::ConnectionFailed);
1202 assert!(details.message.contains("refused"));
1203 } else {
1204 panic!("Expected Transport error variant");
1205 }
1206 }
1207}