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
30use serde::{Deserialize, Serialize};
31
32/// Standard JSON-RPC error codes
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(i32)]
35pub enum ErrorCode {
36    /// Invalid JSON was received
37    ParseError = -32700,
38    /// The JSON sent is not a valid Request object
39    InvalidRequest = -32600,
40    /// The method does not exist / is not available
41    MethodNotFound = -32601,
42    /// Invalid method parameter(s)
43    InvalidParams = -32602,
44    /// Internal JSON-RPC error
45    InternalError = -32603,
46}
47
48/// MCP-specific error codes (in the -32000 to -32099 range)
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[repr(i32)]
51pub enum McpErrorCode {
52    /// Transport connection was closed
53    ConnectionClosed = -32000,
54    /// Request exceeded timeout
55    RequestTimeout = -32001,
56    /// Resource not found
57    ResourceNotFound = -32002,
58    /// Resource already subscribed
59    AlreadySubscribed = -32003,
60    /// Resource not subscribed (for unsubscribe)
61    NotSubscribed = -32004,
62    /// Session not found or expired - client should re-initialize
63    SessionNotFound = -32005,
64    /// Session ID is required but was not provided
65    SessionRequired = -32006,
66}
67
68impl McpErrorCode {
69    pub fn code(self) -> i32 {
70        self as i32
71    }
72}
73
74impl ErrorCode {
75    pub fn code(self) -> i32 {
76        self as i32
77    }
78}
79
80/// JSON-RPC error object
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct JsonRpcError {
83    pub code: i32,
84    pub message: String,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub data: Option<serde_json::Value>,
87}
88
89impl JsonRpcError {
90    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
91        Self {
92            code: code.code(),
93            message: message.into(),
94            data: None,
95        }
96    }
97
98    pub fn with_data(mut self, data: serde_json::Value) -> Self {
99        self.data = Some(data);
100        self
101    }
102
103    pub fn parse_error(message: impl Into<String>) -> Self {
104        Self::new(ErrorCode::ParseError, message)
105    }
106
107    pub fn invalid_request(message: impl Into<String>) -> Self {
108        Self::new(ErrorCode::InvalidRequest, message)
109    }
110
111    pub fn method_not_found(method: &str) -> Self {
112        Self::new(
113            ErrorCode::MethodNotFound,
114            format!("Method not found: {}", method),
115        )
116    }
117
118    pub fn invalid_params(message: impl Into<String>) -> Self {
119        Self::new(ErrorCode::InvalidParams, message)
120    }
121
122    pub fn internal_error(message: impl Into<String>) -> Self {
123        Self::new(ErrorCode::InternalError, message)
124    }
125
126    /// Create an MCP-specific error
127    pub fn mcp_error(code: McpErrorCode, message: impl Into<String>) -> Self {
128        Self {
129            code: code.code(),
130            message: message.into(),
131            data: None,
132        }
133    }
134
135    /// Connection was closed
136    pub fn connection_closed(message: impl Into<String>) -> Self {
137        Self::mcp_error(McpErrorCode::ConnectionClosed, message)
138    }
139
140    /// Request timed out
141    pub fn request_timeout(message: impl Into<String>) -> Self {
142        Self::mcp_error(McpErrorCode::RequestTimeout, message)
143    }
144
145    /// Resource not found
146    pub fn resource_not_found(uri: &str) -> Self {
147        Self::mcp_error(
148            McpErrorCode::ResourceNotFound,
149            format!("Resource not found: {}", uri),
150        )
151    }
152
153    /// Resource already subscribed
154    pub fn already_subscribed(uri: &str) -> Self {
155        Self::mcp_error(
156            McpErrorCode::AlreadySubscribed,
157            format!("Already subscribed to: {}", uri),
158        )
159    }
160
161    /// Resource not subscribed
162    pub fn not_subscribed(uri: &str) -> Self {
163        Self::mcp_error(
164            McpErrorCode::NotSubscribed,
165            format!("Not subscribed to: {}", uri),
166        )
167    }
168
169    /// Session not found or expired
170    ///
171    /// Clients receiving this error should re-initialize the connection.
172    /// The session may have expired due to inactivity or server restart.
173    pub fn session_not_found() -> Self {
174        Self::mcp_error(
175            McpErrorCode::SessionNotFound,
176            "Session not found or expired. Please re-initialize the connection.",
177        )
178    }
179
180    /// Session not found with a specific session ID
181    pub fn session_not_found_with_id(session_id: &str) -> Self {
182        Self::mcp_error(
183            McpErrorCode::SessionNotFound,
184            format!(
185                "Session '{}' not found or expired. Please re-initialize the connection.",
186                session_id
187            ),
188        )
189    }
190
191    /// Session ID is required
192    pub fn session_required() -> Self {
193        Self::mcp_error(
194            McpErrorCode::SessionRequired,
195            "MCP-Session-Id header is required for this request.",
196        )
197    }
198}
199
200/// Tool execution error with context
201#[derive(Debug)]
202pub struct ToolError {
203    /// The tool name that failed
204    pub tool: Option<String>,
205    /// Error message
206    pub message: String,
207    /// Source error if any
208    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
209}
210
211impl std::fmt::Display for ToolError {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        if let Some(tool) = &self.tool {
214            write!(f, "Tool '{}' error: {}", tool, self.message)
215        } else {
216            write!(f, "Tool error: {}", self.message)
217        }
218    }
219}
220
221impl std::error::Error for ToolError {
222    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
223        self.source
224            .as_ref()
225            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
226    }
227}
228
229impl ToolError {
230    /// Create a new tool error with just a message
231    pub fn new(message: impl Into<String>) -> Self {
232        Self {
233            tool: None,
234            message: message.into(),
235            source: None,
236        }
237    }
238
239    /// Create a tool error with the tool name
240    pub fn with_tool(tool: impl Into<String>, message: impl Into<String>) -> Self {
241        Self {
242            tool: Some(tool.into()),
243            message: message.into(),
244            source: None,
245        }
246    }
247
248    /// Add a source error
249    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
250        self.source = Some(Box::new(source));
251        self
252    }
253}
254
255/// tower-mcp error type
256#[derive(Debug, thiserror::Error)]
257pub enum Error {
258    #[error("JSON-RPC error: {0:?}")]
259    JsonRpc(JsonRpcError),
260
261    #[error("Serialization error: {0}")]
262    Serialization(#[from] serde_json::Error),
263
264    #[error("{0}")]
265    Tool(#[from] ToolError),
266
267    #[error("Transport error: {0}")]
268    Transport(String),
269
270    #[error("Internal error: {0}")]
271    Internal(String),
272}
273
274impl Error {
275    /// Create a simple tool error from a string (for backwards compatibility)
276    pub fn tool(message: impl Into<String>) -> Self {
277        Error::Tool(ToolError::new(message))
278    }
279
280    /// Create a tool error with the tool name
281    pub fn tool_with_name(tool: impl Into<String>, message: impl Into<String>) -> Self {
282        Error::Tool(ToolError::with_tool(tool, message))
283    }
284
285    /// Create a tool error from any `Display` type.
286    ///
287    /// This is useful for converting errors in a `map_err` chain:
288    ///
289    /// ```rust
290    /// # use tower_mcp::Error;
291    /// # fn example() -> Result<(), Error> {
292    /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("oops"));
293    /// result.map_err(Error::tool_from)?;
294    /// # Ok(())
295    /// # }
296    /// ```
297    pub fn tool_from<E: std::fmt::Display>(err: E) -> Self {
298        Error::Tool(ToolError::new(err.to_string()))
299    }
300
301    /// Create a tool error with context prefix.
302    ///
303    /// This is useful for adding context when converting errors:
304    ///
305    /// ```rust
306    /// # use tower_mcp::Error;
307    /// # fn example() -> Result<(), Error> {
308    /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("connection refused"));
309    /// result.map_err(|e| Error::tool_context("API request failed", e))?;
310    /// # Ok(())
311    /// # }
312    /// ```
313    pub fn tool_context<E: std::fmt::Display>(context: impl Into<String>, err: E) -> Self {
314        Error::Tool(ToolError::new(format!("{}: {}", context.into(), err)))
315    }
316}
317
318impl From<JsonRpcError> for Error {
319    fn from(err: JsonRpcError) -> Self {
320        Error::JsonRpc(err)
321    }
322}
323
324/// Result type alias for tower-mcp
325pub type Result<T> = std::result::Result<T, Error>;