Skip to main content

github_mcp/core/
errors.rs

1// GitHub v3 REST API MCP server — generated by mcpify. Do not hand-edit.
2
3use thiserror::Error;
4
5/// Mirrors `targets::typescript`'s `McpifyError` hierarchy (`errors.ts`):
6/// one variant per error family, each carrying the literal `code()` string
7/// callers match on instead of downcasting.
8#[derive(Debug, Error)]
9pub enum McpifyError {
10    #[error("{0}")]
11    Configuration(String),
12
13    #[error("{0}")]
14    Authentication(String),
15
16    #[error("{message}{}", format_validation_details(details))]
17    Validation {
18        message: String,
19        details: Option<serde_json::Value>,
20    },
21
22    #[error("{0}")]
23    NotFound(String),
24
25    #[error("circuit breaker is open")]
26    CircuitBreakerOpen,
27
28    #[error("rate limit exceeded")]
29    RateLimitExceeded,
30}
31
32/// Renders `Validation::details` (a JSON array of jsonschema/Ajv violation
33/// strings, or `None` when the schema itself is missing) as a `": a; b; c"`
34/// suffix, so `Display`/`to_string()` — what `run_tool` sends back as the
35/// MCP tool's error text — carries the actual violated constraints instead
36/// of just the generic "invalid input for 'X'" / "unexpected response
37/// shape for 'X'" message.
38fn format_validation_details(details: &Option<serde_json::Value>) -> String {
39    match details.as_ref().and_then(serde_json::Value::as_array) {
40        Some(errors) if !errors.is_empty() => {
41            let joined = errors
42                .iter()
43                .filter_map(serde_json::Value::as_str)
44                .collect::<Vec<_>>()
45                .join("; ");
46            format!(": {joined}")
47        }
48        _ => String::new(),
49    }
50}
51
52impl McpifyError {
53    pub fn code(&self) -> &'static str {
54        match self {
55            Self::Configuration(_) => "CONFIGURATION_ERROR",
56            Self::Authentication(_) => "AUTHENTICATION_ERROR",
57            Self::Validation { .. } => "VALIDATION_ERROR",
58            Self::NotFound(_) => "NOT_FOUND",
59            Self::CircuitBreakerOpen => "CIRCUIT_BREAKER_OPEN",
60            Self::RateLimitExceeded => "RATE_LIMIT_EXCEEDED",
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn every_variant_reports_its_error_code() {
71        assert_eq!(
72            McpifyError::Configuration("x".into()).code(),
73            "CONFIGURATION_ERROR"
74        );
75        assert_eq!(
76            McpifyError::Authentication("x".into()).code(),
77            "AUTHENTICATION_ERROR"
78        );
79        assert_eq!(
80            McpifyError::Validation {
81                message: "x".into(),
82                details: None
83            }
84            .code(),
85            "VALIDATION_ERROR"
86        );
87        assert_eq!(McpifyError::NotFound("x".into()).code(), "NOT_FOUND");
88        assert_eq!(
89            McpifyError::CircuitBreakerOpen.code(),
90            "CIRCUIT_BREAKER_OPEN"
91        );
92        assert_eq!(McpifyError::RateLimitExceeded.code(), "RATE_LIMIT_EXCEEDED");
93    }
94
95    #[test]
96    fn validation_display_appends_details_when_present() {
97        let err = McpifyError::Validation {
98            message: "unexpected response shape for 'getAllProjects'".into(),
99            details: Some(serde_json::json!(["\"foo\" is not of type \"object\""])),
100        };
101        assert_eq!(
102            err.to_string(),
103            "unexpected response shape for 'getAllProjects': \"foo\" is not of type \"object\""
104        );
105    }
106
107    #[test]
108    fn validation_display_omits_suffix_when_details_absent() {
109        let err = McpifyError::Validation {
110            message: "invalid input for 'getIssue'".into(),
111            details: None,
112        };
113        assert_eq!(err.to_string(), "invalid input for 'getIssue'");
114    }
115}