objectiveai_sdk/mcp/error.rs
1//! MCP client errors.
2
3/// Walk the `Error::source` chain of any `std::error::Error` and join
4/// every level's `Display` with `: ` so the bottom-most cause (e.g. an
5/// I/O `deadline has elapsed` deep under a `reqwest::Error`) actually
6/// reaches the user instead of being hidden behind a generic outer
7/// wrapper.
8fn fmt_error_chain(err: &dyn std::error::Error) -> String {
9 let mut out = err.to_string();
10 let mut current = err.source();
11 while let Some(source) = current {
12 let s = source.to_string();
13 // Skip duplicate levels — `reqwest::Error::Display` sometimes
14 // repeats its own source, which would produce noise like
15 // `... : foo: foo`.
16 if !out.ends_with(&s) {
17 out.push_str(": ");
18 out.push_str(&s);
19 }
20 current = source.source();
21 }
22 out
23}
24
25/// Errors that can occur during MCP operations.
26#[derive(Debug, thiserror::Error)]
27pub enum Error {
28 /// Failed to connect to the MCP server.
29 #[error("connection error to {url}: {}", fmt_error_chain(source))]
30 Connection {
31 /// The URL the failing request was targeting.
32 url: String,
33 /// The underlying reqwest error.
34 source: reqwest::Error,
35 },
36 /// HTTP request failed (post-connect).
37 #[error("request error to {url}: {}", fmt_error_chain(source))]
38 Request {
39 /// The URL the failing request was targeting.
40 url: String,
41 /// The underlying reqwest error.
42 source: reqwest::Error,
43 },
44 /// Server returned a non-success HTTP status code.
45 #[error("bad status from {url} ({code}): {body}")]
46 BadStatus {
47 /// The URL the failing request was targeting.
48 url: String,
49 /// The HTTP status code received.
50 code: reqwest::StatusCode,
51 /// The response body.
52 body: String,
53 },
54 /// The server returned a JSON-RPC error.
55 #[error("json-rpc error from {url} ({code}): {message}{}", data.as_ref().map(|d| format!("; data: {d}")).unwrap_or_default())]
56 JsonRpc {
57 /// The URL the failing request was targeting.
58 url: String,
59 /// The JSON-RPC error code.
60 code: i64,
61 /// The error message.
62 message: String,
63 /// Optional additional error data.
64 data: Option<serde_json::Value>,
65 },
66 /// The session expired (server returned 404).
67 #[error("session expired at {url}")]
68 SessionExpired {
69 /// The URL whose session was expired.
70 url: String,
71 },
72 /// The server did not return a session ID on initialization.
73 #[error("server did not return Mcp-Session-Id header at {url}; body: {body}")]
74 NoSessionId {
75 /// The URL we attempted to initialize against.
76 url: String,
77 /// The response body the server returned, truncated to a
78 /// reasonable preview length. Often carries a JSON-RPC error
79 /// describing why the session wasn't created (e.g., upstream
80 /// connect failed for a specific URL).
81 body: String,
82 },
83 /// Authorization required but not provided for this MCP server URL.
84 #[error("missing authorization for MCP server: {0}")]
85 MissingAuthorization(String),
86 /// The server returned a body that wasn't decodable as JSON or SSE.
87 #[error("malformed JSON-RPC response from {url}: {message}")]
88 MalformedResponse {
89 /// The URL that produced the unparseable response.
90 url: String,
91 /// What was wrong with the body, including a preview.
92 message: String,
93 },
94}