1use serde::Serialize;
12use serde_json::{json, Value};
13
14use crate::actions::{action_names, ActionValidationError};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "snake_case")]
20pub enum ServiceErrorKind {
21 Validation,
23 Timeout,
25 RateLimited,
27 AuthRejected,
29 UpstreamUnavailable,
31 Execution,
33 Internal,
35}
36
37impl ServiceErrorKind {
38 pub fn as_str(self) -> &'static str {
40 match self {
41 Self::Validation => "validation",
42 Self::Timeout => "timeout",
43 Self::RateLimited => "rate_limited",
44 Self::AuthRejected => "auth_rejected",
45 Self::UpstreamUnavailable => "upstream_unavailable",
46 Self::Execution => "execution",
47 Self::Internal => "internal",
48 }
49 }
50
51 pub fn http_status_code(self) -> u16 {
53 match self {
54 Self::Validation => 400,
55 Self::AuthRejected => 403,
56 Self::RateLimited => 429,
57 Self::Timeout | Self::UpstreamUnavailable => 503,
58 Self::Execution | Self::Internal => 500,
59 }
60 }
61
62 pub fn retryable(self) -> bool {
64 matches!(
65 self,
66 Self::Validation | Self::Timeout | Self::RateLimited | Self::UpstreamUnavailable
67 )
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73pub struct ToolError {
74 pub schema_version: u8,
76 pub kind: ServiceErrorKind,
78 pub code: String,
80 pub message: String,
82 pub retryable: bool,
84 pub remediation: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub field: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub bad_value: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub expected_pattern: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub reason_kind: Option<String>,
98 #[serde(skip_serializing_if = "Vec::is_empty")]
100 pub available_actions: Vec<&'static str>,
101}
102
103impl ToolError {
104 pub fn validation(
106 code: impl Into<String>,
107 message: impl Into<String>,
108 remediation: impl Into<String>,
109 ) -> Self {
110 Self {
111 schema_version: 1,
112 kind: ServiceErrorKind::Validation,
113 code: code.into(),
114 message: message.into(),
115 retryable: true,
116 remediation: remediation.into(),
117 field: None,
118 bad_value: None,
119 expected_pattern: None,
120 reason_kind: None,
121 available_actions: Vec::new(),
122 }
123 }
124
125 pub fn from_action_validation(error: &ActionValidationError) -> Self {
128 Self::from_action_validation_with_actions(error, action_names())
129 }
130
131 pub fn from_action_validation_with_actions(
134 error: &ActionValidationError,
135 available_actions: Vec<&'static str>,
136 ) -> Self {
137 let mut tool_error = Self::validation(error.code(), error.to_string(), error.remediation())
138 .with_available_actions(available_actions);
139 if let Some(field) = error.field() {
140 tool_error = tool_error.with_field(field);
141 }
142 if let Some(bad_value) = error.bad_value() {
143 tool_error = tool_error.with_bad_value(bad_value);
144 }
145 tool_error
146 }
147
148 pub fn execution(error: &anyhow::Error) -> Self {
151 let reason_kind = classify_execution_error(error);
152 Self {
153 schema_version: 1,
154 kind: reason_kind,
155 code: "execution_error".to_owned(),
156 message: "Tool execution failed. Check server logs for details.".to_owned(),
157 retryable: reason_kind.retryable(),
158 remediation: "Check service configuration and upstream availability, then retry. Use action=status or action=help for diagnostics.".to_owned(),
159 field: None,
160 bad_value: None,
161 expected_pattern: None,
162 reason_kind: Some(reason_kind.as_str().to_owned()),
163 available_actions: Vec::new(),
164 }
165 }
166
167 pub fn with_field(mut self, field: impl Into<String>) -> Self {
169 self.field = Some(field.into());
170 self
171 }
172
173 pub fn with_bad_value(mut self, bad_value: impl Into<String>) -> Self {
175 self.bad_value = Some(bad_value.into());
176 self
177 }
178
179 pub fn with_expected_pattern(mut self, expected_pattern: impl Into<String>) -> Self {
181 self.expected_pattern = Some(expected_pattern.into());
182 self
183 }
184
185 pub fn with_available_actions(mut self, available_actions: Vec<&'static str>) -> Self {
187 self.available_actions = available_actions;
188 self
189 }
190
191 pub fn http_status_code(&self) -> u16 {
193 self.kind.http_status_code()
194 }
195
196 pub fn to_rest_payload(&self) -> Value {
198 let mut payload = json!({
199 "error": self.message,
200 "kind": self.kind.as_str(),
201 "schema_version": self.schema_version,
202 "code": self.code,
203 "message": self.message,
204 "retryable": self.retryable,
205 "remediation": self.remediation,
206 });
207 self.add_optional_fields(&mut payload);
208 payload
209 }
210
211 pub fn to_mcp_payload(&self, tool: &str, action: Option<&str>) -> Value {
214 let mut payload = json!({
215 "kind": "mcp_tool_error",
216 "schema_version": self.schema_version,
217 "code": self.code,
218 "tool": tool,
219 "action": action,
220 "message": self.message,
221 "retryable": self.retryable,
222 "remediation": self.remediation,
223 });
224 payload["service_error_kind"] = json!(self.kind.as_str());
225 self.add_optional_fields(&mut payload);
226 payload
227 }
228
229 fn add_optional_fields(&self, payload: &mut Value) {
230 if let Some(field) = &self.field {
231 payload["field"] = json!(field);
232 }
233 if let Some(bad_value) = &self.bad_value {
234 payload["bad_value"] = json!(bad_value);
235 }
236 if let Some(expected_pattern) = &self.expected_pattern {
237 payload["expected_pattern"] = json!(expected_pattern);
238 }
239 if let Some(reason_kind) = &self.reason_kind {
240 payload["reason_kind"] = json!(reason_kind);
241 }
242 if !self.available_actions.is_empty() {
243 payload["available_actions"] = json!(self.available_actions);
244 }
245 }
246}
247
248pub type ServiceError = ToolError;
250
251pub fn classify_execution_error(error: &anyhow::Error) -> ServiceErrorKind {
254 let text = error.to_string().to_ascii_lowercase();
255 if text.contains("timeout") || text.contains("timed out") {
256 ServiceErrorKind::Timeout
257 } else if text.contains("rate limit") || text.contains("429") {
258 ServiceErrorKind::RateLimited
259 } else if text.contains("unauthorized")
260 || text.contains("forbidden")
261 || text.contains("401")
262 || text.contains("403")
263 {
264 ServiceErrorKind::AuthRejected
265 } else if text.contains("connection refused")
266 || text.contains("connection reset")
267 || text.contains("dns")
268 || text.contains("unreachable")
269 || text.contains("temporarily unavailable")
270 {
271 ServiceErrorKind::UpstreamUnavailable
272 } else {
273 ServiceErrorKind::Execution
274 }
275}
276
277#[cfg(test)]
278#[path = "errors_tests.rs"]
279mod tests;