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(
74        "server did not return Mcp-Session-Id header at {url}; body: {body}"
75    )]
76    NoSessionId {
77        /// The URL we attempted to initialize against.
78        url: String,
79        /// The response body the server returned, truncated to a
80        /// reasonable preview length. Often carries a JSON-RPC error
81        /// describing why the session wasn't created (e.g., upstream
82        /// connect failed for a specific URL).
83        body: String,
84    },
85    /// Authorization required but not provided for this MCP server URL.
86    #[error("missing authorization for MCP server: {0}")]
87    MissingAuthorization(String),
88    /// The server returned a body that wasn't decodable as JSON or SSE.
89    #[error("malformed JSON-RPC response from {url}: {message}")]
90    MalformedResponse {
91        /// The URL that produced the unparseable response.
92        url: String,
93        /// What was wrong with the body, including a preview.
94        message: String,
95    },
96    /// The server did not declare the capability required to service this
97    /// request. Returned by `call_tool` when the server has no `tools`
98    /// capability, and by `read_resource` when the server has no
99    /// `resources` capability.
100    #[error("server does not support the {capability} capability")]
101    UnsupportedCapability {
102        /// The capability that's missing (`"tools"` or `"resources"`).
103        capability: &'static str,
104    },
105}