Skip to main content

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}