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
31use serde::{Deserialize, Serialize};
32
33/// Type-erased error type used for middleware composition.
34///
35/// This is the standard error type in the tower ecosystem, used by
36/// [`tower`](https://docs.rs/tower), [`tower-http`](https://docs.rs/tower-http),
37/// and other tower-compatible crates.
38pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
39
40/// Standard JSON-RPC error codes
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[repr(i32)]
43pub enum ErrorCode {
44    /// Invalid JSON was received
45    ParseError = -32700,
46    /// The JSON sent is not a valid Request object
47    InvalidRequest = -32600,
48    /// The method does not exist / is not available
49    MethodNotFound = -32601,
50    /// Invalid method parameter(s)
51    InvalidParams = -32602,
52    /// Internal JSON-RPC error
53    InternalError = -32603,
54}
55
56/// MCP-specific error codes (in the -32000 to -32099 range)
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58#[repr(i32)]
59pub enum McpErrorCode {
60    /// Transport connection was closed
61    ConnectionClosed = -32000,
62    /// Request exceeded timeout
63    RequestTimeout = -32001,
64    /// Resource not found
65    ResourceNotFound = -32002,
66    /// Resource already subscribed
67    AlreadySubscribed = -32003,
68    /// Resource not subscribed (for unsubscribe)
69    NotSubscribed = -32004,
70    /// Session not found or expired - client should re-initialize
71    SessionNotFound = -32005,
72    /// Session ID is required but was not provided
73    SessionRequired = -32006,
74    /// Access forbidden (insufficient scope or authorization)
75    Forbidden = -32007,
76}
77
78impl McpErrorCode {
79    pub fn code(self) -> i32 {
80        self as i32
81    }
82}
83
84impl ErrorCode {
85    pub fn code(self) -> i32 {
86        self as i32
87    }
88}
89
90/// JSON-RPC error object
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct JsonRpcError {
93    pub code: i32,
94    pub message: String,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub data: Option<serde_json::Value>,
97}
98
99impl JsonRpcError {
100    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
101        Self {
102            code: code.code(),
103            message: message.into(),
104            data: None,
105        }
106    }
107
108    pub fn with_data(mut self, data: serde_json::Value) -> Self {
109        self.data = Some(data);
110        self
111    }
112
113    pub fn parse_error(message: impl Into<String>) -> Self {
114        Self::new(ErrorCode::ParseError, message)
115    }
116
117    pub fn invalid_request(message: impl Into<String>) -> Self {
118        Self::new(ErrorCode::InvalidRequest, message)
119    }
120
121    pub fn method_not_found(method: &str) -> Self {
122        Self::new(
123            ErrorCode::MethodNotFound,
124            format!("Method not found: {}", method),
125        )
126    }
127
128    pub fn invalid_params(message: impl Into<String>) -> Self {
129        Self::new(ErrorCode::InvalidParams, message)
130    }
131
132    pub fn internal_error(message: impl Into<String>) -> Self {
133        Self::new(ErrorCode::InternalError, message)
134    }
135
136    /// Create an MCP-specific error
137    pub fn mcp_error(code: McpErrorCode, message: impl Into<String>) -> Self {
138        Self {
139            code: code.code(),
140            message: message.into(),
141            data: None,
142        }
143    }
144
145    /// Connection was closed
146    pub fn connection_closed(message: impl Into<String>) -> Self {
147        Self::mcp_error(McpErrorCode::ConnectionClosed, message)
148    }
149
150    /// Request timed out
151    pub fn request_timeout(message: impl Into<String>) -> Self {
152        Self::mcp_error(McpErrorCode::RequestTimeout, message)
153    }
154
155    /// Resource not found
156    pub fn resource_not_found(uri: &str) -> Self {
157        Self::mcp_error(
158            McpErrorCode::ResourceNotFound,
159            format!("Resource not found: {}", uri),
160        )
161    }
162
163    /// Resource already subscribed
164    pub fn already_subscribed(uri: &str) -> Self {
165        Self::mcp_error(
166            McpErrorCode::AlreadySubscribed,
167            format!("Already subscribed to: {}", uri),
168        )
169    }
170
171    /// Resource not subscribed
172    pub fn not_subscribed(uri: &str) -> Self {
173        Self::mcp_error(
174            McpErrorCode::NotSubscribed,
175            format!("Not subscribed to: {}", uri),
176        )
177    }
178
179    /// Session not found or expired
180    ///
181    /// Clients receiving this error should re-initialize the connection.
182    /// The session may have expired due to inactivity or server restart.
183    pub fn session_not_found() -> Self {
184        Self::mcp_error(
185            McpErrorCode::SessionNotFound,
186            "Session not found or expired. Please re-initialize the connection.",
187        )
188    }
189
190    /// Session not found with a specific session ID
191    pub fn session_not_found_with_id(session_id: &str) -> Self {
192        Self::mcp_error(
193            McpErrorCode::SessionNotFound,
194            format!(
195                "Session '{}' not found or expired. Please re-initialize the connection.",
196                session_id
197            ),
198        )
199    }
200
201    /// Session ID is required
202    pub fn session_required() -> Self {
203        Self::mcp_error(
204            McpErrorCode::SessionRequired,
205            "MCP-Session-Id header is required for this request.",
206        )
207    }
208
209    /// Access forbidden (insufficient scope or authorization)
210    pub fn forbidden(message: impl Into<String>) -> Self {
211        Self::mcp_error(McpErrorCode::Forbidden, message)
212    }
213}
214
215/// Tool execution error with context
216#[derive(Debug)]
217pub struct ToolError {
218    /// The tool name that failed
219    pub tool: Option<String>,
220    /// Error message
221    pub message: String,
222    /// Source error if any
223    pub source: Option<BoxError>,
224}
225
226impl std::fmt::Display for ToolError {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        if let Some(tool) = &self.tool {
229            write!(f, "Tool '{}' error: {}", tool, self.message)
230        } else {
231            write!(f, "Tool error: {}", self.message)
232        }
233    }
234}
235
236impl std::error::Error for ToolError {
237    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
238        self.source
239            .as_ref()
240            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
241    }
242}
243
244impl ToolError {
245    /// Create a new tool error with just a message
246    pub fn new(message: impl Into<String>) -> Self {
247        Self {
248            tool: None,
249            message: message.into(),
250            source: None,
251        }
252    }
253
254    /// Create a tool error with the tool name
255    pub fn with_tool(tool: impl Into<String>, message: impl Into<String>) -> Self {
256        Self {
257            tool: Some(tool.into()),
258            message: message.into(),
259            source: None,
260        }
261    }
262
263    /// Add a source error
264    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
265        self.source = Some(Box::new(source));
266        self
267    }
268}
269
270/// tower-mcp error type
271#[derive(Debug, thiserror::Error)]
272pub enum Error {
273    #[error("JSON-RPC error: {0:?}")]
274    JsonRpc(JsonRpcError),
275
276    #[error("Serialization error: {0}")]
277    Serialization(#[from] serde_json::Error),
278
279    /// A tool execution error.
280    ///
281    /// When returned from a tool handler, this variant is mapped to JSON-RPC
282    /// error code `-32603` (Internal Error) in the router's `Service::call`
283    /// implementation. The `ToolError` message becomes the JSON-RPC error message.
284    #[error("{0}")]
285    Tool(#[from] ToolError),
286
287    #[error("Transport error: {0}")]
288    Transport(String),
289
290    #[error("Internal error: {0}")]
291    Internal(String),
292}
293
294impl Error {
295    /// Create a simple tool error from a string (for backwards compatibility)
296    pub fn tool(message: impl Into<String>) -> Self {
297        Error::Tool(ToolError::new(message))
298    }
299
300    /// Create a tool error with the tool name
301    pub fn tool_with_name(tool: impl Into<String>, message: impl Into<String>) -> Self {
302        Error::Tool(ToolError::with_tool(tool, message))
303    }
304
305    /// Create a tool error from any `Display` type.
306    ///
307    /// This is useful for converting errors in a `map_err` chain:
308    ///
309    /// ```rust
310    /// # use tower_mcp::Error;
311    /// # fn example() -> Result<(), Error> {
312    /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("oops"));
313    /// result.map_err(Error::tool_from)?;
314    /// # Ok(())
315    /// # }
316    /// ```
317    pub fn tool_from<E: std::fmt::Display>(err: E) -> Self {
318        Error::Tool(ToolError::new(err.to_string()))
319    }
320
321    /// Create a tool error with context prefix.
322    ///
323    /// This is useful for adding context when converting errors:
324    ///
325    /// ```rust
326    /// # use tower_mcp::Error;
327    /// # fn example() -> Result<(), Error> {
328    /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("connection refused"));
329    /// result.map_err(|e| Error::tool_context("API request failed", e))?;
330    /// # Ok(())
331    /// # }
332    /// ```
333    pub fn tool_context<E: std::fmt::Display>(context: impl Into<String>, err: E) -> Self {
334        Error::Tool(ToolError::new(format!("{}: {}", context.into(), err)))
335    }
336
337    /// Create a JSON-RPC "Invalid params" error (`-32602`).
338    ///
339    /// Shorthand for `Error::JsonRpc(JsonRpcError::invalid_params(msg))`.
340    ///
341    /// ```rust
342    /// # use tower_mcp::Error;
343    /// let err = Error::invalid_params("missing required field 'name'");
344    /// ```
345    pub fn invalid_params(message: impl Into<String>) -> Self {
346        Error::JsonRpc(JsonRpcError::invalid_params(message))
347    }
348
349    /// Create a JSON-RPC "Internal error" error (`-32603`).
350    ///
351    /// Shorthand for `Error::JsonRpc(JsonRpcError::internal_error(msg))`.
352    ///
353    /// ```rust
354    /// # use tower_mcp::Error;
355    /// let err = Error::internal("unexpected state");
356    /// ```
357    pub fn internal(message: impl Into<String>) -> Self {
358        Error::JsonRpc(JsonRpcError::internal_error(message))
359    }
360}
361
362impl From<JsonRpcError> for Error {
363    fn from(err: JsonRpcError) -> Self {
364        Error::JsonRpc(err)
365    }
366}
367
368/// Result type alias for tower-mcp
369pub type Result<T> = std::result::Result<T, Error>;
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_box_error_from_io_error() {
377        let io_err = std::io::Error::other("disk full");
378        let boxed: BoxError = io_err.into();
379        assert_eq!(boxed.to_string(), "disk full");
380    }
381
382    #[test]
383    fn test_box_error_from_string() {
384        let err: BoxError = "something went wrong".into();
385        assert_eq!(err.to_string(), "something went wrong");
386    }
387
388    #[test]
389    fn test_box_error_is_send_sync() {
390        fn assert_send_sync<T: Send + Sync>() {}
391        assert_send_sync::<BoxError>();
392    }
393
394    #[test]
395    fn test_tool_error_source_uses_box_error() {
396        let io_err = std::io::Error::other("timeout");
397        let tool_err = ToolError::new("failed").with_source(io_err);
398        assert!(tool_err.source.is_some());
399        assert_eq!(tool_err.source.unwrap().to_string(), "timeout");
400    }
401}