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
19//! errors. Codes assigned by the spec are marked **spec**; codes used only by
20//! this implementation (in the JSON-RPC "implementation-defined" subrange) are
21//! marked *impl*.
22//!
23//! | Code | Name | Source | Meaning |
24//! |--------|-------------------------------|--------|--------------------------------------|
25//! | -32000 | ConnectionClosed | *impl* | Transport connection was closed |
26//! | -32002 | ResourceNotFound | *deprecated* | **SEP-2164** reassigned to -32602; variant kept for backcompat |
27//! | -32005 | SessionNotFound | *impl* | Session not found or expired (legacy; deprecated by SEP-2567) |
28//! | -32006 | SessionRequired | *impl* | Mcp-Session-Id header required (legacy; deprecated by SEP-2567) |
29//! | -32007 | Forbidden | *impl* | Access forbidden (insufficient scope)|
30//! | -32008 | AlreadySubscribed | *impl* | Resource already subscribed |
31//! | -32009 | NotSubscribed | *impl* | Resource not subscribed |
32//! | -32010 | RequestTimeout | *impl* | Request exceeded timeout |
33//! | -32020 | HeaderMismatch | **spec** (SEP-2243) | HTTP headers do not match the request body, or required headers are missing/malformed |
34//! | -32021 | MissingRequiredClientCapability | **spec** (SEP-2575) | Client lacks a capability required by the request |
35//! | -32022 | UnsupportedProtocolVersion | **spec** (SEP-2575) | Server does not support the request's protocol version |
36//! | -32042 | UrlElicitationRequired | TS SDK | URL elicitation required |
37//!
38//! The three **spec** codes were renumbered upstream by the error-code
39//! allocation policy (spec PR modelcontextprotocol#2907, 2026-06): the SEP
40//! documents originally assigned -32001 (HeaderMismatch), -32003
41//! (MissingRequiredClientCapability), and -32004 (UnsupportedProtocolVersion);
42//! the draft schema now assigns -32020/-32021/-32022 and is canonical. Verify
43//! against the published `schema/2026-07-28` at finalization.
44
45use serde::{Deserialize, Serialize};
46
47use crate::protocol::ClientCapabilities;
48
49/// Type-erased error type used for middleware composition.
50///
51/// This is the standard error type in the tower ecosystem, used by
52/// [`tower`](https://docs.rs/tower), [`tower-http`](https://docs.rs/tower-http),
53/// and other tower-compatible crates.
54pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
55
56/// Standard JSON-RPC error codes
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58#[repr(i32)]
59#[non_exhaustive]
60pub enum ErrorCode {
61 /// Invalid JSON was received
62 ParseError = -32700,
63 /// The JSON sent is not a valid Request object
64 InvalidRequest = -32600,
65 /// The method does not exist / is not available
66 MethodNotFound = -32601,
67 /// Invalid method parameter(s)
68 InvalidParams = -32602,
69 /// Internal JSON-RPC error
70 InternalError = -32603,
71}
72
73/// MCP-specific error codes (in the -32000 to -32099 range).
74///
75/// Codes marked **spec** are assigned by an MCP SEP. Others are
76/// implementation-defined within the JSON-RPC server-error range; using them
77/// is permitted by JSON-RPC 2.0 but they are not part of the MCP wire spec.
78///
79/// Recently-changed assignments:
80/// - `HeaderMismatch` (SEP-2243), `MissingRequiredClientCapability`, and
81/// `UnsupportedProtocolVersion` (both SEP-2575) were originally assigned
82/// -32001/-32003/-32004 by their SEP documents. The upstream error-code
83/// allocation policy (spec PR modelcontextprotocol#2907, 2026-06)
84/// renumbered them to -32020/-32021/-32022 in the draft schema, which is
85/// canonical. **This is a breaking change on the wire** within the
86/// experimental 2026-07-28 surface.
87/// - `AlreadySubscribed` and `NotSubscribed` previously occupied -32003 and
88/// -32004; they moved to -32008 and -32009 when those slots were believed
89/// to be spec-assigned. They stay at -32008/-32009.
90/// - `RequestTimeout` previously occupied -32001 and moved to -32010 for the
91/// same reason. It stays at -32010.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[repr(i32)]
94#[non_exhaustive]
95pub enum McpErrorCode {
96 /// Transport connection was closed.
97 ConnectionClosed = -32000,
98 /// SEP-2243: HTTP headers (e.g. `Mcp-Method`, `Mcp-Name`,
99 /// `Mcp-Param-*`) do not match the corresponding values in the request
100 /// body, or a required header is missing or malformed.
101 ///
102 /// Originally -32001 in the SEP text; renumbered to -32020 by the
103 /// upstream error-code allocation (spec PR modelcontextprotocol#2907).
104 ///
105 /// Use [`JsonRpcError::header_mismatch`] to construct.
106 HeaderMismatch = -32020,
107 /// Resource not found.
108 ///
109 /// **Deprecated**: SEP-2164 (FINAL) moves this to the standard JSON-RPC
110 /// `InvalidParams` code (-32602). The
111 /// [`JsonRpcError::resource_not_found`] constructor now emits -32602.
112 /// The enum variant is retained so existing pattern matches on
113 /// `McpErrorCode::ResourceNotFound` keep compiling, but the variant's
114 /// numeric value is no longer what the constructor produces.
115 #[deprecated(
116 since = "0.12.0",
117 note = "SEP-2164 reassigned resource-not-found to InvalidParams (-32602). \
118 Use JsonRpcError::resource_not_found or ErrorCode::InvalidParams."
119 )]
120 ResourceNotFound = -32002,
121 /// SEP-2575: client capabilities advertised on the request do not
122 /// include a capability required by the called method.
123 ///
124 /// Originally -32003 in the SEP text; renumbered to -32021 by the
125 /// upstream error-code allocation (spec PR modelcontextprotocol#2907).
126 MissingRequiredClientCapability = -32021,
127 /// SEP-2575: server does not support the protocol version the client
128 /// requested (via `MCP-Protocol-Version` header or per-request `_meta`).
129 ///
130 /// Originally -32004 in the SEP text; renumbered to -32022 by the
131 /// upstream error-code allocation (spec PR modelcontextprotocol#2907).
132 ///
133 /// Use [`JsonRpcError::unsupported_protocol_version`] to construct.
134 UnsupportedProtocolVersion = -32022,
135 /// Session not found or expired -- client should re-initialize.
136 ///
137 /// SEP-2567 deprecates sessions entirely. This code stays for the
138 /// 2025-11-25 protocol path; sessionless deployments will never emit it.
139 SessionNotFound = -32005,
140 /// Session ID is required but was not provided.
141 ///
142 /// SEP-2567 deprecates sessions entirely. Same caveat as `SessionNotFound`.
143 SessionRequired = -32006,
144 /// Access forbidden (insufficient scope or authorization).
145 Forbidden = -32007,
146 /// Resource already subscribed. Moved from -32003 when that slot was
147 /// believed to be spec-assigned; stays at -32008.
148 AlreadySubscribed = -32008,
149 /// Resource not subscribed (for unsubscribe). Moved from -32004 when
150 /// that slot was believed to be spec-assigned; stays at -32009.
151 NotSubscribed = -32009,
152 /// Request exceeded timeout. Moved from -32001 when that slot was
153 /// believed to be spec-assigned; stays at -32010.
154 RequestTimeout = -32010,
155 /// URL elicitation is required before processing the request.
156 UrlElicitationRequired = -32042,
157}
158
159impl McpErrorCode {
160 /// Return this MCP-specific error code as its JSON-RPC integer value.
161 pub fn code(self) -> i32 {
162 self as i32
163 }
164}
165
166impl ErrorCode {
167 /// Return this standard JSON-RPC error code as its integer value.
168 pub fn code(self) -> i32 {
169 self as i32
170 }
171}
172
173/// JSON-RPC error object
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct JsonRpcError {
176 /// Numeric JSON-RPC error code.
177 pub code: i32,
178 /// Human-readable summary of the error.
179 pub message: String,
180 /// Optional structured details supplied by the error producer.
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub data: Option<serde_json::Value>,
183}
184
185impl std::fmt::Display for JsonRpcError {
186 /// Render for a human.
187 ///
188 /// This is the most common failure a client has to show, since it is
189 /// every rejection a server returns. `Debug` still gives the full struct
190 /// for a log or a panic; this gives the two parts a person reads.
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 write!(f, "{} (code {})", self.message, self.code)?;
193 // Structured detail is worth showing when a producer supplied it, and
194 // worth omitting entirely when it did not: `data: None` tells a reader
195 // nothing.
196 if let Some(data) = &self.data {
197 write!(f, ": {data}")?;
198 }
199 Ok(())
200 }
201}
202
203impl JsonRpcError {
204 /// Create an error with a standard JSON-RPC code and no structured data.
205 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
206 Self {
207 code: code.code(),
208 message: message.into(),
209 data: None,
210 }
211 }
212
213 /// Attach structured application-specific data to this error.
214 pub fn with_data(mut self, data: serde_json::Value) -> Self {
215 self.data = Some(data);
216 self
217 }
218
219 /// Create a JSON-RPC parse error (`-32700`).
220 pub fn parse_error(message: impl Into<String>) -> Self {
221 Self::new(ErrorCode::ParseError, message)
222 }
223
224 /// Create a JSON-RPC invalid-request error (`-32600`).
225 pub fn invalid_request(message: impl Into<String>) -> Self {
226 Self::new(ErrorCode::InvalidRequest, message)
227 }
228
229 /// Create a JSON-RPC method-not-found error (`-32601`) for `method`.
230 pub fn method_not_found(method: &str) -> Self {
231 Self::new(
232 ErrorCode::MethodNotFound,
233 format!("Method not found: {}", method),
234 )
235 }
236
237 /// Create a JSON-RPC invalid-parameters error (`-32602`).
238 pub fn invalid_params(message: impl Into<String>) -> Self {
239 Self::new(ErrorCode::InvalidParams, message)
240 }
241
242 /// Create a JSON-RPC internal error (`-32603`).
243 pub fn internal_error(message: impl Into<String>) -> Self {
244 Self::new(ErrorCode::InternalError, message)
245 }
246
247 /// Create an MCP-specific error
248 pub fn mcp_error(code: McpErrorCode, message: impl Into<String>) -> Self {
249 Self {
250 code: code.code(),
251 message: message.into(),
252 data: None,
253 }
254 }
255
256 /// Connection was closed
257 pub fn connection_closed(message: impl Into<String>) -> Self {
258 Self::mcp_error(McpErrorCode::ConnectionClosed, message)
259 }
260
261 /// Request timed out
262 pub fn request_timeout(message: impl Into<String>) -> Self {
263 Self::mcp_error(McpErrorCode::RequestTimeout, message)
264 }
265
266 /// Resource not found. Per SEP-2164 (FINAL) this now uses the
267 /// standard JSON-RPC `InvalidParams` code (-32602) rather than the
268 /// legacy MCP-specific [`McpErrorCode::ResourceNotFound`] (-32002).
269 /// The resource URI is a parameter, so missing-parameter and
270 /// unknown-resource-URI are the same error class.
271 pub fn resource_not_found(uri: &str) -> Self {
272 // SEP-2164: the error data SHOULD carry the requested URI so clients
273 // can programmatically identify which resource was missing.
274 Self::new(
275 ErrorCode::InvalidParams,
276 format!("Resource not found: {}", uri),
277 )
278 .with_data(serde_json::json!({ "uri": uri }))
279 }
280
281 /// Resource already subscribed
282 pub fn already_subscribed(uri: &str) -> Self {
283 Self::mcp_error(
284 McpErrorCode::AlreadySubscribed,
285 format!("Already subscribed to: {}", uri),
286 )
287 }
288
289 /// Resource not subscribed
290 pub fn not_subscribed(uri: &str) -> Self {
291 Self::mcp_error(
292 McpErrorCode::NotSubscribed,
293 format!("Not subscribed to: {}", uri),
294 )
295 }
296
297 /// Session not found or expired
298 ///
299 /// Clients receiving this error should re-initialize the connection.
300 /// The session may have expired due to inactivity or server restart.
301 pub fn session_not_found() -> Self {
302 Self::mcp_error(
303 McpErrorCode::SessionNotFound,
304 "Session not found or expired. Please re-initialize the connection.",
305 )
306 }
307
308 /// Session not found with a specific session ID
309 pub fn session_not_found_with_id(session_id: &str) -> Self {
310 Self::mcp_error(
311 McpErrorCode::SessionNotFound,
312 format!(
313 "Session '{}' not found or expired. Please re-initialize the connection.",
314 session_id
315 ),
316 )
317 }
318
319 /// Session ID is required
320 pub fn session_required() -> Self {
321 Self::mcp_error(
322 McpErrorCode::SessionRequired,
323 "MCP-Session-Id header is required for this request.",
324 )
325 }
326
327 /// Access forbidden (insufficient scope or authorization)
328 pub fn forbidden(message: impl Into<String>) -> Self {
329 Self::mcp_error(McpErrorCode::Forbidden, message)
330 }
331
332 /// URL elicitation is required before processing the request
333 pub fn url_elicitation_required(message: impl Into<String>) -> Self {
334 Self::mcp_error(McpErrorCode::UrlElicitationRequired, message)
335 }
336
337 /// SEP-2243: HTTP headers do not match the request body, or a required
338 /// header is missing or malformed.
339 ///
340 /// Servers using the Streamable HTTP transport return this error code
341 /// (with HTTP status `400 Bad Request`) when any of the following hold:
342 ///
343 /// - A required standard header (`Mcp-Method`, `Mcp-Name`) is missing
344 /// for the corresponding method.
345 /// - A header value does not match the request body value.
346 /// - A `Mcp-Param-{Name}` Base64-encoded value cannot be decoded.
347 /// - A header value contains invalid characters.
348 pub fn header_mismatch(message: impl Into<String>) -> Self {
349 Self::mcp_error(McpErrorCode::HeaderMismatch, message)
350 }
351
352 /// SEP-2575: the client did not advertise capabilities required to
353 /// service the request.
354 ///
355 /// The error data identifies the missing capability shape:
356 ///
357 /// ```text
358 /// data: { requiredCapabilities: { ... } }
359 /// ```
360 pub fn missing_required_client_capability(required_capabilities: ClientCapabilities) -> Self {
361 let data = MissingRequiredClientCapabilityData {
362 required_capabilities,
363 };
364 Self {
365 code: McpErrorCode::MissingRequiredClientCapability.code(),
366 message: "Client is missing a capability required by this request".to_string(),
367 data: Some(
368 serde_json::to_value(&data)
369 .expect("MissingRequiredClientCapabilityData is serializable"),
370 ),
371 }
372 }
373
374 /// SEP-2575: server does not support the protocol version the client
375 /// requested. The error data carries both the AS-supported versions
376 /// and the version the client asked for, matching the spec shape:
377 ///
378 /// ```text
379 /// data: { supported: [...], requested: "..." }
380 /// ```
381 pub fn unsupported_protocol_version(
382 requested: impl Into<String>,
383 supported: impl IntoIterator<Item = impl Into<String>>,
384 ) -> Self {
385 let data = UnsupportedProtocolVersionData {
386 supported: supported.into_iter().map(Into::into).collect(),
387 requested: requested.into(),
388 };
389 Self {
390 code: McpErrorCode::UnsupportedProtocolVersion.code(),
391 message: format!(
392 "Unsupported protocol version: {} (supported: {})",
393 data.requested,
394 data.supported.join(", "),
395 ),
396 data: Some(
397 serde_json::to_value(&data)
398 .expect("UnsupportedProtocolVersionData is serializable"),
399 ),
400 }
401 }
402}
403
404/// SEP-2575 error data for `MissingRequiredClientCapability` (-32021).
405#[derive(Debug, Clone, Serialize, Deserialize)]
406#[serde(rename_all = "camelCase")]
407pub struct MissingRequiredClientCapabilityData {
408 /// The client capability shape required to service the request.
409 pub required_capabilities: ClientCapabilities,
410}
411
412/// SEP-2575 error data for `UnsupportedProtocolVersion` (-32022).
413///
414/// Wire shape per the draft schema:
415/// ```json
416/// {
417/// "supported": ["2026-07-28", "2025-11-25"],
418/// "requested": "2027-01-01"
419/// }
420/// ```
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct UnsupportedProtocolVersionData {
423 /// Protocol versions the server supports. The client should pick a
424 /// mutually-supported version from this list and retry.
425 pub supported: Vec<String>,
426 /// The protocol version that was requested by the client.
427 pub requested: String,
428}
429
430/// Tool execution error with context
431#[derive(Debug)]
432pub struct ToolError {
433 /// The tool name that failed
434 pub tool: Option<String>,
435 /// Error message
436 pub message: String,
437 /// Source error if any
438 pub source: Option<BoxError>,
439}
440
441impl std::fmt::Display for ToolError {
442 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
443 if let Some(tool) = &self.tool {
444 write!(f, "Tool '{}' error: {}", tool, self.message)
445 } else {
446 write!(f, "Tool error: {}", self.message)
447 }
448 }
449}
450
451impl std::error::Error for ToolError {
452 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
453 self.source
454 .as_ref()
455 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
456 }
457}
458
459impl ToolError {
460 /// Create a new tool error with just a message
461 pub fn new(message: impl Into<String>) -> Self {
462 Self {
463 tool: None,
464 message: message.into(),
465 source: None,
466 }
467 }
468
469 /// Create a tool error with the tool name
470 pub fn with_tool(tool: impl Into<String>, message: impl Into<String>) -> Self {
471 Self {
472 tool: Some(tool.into()),
473 message: message.into(),
474 source: None,
475 }
476 }
477
478 /// Add a source error
479 pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
480 self.source = Some(Box::new(source));
481 self
482 }
483}
484
485/// tower-mcp error type
486#[derive(Debug, thiserror::Error)]
487#[non_exhaustive]
488pub enum Error {
489 /// A protocol-level JSON-RPC error returned by a peer or handler.
490 #[error("JSON-RPC error: {0}")]
491 JsonRpc(JsonRpcError),
492
493 /// JSON encoding or decoding failed.
494 #[error("Serialization error: {0}")]
495 Serialization(#[from] serde_json::Error),
496
497 /// A tool execution error.
498 ///
499 /// When returned from a tool handler, this variant is mapped to JSON-RPC
500 /// error code `-32603` (Internal Error) in the router's `Service::call`
501 /// implementation. The `ToolError` message becomes the JSON-RPC error message.
502 #[error("{0}")]
503 Tool(#[from] ToolError),
504
505 /// The underlying transport failed or returned an unusable response.
506 #[error("Transport error: {0}")]
507 Transport(String),
508
509 /// A live task was cancelled while its handler was waiting.
510 ///
511 /// Returned by the awaiting methods on `TaskContext` so a handler that
512 /// propagates with `?` unwinds correctly without writing a `select!`.
513 /// The router maps it to a cancelled task rather than a failed one.
514 #[error("Task cancelled")]
515 TaskCancelled,
516
517 /// The server indicated the session has expired or is not found.
518 ///
519 /// This corresponds to JSON-RPC error code `-32005` (SessionNotFound)
520 /// or an HTTP 404 response when a session ID was attached.
521 /// Clients should re-initialize the connection.
522 #[error("Session expired")]
523 SessionExpired,
524
525 /// A single SSE event exceeded the configured maximum size.
526 ///
527 /// Raised by SSE-consuming client transports when a server streams an
528 /// event larger than the configured cap. The stream is terminated
529 /// rather than buffering without bound. `size` is the buffered size at
530 /// the point the limit was detected; `limit` is the configured cap.
531 #[error("SSE event too large: {size} bytes buffered, limit is {limit} bytes")]
532 SseEventTooLarge {
533 /// Bytes buffered for the event when the limit was exceeded.
534 size: usize,
535 /// The configured maximum event size in bytes.
536 limit: usize,
537 },
538
539 /// A single newline-delimited frame exceeded the configured maximum size.
540 ///
541 /// Raised by frame-reading transports (stdio, child process) when a peer
542 /// writes more bytes than the configured cap without a `\n` delimiter.
543 /// Reading stops and the buffer is discarded rather than growing without
544 /// bound. `size` is the buffered size at the point the limit was
545 /// detected; `limit` is the configured cap. Analogous to
546 /// [`Error::SseEventTooLarge`] for the HTTP/SSE client.
547 #[error("frame too large: {size} bytes buffered, limit is {limit} bytes")]
548 FrameTooLarge {
549 /// Bytes buffered for the frame when the limit was exceeded.
550 size: usize,
551 /// The configured maximum frame size in bytes.
552 limit: usize,
553 },
554
555 /// An internal library or server failure not represented by another variant.
556 #[error("Internal error: {0}")]
557 Internal(String),
558}
559
560impl Error {
561 /// Create a simple tool error from a string (for backwards compatibility)
562 pub fn tool(message: impl Into<String>) -> Self {
563 Error::Tool(ToolError::new(message))
564 }
565
566 /// Create a tool error with the tool name
567 pub fn tool_with_name(tool: impl Into<String>, message: impl Into<String>) -> Self {
568 Error::Tool(ToolError::with_tool(tool, message))
569 }
570
571 /// Create a tool error from any `Display` type.
572 ///
573 /// This is useful for converting errors in a `map_err` chain:
574 ///
575 /// ```rust
576 /// # use tower_mcp_types::Error;
577 /// # fn example() -> Result<(), Error> {
578 /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("oops"));
579 /// result.map_err(Error::tool_from)?;
580 /// # Ok(())
581 /// # }
582 /// ```
583 pub fn tool_from<E: std::fmt::Display>(err: E) -> Self {
584 Error::Tool(ToolError::new(err.to_string()))
585 }
586
587 /// Create a tool error with context prefix.
588 ///
589 /// This is useful for adding context when converting errors.
590 /// For a more ergonomic API, see [`ResultExt::tool_context`] which can be
591 /// called directly on `Result` values:
592 ///
593 /// ```rust
594 /// # use tower_mcp_types::error::ResultExt;
595 /// # fn example() -> tower_mcp_types::Result<()> {
596 /// let result: Result<(), std::io::Error> = Err(std::io::Error::other("connection refused"));
597 /// result.tool_context("API request failed")?;
598 /// # Ok(())
599 /// # }
600 /// ```
601 pub fn tool_context<E: std::fmt::Display>(context: impl Into<String>, err: E) -> Self {
602 Error::Tool(ToolError::new(format!("{}: {}", context.into(), err)))
603 }
604
605 /// Create a JSON-RPC "Invalid params" error (`-32602`).
606 ///
607 /// Shorthand for `Error::JsonRpc(JsonRpcError::invalid_params(msg))`.
608 ///
609 /// ```rust
610 /// # use tower_mcp_types::Error;
611 /// let err = Error::invalid_params("missing required field 'name'");
612 /// ```
613 pub fn invalid_params(message: impl Into<String>) -> Self {
614 Error::JsonRpc(JsonRpcError::invalid_params(message))
615 }
616
617 /// Create a JSON-RPC "Internal error" error (`-32603`).
618 ///
619 /// Shorthand for `Error::JsonRpc(JsonRpcError::internal_error(msg))`.
620 ///
621 /// ```rust
622 /// # use tower_mcp_types::Error;
623 /// let err = Error::internal("unexpected state");
624 /// ```
625 pub fn internal(message: impl Into<String>) -> Self {
626 Error::JsonRpc(JsonRpcError::internal_error(message))
627 }
628
629 /// Convert into the JSON-RPC error a client will see for this failure.
630 ///
631 /// A structured [`Error::JsonRpc`] is returned as-is, preserving its
632 /// code and message. Every other variant is sanitized to `-32603`
633 /// (Internal Error) using the error's `Display` text, the same policy
634 /// `McpRouter`'s top-level dispatch and `transport::service::CatchError`
635 /// already apply. Callers that sit in front of a `Service` boundary
636 /// (a resource or prompt handler wrapper, for instance) use this to
637 /// convert their own error before it ever reaches a Tower layer, so a
638 /// structured error rides through middleware untouched and only a
639 /// genuine middleware failure needs sanitizing.
640 ///
641 /// ```rust
642 /// # use tower_mcp_types::Error;
643 /// # use tower_mcp_types::error::ErrorCode;
644 /// let structured = Error::invalid_params("bad shape").into_json_rpc_error();
645 /// assert_eq!(structured.code, ErrorCode::InvalidParams.code());
646 ///
647 /// let opaque = Error::Internal("db pool exhausted".to_string()).into_json_rpc_error();
648 /// assert_eq!(opaque.code, ErrorCode::InternalError.code());
649 /// assert!(opaque.message.contains("db pool exhausted"));
650 /// ```
651 pub fn into_json_rpc_error(self) -> JsonRpcError {
652 match self {
653 Error::JsonRpc(err) => err,
654 other => JsonRpcError::internal_error(other.to_string()),
655 }
656 }
657}
658
659/// Extension trait for converting errors into tower-mcp tool errors.
660///
661/// Provides ergonomic error conversion methods on `Result` types,
662/// similar to `anyhow::Context`. Import this trait to use `.tool_err()`
663/// and `.tool_context()` on any `Result` whose error type implements `Display`.
664///
665/// # Examples
666///
667/// ```rust
668/// use tower_mcp_types::error::ResultExt;
669///
670/// fn query_database() -> tower_mcp_types::Result<String> {
671/// let result: Result<String, std::io::Error> =
672/// Err(std::io::Error::other("connection refused"));
673/// let value = result.tool_context("database query failed")?;
674/// Ok(value)
675/// }
676/// ```
677pub trait ResultExt<T> {
678 /// Convert the error into a tool error.
679 ///
680 /// ```rust
681 /// use tower_mcp_types::error::ResultExt;
682 /// # fn example() -> tower_mcp_types::Result<()> {
683 /// let value: Result<i32, std::io::Error> = Err(std::io::Error::other("timeout"));
684 /// let value = value.tool_err()?;
685 /// # Ok(())
686 /// # }
687 /// ```
688 fn tool_err(self) -> std::result::Result<T, Error>;
689
690 /// Convert the error into a tool error with additional context.
691 ///
692 /// ```rust
693 /// use tower_mcp_types::error::ResultExt;
694 /// # fn example() -> tower_mcp_types::Result<()> {
695 /// let value: Result<i32, std::io::Error> = Err(std::io::Error::other("timeout"));
696 /// let value = value.tool_context("database query failed")?;
697 /// # Ok(())
698 /// # }
699 /// ```
700 fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error>;
701}
702
703impl<T, E: std::fmt::Display> ResultExt<T> for std::result::Result<T, E> {
704 fn tool_err(self) -> std::result::Result<T, Error> {
705 self.map_err(Error::tool_from)
706 }
707
708 fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error> {
709 self.map_err(|e| Error::tool_context(context, e))
710 }
711}
712
713impl From<JsonRpcError> for Error {
714 fn from(err: JsonRpcError) -> Self {
715 Error::JsonRpc(err)
716 }
717}
718
719impl From<std::convert::Infallible> for Error {
720 fn from(err: std::convert::Infallible) -> Self {
721 match err {}
722 }
723}
724
725/// Result type alias for tower-mcp
726pub type Result<T> = std::result::Result<T, Error>;
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 // =========================================================================
733 // SEP-2575 UnsupportedProtocolVersion (-32022) wire-format tests
734 // =========================================================================
735
736 #[test]
737 fn unsupported_protocol_version_code_is_negative_32022() {
738 // Renumbered from -32004 by the upstream error-code allocation
739 // (spec PR modelcontextprotocol#2907).
740 assert_eq!(McpErrorCode::UnsupportedProtocolVersion.code(), -32022);
741 }
742
743 #[test]
744 fn unsupported_protocol_version_constructor_has_spec_shape() {
745 let err =
746 JsonRpcError::unsupported_protocol_version("2027-01-01", ["2026-07-28", "2025-11-25"]);
747 assert_eq!(err.code, -32022);
748 let data = err.data.expect("data must be present");
749 assert_eq!(
750 data["supported"],
751 serde_json::json!(["2026-07-28", "2025-11-25"])
752 );
753 assert_eq!(data["requested"], "2027-01-01");
754 assert!(
755 data.get("supportedVersions").is_none(),
756 "spec field name is 'supported', not 'supportedVersions'"
757 );
758 }
759
760 #[test]
761 fn unsupported_protocol_version_data_round_trip() {
762 let original = UnsupportedProtocolVersionData {
763 supported: vec!["2026-07-28".into(), "2025-11-25".into()],
764 requested: "2027-01-01".into(),
765 };
766 let json = serde_json::to_value(&original).unwrap();
767 let parsed: UnsupportedProtocolVersionData = serde_json::from_value(json.clone()).unwrap();
768 assert_eq!(parsed.supported, original.supported);
769 assert_eq!(parsed.requested, original.requested);
770 }
771
772 #[test]
773 fn missing_required_client_capability_has_spec_shape() {
774 let required = ClientCapabilities {
775 sampling: Some(Default::default()),
776 ..Default::default()
777 };
778 let err = JsonRpcError::missing_required_client_capability(required);
779 assert_eq!(
780 err.code,
781 McpErrorCode::MissingRequiredClientCapability.code()
782 );
783 let data = err.data.expect("data must be present");
784 assert_eq!(
785 data,
786 serde_json::json!({
787 "requiredCapabilities": {
788 "sampling": {}
789 }
790 })
791 );
792 }
793
794 // =========================================================================
795 // SEP-2164: ResourceNotFound -> InvalidParams (-32602)
796 // =========================================================================
797
798 #[test]
799 fn resource_not_found_constructor_uses_invalid_params() {
800 let err = JsonRpcError::resource_not_found("file:///gone.txt");
801 assert_eq!(err.code, ErrorCode::InvalidParams.code());
802 assert_eq!(err.code, -32602);
803 assert!(err.message.contains("file:///gone.txt"));
804 // SEP-2164: data.uri carries the requested URI.
805 let data = err.data.expect("data must be present");
806 assert_eq!(data["uri"], "file:///gone.txt");
807 }
808
809 #[test]
810 fn resource_not_found_serializes_with_spec_code() {
811 let err = JsonRpcError::resource_not_found("urn:test:x");
812 let json = serde_json::to_value(&err).unwrap();
813 assert_eq!(json["code"], -32602);
814 }
815
816 // =========================================================================
817 // Subscribe-code migration (avoid SEP-2575 collision)
818 // =========================================================================
819
820 #[test]
821 fn subscribe_codes_moved_off_spec_assignments() {
822 // Our subscribe codes moved to -32008/-32009 when -32003/-32004 were
823 // believed spec-assigned. The SEP-2575 codes have since been
824 // renumbered to -32021/-32022 (spec PR modelcontextprotocol#2907);
825 // the subscribe codes stay put.
826 assert_eq!(McpErrorCode::AlreadySubscribed.code(), -32008);
827 assert_eq!(McpErrorCode::NotSubscribed.code(), -32009);
828 assert_eq!(McpErrorCode::MissingRequiredClientCapability.code(), -32021);
829 assert_eq!(McpErrorCode::UnsupportedProtocolVersion.code(), -32022);
830 }
831
832 // =========================================================================
833 // SEP-2243 HeaderMismatch (-32020) wire-format tests
834 // =========================================================================
835
836 #[test]
837 fn header_mismatch_code_is_negative_32020() {
838 // Renumbered from -32001 by the upstream error-code allocation
839 // (spec PR modelcontextprotocol#2907).
840 assert_eq!(McpErrorCode::HeaderMismatch.code(), -32020);
841 }
842
843 #[test]
844 fn header_mismatch_constructor_uses_spec_code() {
845 let err = JsonRpcError::header_mismatch(
846 "Mcp-Name header value 'foo' does not match body value 'bar'",
847 );
848 assert_eq!(err.code, -32020);
849 assert_eq!(err.code, McpErrorCode::HeaderMismatch.code());
850 assert!(err.message.contains("Mcp-Name"));
851 }
852
853 #[test]
854 fn request_timeout_moved_off_spec_assignment() {
855 // RequestTimeout moved to -32010 when -32001 was believed
856 // spec-assigned; HeaderMismatch is now -32020.
857 assert_eq!(McpErrorCode::RequestTimeout.code(), -32010);
858 assert_eq!(McpErrorCode::HeaderMismatch.code(), -32020);
859 }
860
861 #[test]
862 #[allow(deprecated)] // ResourceNotFound stays in the enum for backcompat
863 fn no_two_mcp_codes_share_a_value() {
864 let all = [
865 McpErrorCode::ConnectionClosed.code(),
866 McpErrorCode::HeaderMismatch.code(),
867 McpErrorCode::RequestTimeout.code(),
868 McpErrorCode::ResourceNotFound.code(),
869 McpErrorCode::MissingRequiredClientCapability.code(),
870 McpErrorCode::UnsupportedProtocolVersion.code(),
871 McpErrorCode::SessionNotFound.code(),
872 McpErrorCode::SessionRequired.code(),
873 McpErrorCode::Forbidden.code(),
874 McpErrorCode::AlreadySubscribed.code(),
875 McpErrorCode::NotSubscribed.code(),
876 McpErrorCode::UrlElicitationRequired.code(),
877 ];
878 let mut sorted = all.to_vec();
879 sorted.sort_unstable();
880 let original_len = sorted.len();
881 sorted.dedup();
882 assert_eq!(
883 sorted.len(),
884 original_len,
885 "collision in McpErrorCode values: {:?}",
886 all
887 );
888 }
889
890 #[test]
891 fn test_box_error_from_io_error() {
892 let io_err = std::io::Error::other("disk full");
893 let boxed: BoxError = io_err.into();
894 assert_eq!(boxed.to_string(), "disk full");
895 }
896
897 #[test]
898 fn test_box_error_from_string() {
899 let err: BoxError = "something went wrong".into();
900 assert_eq!(err.to_string(), "something went wrong");
901 }
902
903 #[test]
904 fn test_box_error_is_send_sync() {
905 fn assert_send_sync<T: Send + Sync>() {}
906 assert_send_sync::<BoxError>();
907 }
908
909 #[test]
910 fn test_tool_error_source_uses_box_error() {
911 let io_err = std::io::Error::other("timeout");
912 let tool_err = ToolError::new("failed").with_source(io_err);
913 assert!(tool_err.source.is_some());
914 assert_eq!(tool_err.source.unwrap().to_string(), "timeout");
915 }
916
917 #[test]
918 fn test_result_ext_tool_err() {
919 let result: std::result::Result<(), std::io::Error> =
920 Err(std::io::Error::other("disk full"));
921 let err = result.tool_err().unwrap_err();
922 assert!(matches!(err, Error::Tool(_)));
923 assert!(err.to_string().contains("disk full"));
924 }
925
926 #[test]
927 fn test_result_ext_tool_context() {
928 let result: std::result::Result<(), std::io::Error> =
929 Err(std::io::Error::other("connection refused"));
930 let err = result.tool_context("database query failed").unwrap_err();
931 assert!(matches!(err, Error::Tool(_)));
932 assert!(err.to_string().contains("database query failed"));
933 assert!(err.to_string().contains("connection refused"));
934 }
935
936 #[test]
937 fn test_result_ext_ok_passes_through() {
938 let result: std::result::Result<i32, std::io::Error> = Ok(42);
939 assert_eq!(result.tool_err().unwrap(), 42);
940 let result: std::result::Result<i32, std::io::Error> = Ok(42);
941 assert_eq!(result.tool_context("should not appear").unwrap(), 42);
942 }
943
944 // =========================================================================
945 // #1280: Error::into_json_rpc_error, structured-preserve / opaque-sanitize
946 // =========================================================================
947
948 #[test]
949 fn into_json_rpc_error_preserves_a_structured_error() {
950 let err = Error::JsonRpc(JsonRpcError::invalid_params("bad shape"));
951 let json = err.into_json_rpc_error();
952 assert_eq!(json.code, ErrorCode::InvalidParams.code());
953 assert_eq!(json.message, "bad shape");
954 }
955
956 #[test]
957 fn into_json_rpc_error_sanitizes_an_opaque_error_to_internal_error() {
958 let err = Error::Internal("db pool exhausted".to_string());
959 let json = err.into_json_rpc_error();
960 assert_eq!(json.code, ErrorCode::InternalError.code());
961 // This crate's existing convention (matched by `CatchError` in
962 // `tower-mcp::transport::service`) is to include the `Display` text
963 // in the sanitized message rather than a bare placeholder.
964 assert!(json.message.contains("db pool exhausted"));
965 }
966}
967
968#[cfg(test)]
969mod jsonrpc_error_display_tests {
970 use super::*;
971
972 /// #1223: `Error::JsonRpc` rendered through `{0:?}`, so every rejection a
973 /// peer sent reached the user as a struct dump, including `data: None`
974 /// which says nothing at all.
975 #[test]
976 fn a_jsonrpc_error_reads_as_a_sentence() {
977 let error = Error::JsonRpc(JsonRpcError {
978 code: -32603,
979 message: "tool index unavailable".to_string(),
980 data: None,
981 });
982 assert_eq!(
983 error.to_string(),
984 "JSON-RPC error: tool index unavailable (code -32603)"
985 );
986 assert!(
987 !error.to_string().contains("data: None"),
988 "absent data must not be mentioned: {error}"
989 );
990 }
991
992 /// Structured detail is shown when a producer supplied it.
993 #[test]
994 fn structured_data_is_shown_when_present() {
995 let error = JsonRpcError {
996 code: -32021,
997 message: "Missing required client capability".to_string(),
998 data: Some(serde_json::json!({"requiredCapabilities": {"extensions": {}}})),
999 };
1000 let rendered = error.to_string();
1001 assert!(rendered.starts_with("Missing required client capability (code -32021): "));
1002 assert!(rendered.contains("requiredCapabilities"));
1003 }
1004
1005 /// `Debug` still yields the full value, which is what a log or a panic
1006 /// wants and what `{0:?}` was reaching for.
1007 #[test]
1008 fn debug_is_unchanged() {
1009 let error = JsonRpcError {
1010 code: -32603,
1011 message: "boom".to_string(),
1012 data: None,
1013 };
1014 let debug = format!("{error:?}");
1015 assert!(debug.contains("JsonRpcError"));
1016 assert!(debug.contains("code: -32603"));
1017 assert!(debug.contains("data: None"));
1018 }
1019}