Skip to main content

tower_mcp_types/
error.rs

1//! Error types for MCP
2//!
3//! ## JSON-RPC Error Codes
4//!
5//! Standard JSON-RPC 2.0 error codes are defined in the specification:
6//! <https://www.jsonrpc.org/specification#error_object>
7//!
8//! | Code   | Message          | Meaning                                  |
9//! |--------|------------------|------------------------------------------|
10//! | -32700 | Parse error      | Invalid JSON was received                |
11//! | -32600 | Invalid Request  | The JSON sent is not a valid Request     |
12//! | -32601 | Method not found | The method does not exist / is not available |
13//! | -32602 | Invalid params   | Invalid method parameter(s)              |
14//! | -32603 | Internal error   | Internal JSON-RPC error                  |
15//!
16//! ## MCP-Specific Error Codes
17//!
18//! MCP uses the server error range (-32000 to -32099) for protocol-specific errors:
19//!
20//! | Code   | Name            | Meaning                                  |
21//! |--------|-----------------|------------------------------------------|
22//! | -32000 | ConnectionClosed| Transport connection was closed          |
23//! | -32001 | RequestTimeout  | Request exceeded timeout                 |
24//! | -32002 | ResourceNotFound| Resource not found                       |
25//! | -32003 | AlreadySubscribed| Resource already subscribed             |
26//! | -32004 | NotSubscribed   | Resource not subscribed (for unsubscribe)|
27//! | -32005 | SessionNotFound | Session not found or expired             |
28//! | -32006 | SessionRequired | MCP-Session-Id header is required        |
29//! | -32007 | Forbidden       | Access forbidden (insufficient scope)    |
30//! | -32042 | UrlElicitationRequired | URL elicitation required          |
31
32use serde::{Deserialize, Serialize};
33
34/// Type-erased error type used for middleware composition.
35///
36/// This is the standard error type in the tower ecosystem, used by
37/// [`tower`](https://docs.rs/tower), [`tower-http`](https://docs.rs/tower-http),
38/// and other tower-compatible crates.
39pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
40
41/// Standard JSON-RPC error codes
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[repr(i32)]
44#[non_exhaustive]
45pub enum ErrorCode {
46    /// Invalid JSON was received
47    ParseError = -32700,
48    /// The JSON sent is not a valid Request object
49    InvalidRequest = -32600,
50    /// The method does not exist / is not available
51    MethodNotFound = -32601,
52    /// Invalid method parameter(s)
53    InvalidParams = -32602,
54    /// Internal JSON-RPC error
55    InternalError = -32603,
56}
57
58/// MCP-specific error codes (in the -32000 to -32099 range)
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[repr(i32)]
61#[non_exhaustive]
62pub enum McpErrorCode {
63    /// Transport connection was closed
64    ConnectionClosed = -32000,
65    /// Request exceeded timeout
66    RequestTimeout = -32001,
67    /// Resource not found
68    ResourceNotFound = -32002,
69    /// Resource already subscribed
70    AlreadySubscribed = -32003,
71    /// Resource not subscribed (for unsubscribe)
72    NotSubscribed = -32004,
73    /// Session not found or expired - client should re-initialize
74    SessionNotFound = -32005,
75    /// Session ID is required but was not provided
76    SessionRequired = -32006,
77    /// Access forbidden (insufficient scope or authorization)
78    Forbidden = -32007,
79    /// URL elicitation is required before processing the request
80    UrlElicitationRequired = -32042,
81}
82
83impl McpErrorCode {
84    pub fn code(self) -> i32 {
85        self as i32
86    }
87}
88
89impl ErrorCode {
90    pub fn code(self) -> i32 {
91        self as i32
92    }
93}
94
95/// JSON-RPC error object
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct JsonRpcError {
98    pub code: i32,
99    pub message: String,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub data: Option<serde_json::Value>,
102}
103
104impl JsonRpcError {
105    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
106        Self {
107            code: code.code(),
108            message: message.into(),
109            data: None,
110        }
111    }
112
113    pub fn with_data(mut self, data: serde_json::Value) -> Self {
114        self.data = Some(data);
115        self
116    }
117
118    pub fn parse_error(message: impl Into<String>) -> Self {
119        Self::new(ErrorCode::ParseError, message)
120    }
121
122    pub fn invalid_request(message: impl Into<String>) -> Self {
123        Self::new(ErrorCode::InvalidRequest, message)
124    }
125
126    pub fn method_not_found(method: &str) -> Self {
127        Self::new(
128            ErrorCode::MethodNotFound,
129            format!("Method not found: {}", method),
130        )
131    }
132
133    pub fn invalid_params(message: impl Into<String>) -> Self {
134        Self::new(ErrorCode::InvalidParams, message)
135    }
136
137    pub fn internal_error(message: impl Into<String>) -> Self {
138        Self::new(ErrorCode::InternalError, message)
139    }
140
141    /// Create an MCP-specific error
142    pub fn mcp_error(code: McpErrorCode, message: impl Into<String>) -> Self {
143        Self {
144            code: code.code(),
145            message: message.into(),
146            data: None,
147        }
148    }
149
150    /// Connection was closed
151    pub fn connection_closed(message: impl Into<String>) -> Self {
152        Self::mcp_error(McpErrorCode::ConnectionClosed, message)
153    }
154
155    /// Request timed out
156    pub fn request_timeout(message: impl Into<String>) -> Self {
157        Self::mcp_error(McpErrorCode::RequestTimeout, message)
158    }
159
160    /// Resource not found
161    pub fn resource_not_found(uri: &str) -> Self {
162        Self::mcp_error(
163            McpErrorCode::ResourceNotFound,
164            format!("Resource not found: {}", uri),
165        )
166    }
167
168    /// Resource already subscribed
169    pub fn already_subscribed(uri: &str) -> Self {
170        Self::mcp_error(
171            McpErrorCode::AlreadySubscribed,
172            format!("Already subscribed to: {}", uri),
173        )
174    }
175
176    /// Resource not subscribed
177    pub fn not_subscribed(uri: &str) -> Self {
178        Self::mcp_error(
179            McpErrorCode::NotSubscribed,
180            format!("Not subscribed to: {}", uri),
181        )
182    }
183
184    /// Session not found or expired
185    ///
186    /// Clients receiving this error should re-initialize the connection.
187    /// The session may have expired due to inactivity or server restart.
188    pub fn session_not_found() -> Self {
189        Self::mcp_error(
190            McpErrorCode::SessionNotFound,
191            "Session not found or expired. Please re-initialize the connection.",
192        )
193    }
194
195    /// Session not found with a specific session ID
196    pub fn session_not_found_with_id(session_id: &str) -> Self {
197        Self::mcp_error(
198            McpErrorCode::SessionNotFound,
199            format!(
200                "Session '{}' not found or expired. Please re-initialize the connection.",
201                session_id
202            ),
203        )
204    }
205
206    /// Session ID is required
207    pub fn session_required() -> Self {
208        Self::mcp_error(
209            McpErrorCode::SessionRequired,
210            "MCP-Session-Id header is required for this request.",
211        )
212    }
213
214    /// Access forbidden (insufficient scope or authorization)
215    pub fn forbidden(message: impl Into<String>) -> Self {
216        Self::mcp_error(McpErrorCode::Forbidden, message)
217    }
218
219    /// URL elicitation is required before processing the request
220    pub fn url_elicitation_required(message: impl Into<String>) -> Self {
221        Self::mcp_error(McpErrorCode::UrlElicitationRequired, message)
222    }
223}
224
225/// Tool execution error with context
226#[derive(Debug)]
227pub struct ToolError {
228    /// The tool name that failed
229    pub tool: Option<String>,
230    /// Error message
231    pub message: String,
232    /// Source error if any
233    pub source: Option<BoxError>,
234}
235
236impl std::fmt::Display for ToolError {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        if let Some(tool) = &self.tool {
239            write!(f, "Tool '{}' error: {}", tool, self.message)
240        } else {
241            write!(f, "Tool error: {}", self.message)
242        }
243    }
244}
245
246impl std::error::Error for ToolError {
247    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
248        self.source
249            .as_ref()
250            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
251    }
252}
253
254impl ToolError {
255    /// Create a new tool error with just a message
256    pub fn new(message: impl Into<String>) -> Self {
257        Self {
258            tool: None,
259            message: message.into(),
260            source: None,
261        }
262    }
263
264    /// Create a tool error with the tool name
265    pub fn with_tool(tool: impl Into<String>, message: impl Into<String>) -> Self {
266        Self {
267            tool: Some(tool.into()),
268            message: message.into(),
269            source: None,
270        }
271    }
272
273    /// Add a source error
274    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
275        self.source = Some(Box::new(source));
276        self
277    }
278}
279
280/// tower-mcp error type
281#[derive(Debug, thiserror::Error)]
282#[non_exhaustive]
283pub enum Error {
284    #[error("JSON-RPC error: {0:?}")]
285    JsonRpc(JsonRpcError),
286
287    #[error("Serialization error: {0}")]
288    Serialization(#[from] serde_json::Error),
289
290    /// A tool execution error.
291    ///
292    /// When returned from a tool handler, this variant is mapped to JSON-RPC
293    /// error code `-32603` (Internal Error) in the router's `Service::call`
294    /// implementation. The `ToolError` message becomes the JSON-RPC error message.
295    #[error("{0}")]
296    Tool(#[from] ToolError),
297
298    #[error("Transport error: {0}")]
299    Transport(String),
300
301    /// The server indicated the session has expired or is not found.
302    ///
303    /// This corresponds to JSON-RPC error code `-32005` (SessionNotFound)
304    /// or an HTTP 404 response when a session ID was attached.
305    /// Clients should re-initialize the connection.
306    #[error("Session expired")]
307    SessionExpired,
308
309    #[error("Internal error: {0}")]
310    Internal(String),
311}
312
313impl Error {
314    /// Create a simple tool error from a string (for backwards compatibility)
315    pub fn tool(message: impl Into<String>) -> Self {
316        Error::Tool(ToolError::new(message))
317    }
318
319    /// Create a tool error with the tool name
320    pub fn tool_with_name(tool: impl Into<String>, message: impl Into<String>) -> Self {
321        Error::Tool(ToolError::with_tool(tool, message))
322    }
323
324    /// Create a tool error from any `Display` type.
325    ///
326    /// This is useful for converting errors in a `map_err` chain:
327    ///
328    /// ```rust
329    /// # use tower_mcp_types::Error;
330    /// # fn example() -> Result<(), Error> {
331    /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("oops"));
332    /// result.map_err(Error::tool_from)?;
333    /// # Ok(())
334    /// # }
335    /// ```
336    pub fn tool_from<E: std::fmt::Display>(err: E) -> Self {
337        Error::Tool(ToolError::new(err.to_string()))
338    }
339
340    /// Create a tool error with context prefix.
341    ///
342    /// This is useful for adding context when converting errors.
343    /// For a more ergonomic API, see [`ResultExt::tool_context`] which can be
344    /// called directly on `Result` values:
345    ///
346    /// ```rust
347    /// # use tower_mcp_types::error::ResultExt;
348    /// # fn example() -> tower_mcp_types::Result<()> {
349    /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("connection refused"));
350    /// result.tool_context("API request failed")?;
351    /// # Ok(())
352    /// # }
353    /// ```
354    pub fn tool_context<E: std::fmt::Display>(context: impl Into<String>, err: E) -> Self {
355        Error::Tool(ToolError::new(format!("{}: {}", context.into(), err)))
356    }
357
358    /// Create a JSON-RPC "Invalid params" error (`-32602`).
359    ///
360    /// Shorthand for `Error::JsonRpc(JsonRpcError::invalid_params(msg))`.
361    ///
362    /// ```rust
363    /// # use tower_mcp_types::Error;
364    /// let err = Error::invalid_params("missing required field 'name'");
365    /// ```
366    pub fn invalid_params(message: impl Into<String>) -> Self {
367        Error::JsonRpc(JsonRpcError::invalid_params(message))
368    }
369
370    /// Create a JSON-RPC "Internal error" error (`-32603`).
371    ///
372    /// Shorthand for `Error::JsonRpc(JsonRpcError::internal_error(msg))`.
373    ///
374    /// ```rust
375    /// # use tower_mcp_types::Error;
376    /// let err = Error::internal("unexpected state");
377    /// ```
378    pub fn internal(message: impl Into<String>) -> Self {
379        Error::JsonRpc(JsonRpcError::internal_error(message))
380    }
381}
382
383/// Extension trait for converting errors into tower-mcp tool errors.
384///
385/// Provides ergonomic error conversion methods on `Result` types,
386/// similar to `anyhow::Context`. Import this trait to use `.tool_err()`
387/// and `.tool_context()` on any `Result` whose error type implements `Display`.
388///
389/// # Examples
390///
391/// ```rust
392/// use tower_mcp_types::error::ResultExt;
393///
394/// fn query_database() -> tower_mcp_types::Result<String> {
395///     let result: Result<String, std::io::Error> =
396///         Err(std::io::Error::other("connection refused"));
397///     let value = result.tool_context("database query failed")?;
398///     Ok(value)
399/// }
400/// ```
401pub trait ResultExt<T> {
402    /// Convert the error into a tool error.
403    ///
404    /// ```rust
405    /// use tower_mcp_types::error::ResultExt;
406    /// # fn example() -> tower_mcp_types::Result<()> {
407    /// let value: Result<i32, std::io::Error> = Err(std::io::Error::other("timeout"));
408    /// let value = value.tool_err()?;
409    /// # Ok(())
410    /// # }
411    /// ```
412    fn tool_err(self) -> std::result::Result<T, Error>;
413
414    /// Convert the error into a tool error with additional context.
415    ///
416    /// ```rust
417    /// use tower_mcp_types::error::ResultExt;
418    /// # fn example() -> tower_mcp_types::Result<()> {
419    /// let value: Result<i32, std::io::Error> = Err(std::io::Error::other("timeout"));
420    /// let value = value.tool_context("database query failed")?;
421    /// # Ok(())
422    /// # }
423    /// ```
424    fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error>;
425}
426
427impl<T, E: std::fmt::Display> ResultExt<T> for std::result::Result<T, E> {
428    fn tool_err(self) -> std::result::Result<T, Error> {
429        self.map_err(Error::tool_from)
430    }
431
432    fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error> {
433        self.map_err(|e| Error::tool_context(context, e))
434    }
435}
436
437impl From<JsonRpcError> for Error {
438    fn from(err: JsonRpcError) -> Self {
439        Error::JsonRpc(err)
440    }
441}
442
443impl From<std::convert::Infallible> for Error {
444    fn from(err: std::convert::Infallible) -> Self {
445        match err {}
446    }
447}
448
449/// Result type alias for tower-mcp
450pub type Result<T> = std::result::Result<T, Error>;
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_box_error_from_io_error() {
458        let io_err = std::io::Error::other("disk full");
459        let boxed: BoxError = io_err.into();
460        assert_eq!(boxed.to_string(), "disk full");
461    }
462
463    #[test]
464    fn test_box_error_from_string() {
465        let err: BoxError = "something went wrong".into();
466        assert_eq!(err.to_string(), "something went wrong");
467    }
468
469    #[test]
470    fn test_box_error_is_send_sync() {
471        fn assert_send_sync<T: Send + Sync>() {}
472        assert_send_sync::<BoxError>();
473    }
474
475    #[test]
476    fn test_tool_error_source_uses_box_error() {
477        let io_err = std::io::Error::other("timeout");
478        let tool_err = ToolError::new("failed").with_source(io_err);
479        assert!(tool_err.source.is_some());
480        assert_eq!(tool_err.source.unwrap().to_string(), "timeout");
481    }
482
483    #[test]
484    fn test_result_ext_tool_err() {
485        let result: std::result::Result<(), std::io::Error> =
486            Err(std::io::Error::other("disk full"));
487        let err = result.tool_err().unwrap_err();
488        assert!(matches!(err, Error::Tool(_)));
489        assert!(err.to_string().contains("disk full"));
490    }
491
492    #[test]
493    fn test_result_ext_tool_context() {
494        let result: std::result::Result<(), std::io::Error> =
495            Err(std::io::Error::other("connection refused"));
496        let err = result.tool_context("database query failed").unwrap_err();
497        assert!(matches!(err, Error::Tool(_)));
498        assert!(err.to_string().contains("database query failed"));
499        assert!(err.to_string().contains("connection refused"));
500    }
501
502    #[test]
503    fn test_result_ext_ok_passes_through() {
504        let result: std::result::Result<i32, std::io::Error> = Ok(42);
505        assert_eq!(result.tool_err().unwrap(), 42);
506        let result: std::result::Result<i32, std::io::Error> = Ok(42);
507        assert_eq!(result.tool_context("should not appear").unwrap(), 42);
508    }
509}