Skip to main content

pmcp/server/
error_codes.rs

1//! Standard error codes for validation with client elicitation support
2//!
3//! Provides a consistent set of error codes that clients can use to
4//! automate UI elicitation and provide better user experiences.
5
6use serde_json::{json, Value};
7
8/// Standard validation error codes for MCP tools
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ValidationErrorCode {
11    /// Required field is missing
12    MissingField,
13    /// Field value has invalid format (e.g., email, URL)
14    InvalidFormat,
15    /// Numeric value is out of allowed range
16    OutOfRange,
17    /// Value is not in the allowed set
18    NotAllowed,
19    /// String is too short
20    TooShort,
21    /// String is too long
22    TooLong,
23    /// Array/collection has too few items
24    TooFewItems,
25    /// Array/collection has too many items
26    TooManyItems,
27    /// Pattern/regex mismatch
28    PatternMismatch,
29    /// Path traversal or security violation
30    SecurityViolation,
31    /// Type mismatch (expected different type)
32    TypeMismatch,
33    /// Custom validation failed
34    CustomValidation,
35}
36
37impl ValidationErrorCode {
38    /// Get the string code for this error
39    pub fn as_str(&self) -> &'static str {
40        match self {
41            Self::MissingField => "missing_field",
42            Self::InvalidFormat => "invalid_format",
43            Self::OutOfRange => "out_of_range",
44            Self::NotAllowed => "not_allowed",
45            Self::TooShort => "too_short",
46            Self::TooLong => "too_long",
47            Self::TooFewItems => "too_few_items",
48            Self::TooManyItems => "too_many_items",
49            Self::PatternMismatch => "pattern_mismatch",
50            Self::SecurityViolation => "security_violation",
51            Self::TypeMismatch => "type_mismatch",
52            Self::CustomValidation => "custom_validation",
53        }
54    }
55
56    /// Get a human-readable description
57    pub fn description(&self) -> &'static str {
58        match self {
59            Self::MissingField => "Required field is missing",
60            Self::InvalidFormat => "Field value has invalid format",
61            Self::OutOfRange => "Value is outside the allowed range",
62            Self::NotAllowed => "Value is not in the allowed set",
63            Self::TooShort => "Value is too short",
64            Self::TooLong => "Value is too long",
65            Self::TooFewItems => "Collection has too few items",
66            Self::TooManyItems => "Collection has too many items",
67            Self::PatternMismatch => "Value does not match the required pattern",
68            Self::SecurityViolation => "Security constraint violated",
69            Self::TypeMismatch => "Value has incorrect type",
70            Self::CustomValidation => "Custom validation failed",
71        }
72    }
73}
74
75impl std::fmt::Display for ValidationErrorCode {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(f, "{}", self.as_str())
78    }
79}
80
81/// Structured validation error with elicitation support
82#[derive(Debug, Clone)]
83pub struct ValidationError {
84    /// The error code
85    pub code: ValidationErrorCode,
86    /// The field that failed validation
87    pub field: String,
88    /// Human-readable error message
89    pub message: String,
90    /// Expected format/value description
91    pub expected: Option<String>,
92    /// The actual value that failed (for debugging)
93    pub actual: Option<Value>,
94    /// Additional context for the error
95    pub context: Option<Value>,
96}
97
98impl ValidationError {
99    /// Create a new validation error
100    pub fn new(code: ValidationErrorCode, field: impl Into<String>) -> Self {
101        let field = field.into();
102        let message = format!("{} for field '{}'", code.description(), field);
103        Self {
104            code,
105            field,
106            message,
107            expected: None,
108            actual: None,
109            context: None,
110        }
111    }
112
113    /// Set the expected value/format description
114    pub fn expected(mut self, expected: impl Into<String>) -> Self {
115        self.expected = Some(expected.into());
116        self
117    }
118
119    /// Set the actual value that failed validation
120    pub fn actual(mut self, actual: impl Into<Value>) -> Self {
121        self.actual = Some(actual.into());
122        self
123    }
124
125    /// Set additional context
126    pub fn context(mut self, context: impl Into<Value>) -> Self {
127        self.context = Some(context.into());
128        self
129    }
130
131    /// Set a custom message
132    pub fn message(mut self, message: impl Into<String>) -> Self {
133        self.message = message.into();
134        self
135    }
136
137    /// Convert to a JSON value for elicitation
138    pub fn to_json(&self) -> Value {
139        let mut obj = json!({
140            "code": self.code.as_str(),
141            "field": self.field,
142            "message": self.message,
143            "elicit": true,
144        });
145
146        if let Some(expected) = &self.expected {
147            obj["expected"] = json!(expected);
148        }
149
150        if let Some(actual) = &self.actual {
151            obj["actual"] = actual.clone();
152        }
153
154        if let Some(context) = &self.context {
155            obj["context"] = context.clone();
156        }
157
158        obj
159    }
160
161    /// Convert to an MCP Error
162    pub fn to_error(&self) -> crate::Error {
163        crate::Error::Validation(self.to_json().to_string())
164    }
165}
166
167/// Helper trait for creating validation errors
168pub trait IntoValidationError {
169    /// Convert to a validation error
170    fn into_validation_error(self, code: ValidationErrorCode, field: &str) -> crate::Error;
171}
172
173impl<T, E> IntoValidationError for Result<T, E>
174where
175    E: std::fmt::Display,
176{
177    fn into_validation_error(self, code: ValidationErrorCode, field: &str) -> crate::Error {
178        match self {
179            Ok(_) => panic!("Cannot convert Ok result to validation error"),
180            Err(e) => ValidationError::new(code, field)
181                .message(format!("{}: {}", code.description(), e))
182                .to_error(),
183        }
184    }
185}
186
187// Convenience functions for common validations
188
189/// Create a missing field error
190pub fn missing_field(field: &str) -> crate::Error {
191    ValidationError::new(ValidationErrorCode::MissingField, field)
192        .expected("This field is required")
193        .to_error()
194}
195
196/// Create an invalid format error
197pub fn invalid_format(field: &str, expected: &str) -> crate::Error {
198    ValidationError::new(ValidationErrorCode::InvalidFormat, field)
199        .expected(expected)
200        .to_error()
201}
202
203/// Create an out of range error
204pub fn out_of_range<T: std::fmt::Display>(
205    field: &str,
206    value: T,
207    min: Option<T>,
208    max: Option<T>,
209) -> crate::Error {
210    let expected = match (min, max) {
211        (Some(min), Some(max)) => format!("Value between {} and {}", min, max),
212        (Some(min), None) => format!("Value >= {}", min),
213        (None, Some(max)) => format!("Value <= {}", max),
214        (None, None) => "Value within valid range".to_string(),
215    };
216
217    ValidationError::new(ValidationErrorCode::OutOfRange, field)
218        .expected(expected)
219        .actual(json!(value.to_string()))
220        .to_error()
221}
222
223/// Create a not allowed error
224pub fn not_allowed<T: std::fmt::Display>(field: &str, value: T, allowed: &[T]) -> crate::Error {
225    let allowed_str = allowed
226        .iter()
227        .map(|v| v.to_string())
228        .collect::<Vec<_>>()
229        .join(", ");
230
231    ValidationError::new(ValidationErrorCode::NotAllowed, field)
232        .expected(format!("One of: {}", allowed_str))
233        .actual(json!(value.to_string()))
234        .to_error()
235}
236
237/// Create a pattern mismatch error
238pub fn pattern_mismatch(field: &str, pattern: &str) -> crate::Error {
239    ValidationError::new(ValidationErrorCode::PatternMismatch, field)
240        .expected(format!("Match pattern: {}", pattern))
241        .to_error()
242}
243
244/// Create a security violation error
245pub fn security_violation(field: &str, reason: &str) -> crate::Error {
246    ValidationError::new(ValidationErrorCode::SecurityViolation, field)
247        .message(format!("Security violation: {}", reason))
248        .to_error()
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_validation_error_json() {
257        let error = ValidationError::new(ValidationErrorCode::OutOfRange, "age")
258            .expected("18-120")
259            .actual(json!(150))
260            .message("Age must be between 18 and 120");
261
262        let json = error.to_json();
263        assert_eq!(json["code"], "out_of_range");
264        assert_eq!(json["field"], "age");
265        assert_eq!(json["expected"], "18-120");
266        assert_eq!(json["actual"], 150);
267        assert_eq!(json["elicit"], true);
268    }
269
270    #[test]
271    fn test_convenience_functions() {
272        let error = missing_field("email");
273        assert!(error.to_string().contains("missing_field"));
274
275        let error = invalid_format("email", "user@example.com");
276        assert!(error.to_string().contains("invalid_format"));
277
278        let error = out_of_range("age", 200, Some(18), Some(120));
279        assert!(error.to_string().contains("out_of_range"));
280    }
281}