Skip to content

Commit 2e3c4ca

Browse files
authored
feat: introduce framework-agnostic McpHttpError type (#143)
1 parent 09c3bff commit 2e3c4ca

4 files changed

Lines changed: 358 additions & 2 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
.DS_store
33
settings.json
44
/.vscode
5-
5+
/docs
66

77
# test artifacts
88
/coverage
99
*.lcov
1010
# *.profraw
11-
target/**
11+
target/**

crates/rust-mcp-sdk/src/hyper_servers/error.rs

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use std::net::AddrParseError;
33
use axum::{http::StatusCode, response::IntoResponse};
44
use thiserror::Error;
55

6+
use crate::mcp_http::McpHttpError;
7+
68
#[cfg(feature = "auth")]
79
use crate::auth::AuthenticationError;
810

@@ -41,3 +43,262 @@ impl IntoResponse for TransportServerError {
4143
response
4244
}
4345
}
46+
47+
impl From<McpHttpError> for TransportServerError {
48+
fn from(err: McpHttpError) -> Self {
49+
match err {
50+
McpHttpError::SessionIdMissing => TransportServerError::SessionIdMissing,
51+
McpHttpError::SessionIdInvalid(s) => TransportServerError::SessionIdInvalid(s),
52+
McpHttpError::StreamIoError(s) => TransportServerError::StreamIoError(s),
53+
McpHttpError::HttpError(s) => TransportServerError::HttpError(s),
54+
McpHttpError::TransportError(s) => TransportServerError::TransportError(s),
55+
}
56+
}
57+
}
58+
59+
impl From<TransportServerError> for McpHttpError {
60+
fn from(err: TransportServerError) -> Self {
61+
match err {
62+
TransportServerError::SessionIdMissing => McpHttpError::SessionIdMissing,
63+
TransportServerError::SessionIdInvalid(s) => McpHttpError::SessionIdInvalid(s),
64+
TransportServerError::StreamIoError(s) => McpHttpError::StreamIoError(s),
65+
TransportServerError::HttpError(s) => McpHttpError::HttpError(s),
66+
TransportServerError::TransportError(s) => McpHttpError::TransportError(s),
67+
68+
#[cfg(feature = "auth")]
69+
TransportServerError::AuthenticationError(e) => McpHttpError::HttpError(e.to_string()),
70+
71+
TransportServerError::AddrParseError(e) => McpHttpError::HttpError(e.to_string()),
72+
TransportServerError::ServerStartError(s) => McpHttpError::HttpError(s),
73+
TransportServerError::InvalidServerOptions(s) => McpHttpError::HttpError(s),
74+
TransportServerError::SslCertError(s) => McpHttpError::HttpError(s),
75+
}
76+
}
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use super::*;
82+
use crate::mcp_http::McpHttpResult;
83+
84+
// McpHttpError to TransportServerError
85+
86+
#[test]
87+
fn mcp_to_transport_session_id_missing() {
88+
let m = McpHttpError::SessionIdMissing;
89+
let t: TransportServerError = m.into();
90+
assert!(matches!(t, TransportServerError::SessionIdMissing));
91+
assert_eq!(format!("{}", t), "'sessionId' query string is missing!");
92+
}
93+
94+
#[test]
95+
fn mcp_to_transport_session_id_invalid() {
96+
let m = McpHttpError::SessionIdInvalid("s1".into());
97+
let t: TransportServerError = m.into();
98+
assert!(matches!(t, TransportServerError::SessionIdInvalid(ref s) if s == "s1"));
99+
assert_eq!(format!("{}", t), "No session found for the given ID: s1.");
100+
}
101+
102+
#[test]
103+
fn mcp_to_transport_stream_io_error() {
104+
let m = McpHttpError::StreamIoError("io".into());
105+
let t: TransportServerError = m.into();
106+
assert!(matches!(t, TransportServerError::StreamIoError(ref s) if s == "io"));
107+
}
108+
109+
#[test]
110+
fn mcp_to_transport_http_error() {
111+
let m = McpHttpError::HttpError("fail".into());
112+
let t: TransportServerError = m.into();
113+
assert!(matches!(t, TransportServerError::HttpError(ref s) if s == "fail"));
114+
}
115+
116+
#[test]
117+
fn mcp_to_transport_transport_error() {
118+
let m = McpHttpError::TransportError("tcp".into());
119+
let t: TransportServerError = m.into();
120+
assert!(matches!(t, TransportServerError::TransportError(ref s) if s == "tcp"));
121+
}
122+
123+
// TransportServerError to McpHttpError (common variants)
124+
125+
#[test]
126+
fn transport_to_mcp_session_id_missing() {
127+
let t = TransportServerError::SessionIdMissing;
128+
let m: McpHttpError = t.into();
129+
assert!(matches!(m, McpHttpError::SessionIdMissing));
130+
}
131+
132+
#[test]
133+
fn transport_to_mcp_session_id_invalid() {
134+
let t = TransportServerError::SessionIdInvalid("s2".into());
135+
let m: McpHttpError = t.into();
136+
assert!(matches!(m, McpHttpError::SessionIdInvalid(ref s) if s == "s2"));
137+
}
138+
139+
#[test]
140+
fn transport_to_mcp_stream_io_error() {
141+
let t = TransportServerError::StreamIoError("eof".into());
142+
let m: McpHttpError = t.into();
143+
assert!(matches!(m, McpHttpError::StreamIoError(ref s) if s == "eof"));
144+
}
145+
146+
#[test]
147+
fn transport_to_mcp_http_error() {
148+
let t = TransportServerError::HttpError("gone".into());
149+
let m: McpHttpError = t.into();
150+
assert!(matches!(m, McpHttpError::HttpError(ref s) if s == "gone"));
151+
}
152+
153+
#[test]
154+
fn transport_to_mcp_transport_error() {
155+
let t = TransportServerError::TransportError("tls".into());
156+
let m: McpHttpError = t.into();
157+
assert!(matches!(m, McpHttpError::TransportError(ref s) if s == "tls"));
158+
}
159+
160+
// TransportServerError to McpHttpError (lossy conversions)
161+
162+
#[test]
163+
fn transport_to_mcp_addr_parse_lossy() {
164+
use std::net::AddrParseError;
165+
let parse_err: AddrParseError = ":::".parse::<std::net::IpAddr>().unwrap_err();
166+
let t = TransportServerError::AddrParseError(parse_err);
167+
let m: McpHttpError = t.into();
168+
assert!(matches!(m, McpHttpError::HttpError(ref s) if !s.is_empty()));
169+
}
170+
171+
#[test]
172+
fn transport_to_mcp_server_start_lossy() {
173+
let t = TransportServerError::ServerStartError("port in use".into());
174+
let m: McpHttpError = t.into();
175+
assert!(matches!(m, McpHttpError::HttpError(ref s) if s == "port in use"));
176+
}
177+
178+
#[test]
179+
fn transport_to_mcp_invalid_options_lossy() {
180+
let t = TransportServerError::InvalidServerOptions("bad config".into());
181+
let m: McpHttpError = t.into();
182+
assert!(matches!(m, McpHttpError::HttpError(ref s) if s == "bad config"));
183+
}
184+
185+
#[test]
186+
fn transport_to_mcp_ssl_cert_lossy() {
187+
let t = TransportServerError::SslCertError("cert expired".into());
188+
let m: McpHttpError = t.into();
189+
assert!(matches!(m, McpHttpError::HttpError(ref s) if s == "cert expired"));
190+
}
191+
192+
#[cfg(feature = "auth")]
193+
#[test]
194+
fn transport_to_mcp_authentication_lossy() {
195+
let auth_err = AuthenticationError::InactiveToken;
196+
let t = TransportServerError::AuthenticationError(auth_err);
197+
let m: McpHttpError = t.into();
198+
assert!(matches!(m, McpHttpError::HttpError(ref s) if s.contains("Inactive")));
199+
}
200+
201+
// Round-trip: McpHttpError to TransportServerError to McpHttpError
202+
203+
#[test]
204+
fn round_trip_session_id_missing() {
205+
let m = McpHttpError::SessionIdMissing;
206+
let t: TransportServerError = m.clone().into();
207+
let back: McpHttpError = t.into();
208+
assert_eq!(format!("{}", m), format!("{}", back));
209+
}
210+
211+
#[test]
212+
fn round_trip_session_id_invalid() {
213+
let m = McpHttpError::SessionIdInvalid("round".into());
214+
let t: TransportServerError = m.clone().into();
215+
let back: McpHttpError = t.into();
216+
assert_eq!(format!("{}", m), format!("{}", back));
217+
}
218+
219+
#[test]
220+
fn round_trip_stream_io_error() {
221+
let m = McpHttpError::StreamIoError("pipe".into());
222+
let t: TransportServerError = m.clone().into();
223+
let back: McpHttpError = t.into();
224+
assert_eq!(format!("{}", m), format!("{}", back));
225+
}
226+
227+
#[test]
228+
fn round_trip_http_error() {
229+
let m = McpHttpError::HttpError("round".into());
230+
let t: TransportServerError = m.clone().into();
231+
let back: McpHttpError = t.into();
232+
assert_eq!(format!("{}", m), format!("{}", back));
233+
}
234+
235+
#[test]
236+
fn round_trip_transport_error() {
237+
let m = McpHttpError::TransportError("round".into());
238+
let t: TransportServerError = m.clone().into();
239+
let back: McpHttpError = t.into();
240+
assert_eq!(format!("{}", m), format!("{}", back));
241+
}
242+
243+
// Round-trip: TransportServerError > McpHttpError > TransportServerError
244+
245+
#[test]
246+
fn reverse_round_trip_session_id_missing() {
247+
let t = TransportServerError::SessionIdMissing;
248+
let m: McpHttpError = t.clone().into();
249+
let back: TransportServerError = m.into();
250+
assert_eq!(format!("{}", t), format!("{}", back));
251+
}
252+
253+
#[test]
254+
fn reverse_round_trip_session_id_invalid() {
255+
let t = TransportServerError::SessionIdInvalid("rev".into());
256+
let m: McpHttpError = t.clone().into();
257+
let back: TransportServerError = m.into();
258+
assert_eq!(format!("{}", t), format!("{}", back));
259+
}
260+
261+
#[test]
262+
fn reverse_round_trip_stream_io_error() {
263+
let t = TransportServerError::StreamIoError("rev".into());
264+
let m: McpHttpError = t.clone().into();
265+
let back: TransportServerError = m.into();
266+
assert_eq!(format!("{}", t), format!("{}", back));
267+
}
268+
269+
#[test]
270+
fn reverse_round_trip_http_error() {
271+
let t = TransportServerError::HttpError("rev".into());
272+
let m: McpHttpError = t.clone().into();
273+
let back: TransportServerError = m.into();
274+
assert_eq!(format!("{}", t), format!("{}", back));
275+
}
276+
277+
#[test]
278+
fn reverse_round_trip_transport_error() {
279+
let t = TransportServerError::TransportError("rev".into());
280+
let m: McpHttpError = t.clone().into();
281+
let back: TransportServerError = m.into();
282+
assert_eq!(format!("{}", t), format!("{}", back));
283+
}
284+
285+
#[test]
286+
fn transport_result_from_mcp_http_error() {
287+
let r: McpHttpResult<()> = Err(McpHttpError::SessionIdMissing);
288+
let t: TransportServerResult<()> = r.map_err(Into::into);
289+
assert!(matches!(
290+
t.unwrap_err(),
291+
TransportServerError::SessionIdMissing
292+
));
293+
}
294+
295+
#[test]
296+
fn mcp_http_result_from_transport_error() {
297+
let r: TransportServerResult<()> = Err(TransportServerError::SessionIdInvalid("x".into()));
298+
let m: McpHttpResult<()> = r.map_err(Into::into);
299+
assert!(matches!(
300+
m.unwrap_err(),
301+
McpHttpError::SessionIdInvalid(ref s) if s == "x"
302+
));
303+
}
304+
}

crates/rust-mcp-sdk/src/mcp_http.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod app_state;
2+
pub(crate) mod error;
23
mod health_handler;
34
pub(crate) mod http_utils;
45
mod mcp_http_handler;
@@ -7,6 +8,7 @@ pub mod middleware;
78
mod types;
89

910
pub use app_state::*;
11+
pub use error::*;
1012
pub use http_utils::*;
1113
pub use mcp_http_handler::*;
1214

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use thiserror::Error;
2+
3+
pub type McpHttpResult<T> = core::result::Result<T, McpHttpError>;
4+
5+
#[derive(Debug, Clone, Error)]
6+
pub enum McpHttpError {
7+
#[error("'sessionId' query string is missing!")]
8+
SessionIdMissing,
9+
10+
#[error("No session found for the given ID: {0}.")]
11+
SessionIdInvalid(String),
12+
13+
#[error("Stream IO Error: {0}.")]
14+
StreamIoError(String),
15+
16+
#[error("{0}")]
17+
HttpError(String),
18+
19+
#[error("{0}")]
20+
TransportError(String),
21+
}
22+
23+
#[cfg(test)]
24+
mod tests {
25+
use super::*;
26+
27+
#[test]
28+
fn display_session_id_missing() {
29+
let err = McpHttpError::SessionIdMissing;
30+
assert_eq!(format!("{}", err), "'sessionId' query string is missing!");
31+
}
32+
33+
#[test]
34+
fn display_session_id_invalid() {
35+
let err = McpHttpError::SessionIdInvalid("abc-123".into());
36+
assert_eq!(
37+
format!("{}", err),
38+
"No session found for the given ID: abc-123."
39+
);
40+
}
41+
42+
#[test]
43+
fn display_stream_io_error() {
44+
let err = McpHttpError::StreamIoError("broken pipe".into());
45+
assert_eq!(format!("{}", err), "Stream IO Error: broken pipe.");
46+
}
47+
48+
#[test]
49+
fn display_http_error() {
50+
let err = McpHttpError::HttpError("bad request".into());
51+
assert_eq!(format!("{}", err), "bad request");
52+
}
53+
54+
#[test]
55+
fn display_transport_error() {
56+
let err = McpHttpError::TransportError("timeout".into());
57+
assert_eq!(format!("{}", err), "timeout");
58+
}
59+
60+
#[test]
61+
fn clone_preserves_value() {
62+
let err = McpHttpError::SessionIdInvalid("xyz".into());
63+
let cloned = err.clone();
64+
assert_eq!(format!("{}", err), format!("{}", cloned));
65+
}
66+
67+
#[test]
68+
fn debug_format_includes_variant() {
69+
let err = McpHttpError::SessionIdMissing;
70+
let debug = format!("{:?}", err);
71+
assert!(debug.contains("SessionIdMissing"));
72+
}
73+
74+
#[test]
75+
fn clone_unit_variant() {
76+
let err = McpHttpError::SessionIdMissing;
77+
let cloned = err.clone();
78+
assert_eq!(format!("{:?}", err), format!("{:?}", cloned));
79+
}
80+
81+
#[test]
82+
fn http_result_ok() {
83+
let result: McpHttpResult<i32> = Ok(42);
84+
assert_eq!(result.unwrap(), 42);
85+
}
86+
87+
#[test]
88+
fn http_result_err() {
89+
let result: McpHttpResult<i32> = Err(McpHttpError::HttpError("fail".into()));
90+
assert!(result.is_err());
91+
assert_eq!(format!("{}", result.unwrap_err()), "fail");
92+
}
93+
}

0 commit comments

Comments
 (0)