1use serde::{Deserialize, Serialize};
33
34pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[repr(i32)]
44pub enum ErrorCode {
45 ParseError = -32700,
47 InvalidRequest = -32600,
49 MethodNotFound = -32601,
51 InvalidParams = -32602,
53 InternalError = -32603,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[repr(i32)]
60pub enum McpErrorCode {
61 ConnectionClosed = -32000,
63 RequestTimeout = -32001,
65 ResourceNotFound = -32002,
67 AlreadySubscribed = -32003,
69 NotSubscribed = -32004,
71 SessionNotFound = -32005,
73 SessionRequired = -32006,
75 Forbidden = -32007,
77 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#[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 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 pub fn connection_closed(message: impl Into<String>) -> Self {
150 Self::mcp_error(McpErrorCode::ConnectionClosed, message)
151 }
152
153 pub fn request_timeout(message: impl Into<String>) -> Self {
155 Self::mcp_error(McpErrorCode::RequestTimeout, message)
156 }
157
158 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 pub fn already_subscribed(uri: &str) -> Self {
168 Self::mcp_error(
169 McpErrorCode::AlreadySubscribed,
170 format!("Already subscribed to: {}", uri),
171 )
172 }
173
174 pub fn not_subscribed(uri: &str) -> Self {
176 Self::mcp_error(
177 McpErrorCode::NotSubscribed,
178 format!("Not subscribed to: {}", uri),
179 )
180 }
181
182 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 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 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 pub fn forbidden(message: impl Into<String>) -> Self {
214 Self::mcp_error(McpErrorCode::Forbidden, message)
215 }
216
217 pub fn url_elicitation_required(message: impl Into<String>) -> Self {
219 Self::mcp_error(McpErrorCode::UrlElicitationRequired, message)
220 }
221}
222
223#[derive(Debug)]
225pub struct ToolError {
226 pub tool: Option<String>,
228 pub message: String,
230 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 pub fn new(message: impl Into<String>) -> Self {
255 Self {
256 tool: None,
257 message: message.into(),
258 source: None,
259 }
260 }
261
262 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 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#[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 #[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 pub fn tool(message: impl Into<String>) -> Self {
305 Error::Tool(ToolError::new(message))
306 }
307
308 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 pub fn tool_from<E: std::fmt::Display>(err: E) -> Self {
326 Error::Tool(ToolError::new(err.to_string()))
327 }
328
329 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 pub fn invalid_params(message: impl Into<String>) -> Self {
356 Error::JsonRpc(JsonRpcError::invalid_params(message))
357 }
358
359 pub fn internal(message: impl Into<String>) -> Self {
368 Error::JsonRpc(JsonRpcError::internal_error(message))
369 }
370}
371
372pub trait ResultExt<T> {
391 fn tool_err(self) -> std::result::Result<T, Error>;
402
403 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
438pub 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}