Skip to main content

neuron_mcp/
error.rs

1//! Error conversion utilities for MCP operations.
2//!
3//! Re-exports [`McpError`] from `neuron-types` and provides conversion
4//! functions from rmcp's error types. We cannot implement `From` directly
5//! due to Rust's orphan rules (both types are foreign).
6
7pub use neuron_types::McpError;
8
9/// Convert an rmcp `ServiceError` into our `McpError`.
10pub(crate) fn from_service_error(err: rmcp::ServiceError) -> McpError {
11    McpError::Transport(err.to_string())
12}
13
14/// Convert an rmcp `ClientInitializeError` into our `McpError`.
15pub(crate) fn from_client_init_error(err: rmcp::service::ClientInitializeError) -> McpError {
16    McpError::Initialization(err.to_string())
17}
18
19#[cfg(test)]
20mod tests {
21    use super::*;
22
23    #[test]
24    fn from_service_error_transport_closed() {
25        let err = rmcp::ServiceError::TransportClosed;
26        let mcp_err = from_service_error(err);
27        match &mcp_err {
28            McpError::Transport(msg) => {
29                assert!(msg.contains("Transport closed"), "got: {msg}");
30            }
31            other => panic!("expected Transport variant, got: {other:?}"),
32        }
33    }
34
35    #[test]
36    fn from_service_error_unexpected_response() {
37        let err = rmcp::ServiceError::UnexpectedResponse;
38        let mcp_err = from_service_error(err);
39        match &mcp_err {
40            McpError::Transport(msg) => {
41                assert!(msg.contains("Unexpected response"), "got: {msg}");
42            }
43            other => panic!("expected Transport variant, got: {other:?}"),
44        }
45    }
46
47    #[test]
48    fn from_service_error_cancelled() {
49        let err = rmcp::ServiceError::Cancelled {
50            reason: Some("test reason".to_string()),
51        };
52        let mcp_err = from_service_error(err);
53        match &mcp_err {
54            McpError::Transport(msg) => {
55                assert!(msg.contains("test reason"), "got: {msg}");
56            }
57            other => panic!("expected Transport variant, got: {other:?}"),
58        }
59    }
60
61    #[test]
62    fn from_service_error_cancelled_no_reason() {
63        let err = rmcp::ServiceError::Cancelled { reason: None };
64        let mcp_err = from_service_error(err);
65        match &mcp_err {
66            McpError::Transport(msg) => {
67                assert!(msg.contains("cancelled"), "got: {msg}");
68            }
69            other => panic!("expected Transport variant, got: {other:?}"),
70        }
71    }
72
73    #[test]
74    fn from_service_error_timeout() {
75        let err = rmcp::ServiceError::Timeout {
76            timeout: std::time::Duration::from_secs(30),
77        };
78        let mcp_err = from_service_error(err);
79        match &mcp_err {
80            McpError::Transport(msg) => {
81                assert!(msg.contains("timeout"), "got: {msg}");
82            }
83            other => panic!("expected Transport variant, got: {other:?}"),
84        }
85    }
86
87    #[test]
88    fn from_client_init_error_connection_closed() {
89        let err = rmcp::service::ClientInitializeError::ConnectionClosed("peer gone".to_string());
90        let mcp_err = from_client_init_error(err);
91        match &mcp_err {
92            McpError::Initialization(msg) => {
93                assert!(msg.contains("peer gone"), "got: {msg}");
94            }
95            other => panic!("expected Initialization variant, got: {other:?}"),
96        }
97    }
98
99    #[test]
100    fn from_client_init_error_expected_init_response_none() {
101        let err = rmcp::service::ClientInitializeError::ExpectedInitResponse(None);
102        let mcp_err = from_client_init_error(err);
103        match &mcp_err {
104            McpError::Initialization(msg) => {
105                assert!(msg.contains("initialized response"), "got: {msg}");
106            }
107            other => panic!("expected Initialization variant, got: {other:?}"),
108        }
109    }
110
111    #[test]
112    fn from_client_init_error_expected_init_result_none() {
113        let err = rmcp::service::ClientInitializeError::ExpectedInitResult(None);
114        let mcp_err = from_client_init_error(err);
115        match &mcp_err {
116            McpError::Initialization(msg) => {
117                assert!(msg.contains("initialized result"), "got: {msg}");
118            }
119            other => panic!("expected Initialization variant, got: {other:?}"),
120        }
121    }
122
123    #[test]
124    fn mcp_error_display_transport() {
125        let err = McpError::Transport("socket closed".to_string());
126        assert_eq!(err.to_string(), "transport error: socket closed");
127    }
128
129    #[test]
130    fn mcp_error_display_initialization() {
131        let err = McpError::Initialization("handshake failed".to_string());
132        assert_eq!(err.to_string(), "initialization failed: handshake failed");
133    }
134
135    #[test]
136    fn mcp_error_display_connection() {
137        let err = McpError::Connection("refused".to_string());
138        assert_eq!(err.to_string(), "connection failed: refused");
139    }
140
141    #[test]
142    fn mcp_error_display_tool_call() {
143        let err = McpError::ToolCall("not found".to_string());
144        assert_eq!(err.to_string(), "tool call failed: not found");
145    }
146}