1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//! [`ExtensionError`] trait and the MCP error wire type.
//!
//! Domain crates implement [`ExtensionError`] on their own typed error
//! enums so the API and MCP layers can render them into responses without
//! introducing a dependency on each domain. Over HTTP an extension error
//! renders through `systemprompt_models::api::ApiError::from_extension`.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.
use http::StatusCode;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct McpErrorData {
pub code: i32,
pub message: String,
// JSON: JSON-RPC 2.0 §5.1 error `data` field — protocol boundary.
#[serde(skip_serializing_if = "Option::is_none")]
// JSON: JSON-RPC 2.0 `error.data` is spec-defined as free-form.
pub data: Option<serde_json::Value>,
}
impl McpErrorData {
#[must_use]
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
#[must_use]
// JSON: JSON-RPC 2.0 `error.data` is spec-defined as free-form.
pub fn with_data(mut self, data: serde_json::Value) -> Self {
self.data = Some(data);
self
}
}
pub trait ExtensionError: std::error::Error + Send + Sync + 'static {
fn code(&self) -> &'static str;
fn status(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
fn is_retryable(&self) -> bool {
false
}
fn user_message(&self) -> String {
self.to_string()
}
fn to_mcp_error(&self) -> McpErrorData {
McpErrorData {
code: i32::from(self.status().as_u16()),
message: self.user_message(),
data: Some(serde_json::json!({
"code": self.code(),
"retryable": self.is_retryable(),
})),
}
}
}