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)]
44#[non_exhaustive]
45pub enum ErrorCode {
46 ParseError = -32700,
48 InvalidRequest = -32600,
50 MethodNotFound = -32601,
52 InvalidParams = -32602,
54 InternalError = -32603,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[repr(i32)]
61#[non_exhaustive]
62pub enum McpErrorCode {
63 ConnectionClosed = -32000,
65 RequestTimeout = -32001,
67 ResourceNotFound = -32002,
69 AlreadySubscribed = -32003,
71 NotSubscribed = -32004,
73 SessionNotFound = -32005,
75 SessionRequired = -32006,
77 Forbidden = -32007,
79 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#[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 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 pub fn connection_closed(message: impl Into<String>) -> Self {
152 Self::mcp_error(McpErrorCode::ConnectionClosed, message)
153 }
154
155 pub fn request_timeout(message: impl Into<String>) -> Self {
157 Self::mcp_error(McpErrorCode::RequestTimeout, message)
158 }
159
160 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 pub fn already_subscribed(uri: &str) -> Self {
170 Self::mcp_error(
171 McpErrorCode::AlreadySubscribed,
172 format!("Already subscribed to: {}", uri),
173 )
174 }
175
176 pub fn not_subscribed(uri: &str) -> Self {
178 Self::mcp_error(
179 McpErrorCode::NotSubscribed,
180 format!("Not subscribed to: {}", uri),
181 )
182 }
183
184 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 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 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 pub fn forbidden(message: impl Into<String>) -> Self {
216 Self::mcp_error(McpErrorCode::Forbidden, message)
217 }
218
219 pub fn url_elicitation_required(message: impl Into<String>) -> Self {
221 Self::mcp_error(McpErrorCode::UrlElicitationRequired, message)
222 }
223}
224
225#[derive(Debug)]
227pub struct ToolError {
228 pub tool: Option<String>,
230 pub message: String,
232 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 pub fn new(message: impl Into<String>) -> Self {
257 Self {
258 tool: None,
259 message: message.into(),
260 source: None,
261 }
262 }
263
264 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 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#[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 #[error("{0}")]
296 Tool(#[from] ToolError),
297
298 #[error("Transport error: {0}")]
299 Transport(String),
300
301 #[error("Session expired")]
307 SessionExpired,
308
309 #[error("Internal error: {0}")]
310 Internal(String),
311}
312
313impl Error {
314 pub fn tool(message: impl Into<String>) -> Self {
316 Error::Tool(ToolError::new(message))
317 }
318
319 pub fn tool_with_name(tool: impl Into<String>, message: impl Into<String>) -> Self {
321 Error::Tool(ToolError::with_tool(tool, message))
322 }
323
324 pub fn tool_from<E: std::fmt::Display>(err: E) -> Self {
337 Error::Tool(ToolError::new(err.to_string()))
338 }
339
340 pub fn tool_context<E: std::fmt::Display>(context: impl Into<String>, err: E) -> Self {
355 Error::Tool(ToolError::new(format!("{}: {}", context.into(), err)))
356 }
357
358 pub fn invalid_params(message: impl Into<String>) -> Self {
367 Error::JsonRpc(JsonRpcError::invalid_params(message))
368 }
369
370 pub fn internal(message: impl Into<String>) -> Self {
379 Error::JsonRpc(JsonRpcError::internal_error(message))
380 }
381}
382
383pub trait ResultExt<T> {
402 fn tool_err(self) -> std::result::Result<T, Error>;
413
414 fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error>;
425}
426
427impl<T, E: std::fmt::Display> ResultExt<T> for std::result::Result<T, E> {
428 fn tool_err(self) -> std::result::Result<T, Error> {
429 self.map_err(Error::tool_from)
430 }
431
432 fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error> {
433 self.map_err(|e| Error::tool_context(context, e))
434 }
435}
436
437impl From<JsonRpcError> for Error {
438 fn from(err: JsonRpcError) -> Self {
439 Error::JsonRpc(err)
440 }
441}
442
443impl From<std::convert::Infallible> for Error {
444 fn from(err: std::convert::Infallible) -> Self {
445 match err {}
446 }
447}
448
449pub type Result<T> = std::result::Result<T, Error>;
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 #[test]
457 fn test_box_error_from_io_error() {
458 let io_err = std::io::Error::other("disk full");
459 let boxed: BoxError = io_err.into();
460 assert_eq!(boxed.to_string(), "disk full");
461 }
462
463 #[test]
464 fn test_box_error_from_string() {
465 let err: BoxError = "something went wrong".into();
466 assert_eq!(err.to_string(), "something went wrong");
467 }
468
469 #[test]
470 fn test_box_error_is_send_sync() {
471 fn assert_send_sync<T: Send + Sync>() {}
472 assert_send_sync::<BoxError>();
473 }
474
475 #[test]
476 fn test_tool_error_source_uses_box_error() {
477 let io_err = std::io::Error::other("timeout");
478 let tool_err = ToolError::new("failed").with_source(io_err);
479 assert!(tool_err.source.is_some());
480 assert_eq!(tool_err.source.unwrap().to_string(), "timeout");
481 }
482
483 #[test]
484 fn test_result_ext_tool_err() {
485 let result: std::result::Result<(), std::io::Error> =
486 Err(std::io::Error::other("disk full"));
487 let err = result.tool_err().unwrap_err();
488 assert!(matches!(err, Error::Tool(_)));
489 assert!(err.to_string().contains("disk full"));
490 }
491
492 #[test]
493 fn test_result_ext_tool_context() {
494 let result: std::result::Result<(), std::io::Error> =
495 Err(std::io::Error::other("connection refused"));
496 let err = result.tool_context("database query failed").unwrap_err();
497 assert!(matches!(err, Error::Tool(_)));
498 assert!(err.to_string().contains("database query failed"));
499 assert!(err.to_string().contains("connection refused"));
500 }
501
502 #[test]
503 fn test_result_ext_ok_passes_through() {
504 let result: std::result::Result<i32, std::io::Error> = Ok(42);
505 assert_eq!(result.tool_err().unwrap(), 42);
506 let result: std::result::Result<i32, std::io::Error> = Ok(42);
507 assert_eq!(result.tool_context("should not appear").unwrap(), 42);
508 }
509}