Skip to main content

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}