Skip to main content

systemprompt_traits/
extension_error.rs

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