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    #[error("Internal error: {0}")]
302    Internal(String),
303}
304
305impl Error {
306    /// Create a simple tool error from a string (for backwards compatibility)
307    pub fn tool(message: impl Into<String>) -> Self {
308        Error::Tool(ToolError::new(message))
309    }
310
311    /// Create a tool error with the tool name
312    pub fn tool_with_name(tool: impl Into<String>, message: impl Into<String>) -> Self {
313        Error::Tool(ToolError::with_tool(tool, message))
314    }
315
316    /// Create a tool error from any `Display` type.
317    ///
318    /// This is useful for converting errors in a `map_err` chain:
319    ///
320    /// ```rust
321    /// # use tower_mcp_types::Error;
322    /// # fn example() -> Result<(), Error> {
323    /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("oops"));
324    /// result.map_err(Error::tool_from)?;
325    /// # Ok(())
326    /// # }
327    /// ```
328    pub fn tool_from<E: std::fmt::Display>(err: E) -> Self {
329        Error::Tool(ToolError::new(err.to_string()))
330    }
331
332    /// Create a tool error with context prefix.
333    ///
334    /// This is useful for adding context when converting errors.
335    /// For a more ergonomic API, see [`ResultExt::tool_context`] which can be
336    /// called directly on `Result` values:
337    ///
338    /// ```rust
339    /// # use tower_mcp_types::error::ResultExt;
340    /// # fn example() -> tower_mcp_types::Result<()> {
341    /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("connection refused"));
342    /// result.tool_context("API request failed")?;
343    /// # Ok(())
344    /// # }
345    /// ```
346    pub fn tool_context<E: std::fmt::Display>(context: impl Into<String>, err: E) -> Self {
347        Error::Tool(ToolError::new(format!("{}: {}", context.into(), err)))
348    }
349
350    /// Create a JSON-RPC "Invalid params" error (`-32602`).
351    ///
352    /// Shorthand for `Error::JsonRpc(JsonRpcError::invalid_params(msg))`.
353    ///
354    /// ```rust
355    /// # use tower_mcp_types::Error;
356    /// let err = Error::invalid_params("missing required field 'name'");
357    /// ```
358    pub fn invalid_params(message: impl Into<String>) -> Self {
359        Error::JsonRpc(JsonRpcError::invalid_params(message))
360    }
361
362    /// Create a JSON-RPC "Internal error" error (`-32603`).
363    ///
364    /// Shorthand for `Error::JsonRpc(JsonRpcError::internal_error(msg))`.
365    ///
366    /// ```rust
367    /// # use tower_mcp_types::Error;
368    /// let err = Error::internal("unexpected state");
369    /// ```
370    pub fn internal(message: impl Into<String>) -> Self {
371        Error::JsonRpc(JsonRpcError::internal_error(message))
372    }
373}
374
375/// Extension trait for converting errors into tower-mcp tool errors.
376///
377/// Provides ergonomic error conversion methods on `Result` types,
378/// similar to `anyhow::Context`. Import this trait to use `.tool_err()`
379/// and `.tool_context()` on any `Result` whose error type implements `Display`.
380///
381/// # Examples
382///
383/// ```rust
384/// use tower_mcp_types::error::ResultExt;
385///
386/// fn query_database() -> tower_mcp_types::Result<String> {
387///     let result: Result<String, std::io::Error> =
388///         Err(std::io::Error::other("connection refused"));
389///     let value = result.tool_context("database query failed")?;
390///     Ok(value)
391/// }
392/// ```
393pub trait ResultExt<T> {
394    /// Convert the error into a tool error.
395    ///
396    /// ```rust
397    /// use tower_mcp_types::error::ResultExt;
398    /// # fn example() -> tower_mcp_types::Result<()> {
399    /// let value: Result<i32, std::io::Error> = Err(std::io::Error::other("timeout"));
400    /// let value = value.tool_err()?;
401    /// # Ok(())
402    /// # }
403    /// ```
404    fn tool_err(self) -> std::result::Result<T, Error>;
405
406    /// Convert the error into a tool error with additional context.
407    ///
408    /// ```rust
409    /// use tower_mcp_types::error::ResultExt;
410    /// # fn example() -> tower_mcp_types::Result<()> {
411    /// let value: Result<i32, std::io::Error> = Err(std::io::Error::other("timeout"));
412    /// let value = value.tool_context("database query failed")?;
413    /// # Ok(())
414    /// # }
415    /// ```
416    fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error>;
417}
418
419impl<T, E: std::fmt::Display> ResultExt<T> for std::result::Result<T, E> {
420    fn tool_err(self) -> std::result::Result<T, Error> {
421        self.map_err(Error::tool_from)
422    }
423
424    fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error> {
425        self.map_err(|e| Error::tool_context(context, e))
426    }
427}
428
429impl From<JsonRpcError> for Error {
430    fn from(err: JsonRpcError) -> Self {
431        Error::JsonRpc(err)
432    }
433}
434
435impl From<std::convert::Infallible> for Error {
436    fn from(err: std::convert::Infallible) -> Self {
437        match err {}
438    }
439}
440
441/// Result type alias for tower-mcp
442pub type Result<T> = std::result::Result<T, Error>;
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn test_box_error_from_io_error() {
450        let io_err = std::io::Error::other("disk full");
451        let boxed: BoxError = io_err.into();
452        assert_eq!(boxed.to_string(), "disk full");
453    }
454
455    #[test]
456    fn test_box_error_from_string() {
457        let err: BoxError = "something went wrong".into();
458        assert_eq!(err.to_string(), "something went wrong");
459    }
460
461    #[test]
462    fn test_box_error_is_send_sync() {
463        fn assert_send_sync<T: Send + Sync>() {}
464        assert_send_sync::<BoxError>();
465    }
466
467    #[test]
468    fn test_tool_error_source_uses_box_error() {
469        let io_err = std::io::Error::other("timeout");
470        let tool_err = ToolError::new("failed").with_source(io_err);
471        assert!(tool_err.source.is_some());
472        assert_eq!(tool_err.source.unwrap().to_string(), "timeout");
473    }
474
475    #[test]
476    fn test_result_ext_tool_err() {
477        let result: std::result::Result<(), std::io::Error> =
478            Err(std::io::Error::other("disk full"));
479        let err = result.tool_err().unwrap_err();
480        assert!(matches!(err, Error::Tool(_)));
481        assert!(err.to_string().contains("disk full"));
482    }
483
484    #[test]
485    fn test_result_ext_tool_context() {
486        let result: std::result::Result<(), std::io::Error> =
487            Err(std::io::Error::other("connection refused"));
488        let err = result.tool_context("database query failed").unwrap_err();
489        assert!(matches!(err, Error::Tool(_)));
490        assert!(err.to_string().contains("database query failed"));
491        assert!(err.to_string().contains("connection refused"));
492    }
493
494    #[test]
495    fn test_result_ext_ok_passes_through() {
496        let result: std::result::Result<i32, std::io::Error> = Ok(42);
497        assert_eq!(result.tool_err().unwrap(), 42);
498        let result: std::result::Result<i32, std::io::Error> = Ok(42);
499        assert_eq!(result.tool_context("should not appear").unwrap(), 42);
500    }
501}