Skip to main content

tower_mcp/
error.rs

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