#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::core::auth::diagnostics::{AuthDiagnostic, AuthDiagnostics, AuthErrorType};
use crate::core::mcp_types::{NextAction, ReasonCode};
use crate::models::auth::SecuritySchemeDetails;
#[derive(Debug, Clone, PartialEq)]
pub enum ErrorClass {
Auth(AuthErrorSubtype),
Policy,
Validation,
RateLimit,
NotFound,
Server,
Unknown,
}
impl ErrorClass {
pub fn as_str(&self) -> &'static str {
match self {
ErrorClass::Auth(_) => "auth_error",
ErrorClass::Policy => "policy_error",
ErrorClass::Validation => "validation_error",
ErrorClass::RateLimit => "rate_limit",
ErrorClass::NotFound => "not_found",
ErrorClass::Server => "server_error",
ErrorClass::Unknown => "unknown_error",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AuthErrorSubtype {
Missing, Invalid, Expired, Scope, }
impl AuthErrorSubtype {
pub fn as_str(&self) -> &'static str {
match self {
AuthErrorSubtype::Missing => "missing_credentials",
AuthErrorSubtype::Invalid => "invalid_credentials",
AuthErrorSubtype::Expired => "expired_token",
AuthErrorSubtype::Scope => "insufficient_scope",
}
}
}
#[derive(Debug, Clone)]
pub struct EnrichedResponseData {
pub status_code: u16,
pub body: String,
pub headers: HashMap<String, String>,
pub auth_scheme_used: Option<String>,
pub request_url: String,
pub operation_id: String,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpErrorGuidance {
pub error_class: String,
pub error_subtype: Option<String>,
pub diagnosis: String,
pub resolutions: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_action: Option<NextAction>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub shell_hints: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_after: Option<u64>,
}
impl McpErrorGuidance {
pub fn unknown(status_code: u16) -> Self {
Self {
error_class: "unknown_error".to_string(),
error_subtype: None,
diagnosis: format!("Request failed with status {}", status_code),
resolutions: vec![
"Check the API documentation for this endpoint".to_string(),
"Verify your request parameters are correct".to_string(),
],
next_action: Some(NextAction {
tool: Some("api_show".to_string()),
params: serde_json::json!({}),
reason_code: ReasonCode::GetOperationDetails,
}),
shell_hints: vec![],
retry_after: None,
}
}
pub fn server_error(status_code: u16, body: &str) -> Self {
let diagnosis = if body.len() > 200 {
format!("Server error ({}): {}...", status_code, &body[..200])
} else if body.is_empty() {
format!("Server error ({})", status_code)
} else {
format!("Server error ({}): {}", status_code, body)
};
Self {
error_class: "server_error".to_string(),
error_subtype: Some(format!("http_{}", status_code)),
diagnosis,
resolutions: vec![
"The API server is experiencing issues".to_string(),
"Try again in a few moments".to_string(),
"If the problem persists, check the API status page".to_string(),
],
next_action: None,
shell_hints: vec![],
retry_after: Some(30),
}
}
pub fn rate_limit(headers: &HashMap<String, String>, body: &str) -> Self {
let retry_after = headers
.get("retry-after")
.and_then(|v| v.parse::<u64>().ok());
let mut resolutions = vec![];
if let Some(seconds) = retry_after {
resolutions.push(format!("Wait {} seconds before retrying", seconds));
} else {
resolutions.push("Wait before retrying".to_string());
}
if let Some(limit) = headers.get("x-ratelimit-limit") {
resolutions.push(format!("Rate limit: {} requests per period", limit));
}
if let Some(remaining) = headers.get("x-ratelimit-remaining") {
resolutions.push(format!("Remaining requests: {}", remaining));
}
resolutions.push("Reduce the frequency of your API requests".to_string());
let diagnosis = if body.contains("quota") {
"API quota exceeded".to_string()
} else {
"Rate limit exceeded".to_string()
};
Self {
error_class: "rate_limit".to_string(),
error_subtype: None,
diagnosis,
resolutions,
next_action: None,
shell_hints: vec![],
retry_after: retry_after.or(Some(60)),
}
}
pub fn not_found(operation_id: &str, body: &str) -> Self {
let body_lower = body.to_lowercase();
let (diagnosis, resolutions) =
if body_lower.contains("endpoint") || body_lower.contains("route") {
(
"API endpoint not found".to_string(),
vec![
"The API endpoint may have been removed or renamed".to_string(),
"Check the API documentation for the correct path".to_string(),
"Verify you're using the correct API version".to_string(),
],
)
} else {
(
"Resource not found".to_string(),
vec![
"The requested resource does not exist".to_string(),
"Verify the resource ID or path parameters are correct".to_string(),
"The resource may have been deleted".to_string(),
],
)
};
Self {
error_class: "not_found".to_string(),
error_subtype: None,
diagnosis,
resolutions,
next_action: Some(NextAction {
tool: Some("api_show".to_string()),
params: serde_json::json!({ "operation_id": operation_id }),
reason_code: ReasonCode::GetOperationDetails,
}),
shell_hints: vec![],
retry_after: None,
}
}
pub fn validation_error(body: &str, operation_id: &str) -> Self {
let mut resolutions = vec![];
let mut diagnosis = "Request validation failed".to_string();
if let Ok(json) = serde_json::from_str::<serde_json::Value>(body) {
if let Some(errors) = json.get("errors").and_then(|e| e.as_array()) {
for error in errors.iter().take(3) {
if let Some(msg) = error.get("message").and_then(|m| m.as_str()) {
resolutions.push(format!("Fix: {}", msg));
} else if let Some(msg) = error.as_str() {
resolutions.push(format!("Fix: {}", msg));
}
}
} else if let Some(detail) = json.get("detail").and_then(|d| d.as_str()) {
diagnosis = detail.to_string();
} else if let Some(message) = json.get("message").and_then(|m| m.as_str()) {
diagnosis = message.to_string();
}
if let Some(fields) = json.get("fields").or(json.get("errors")) {
if let Some(obj) = fields.as_object() {
for (field, error) in obj.iter().take(3) {
let error_msg = error
.as_str()
.or_else(|| error.get("message").and_then(|m| m.as_str()))
.unwrap_or("invalid");
resolutions.push(format!("Field '{}': {}", field, error_msg));
}
}
}
}
if resolutions.is_empty() {
resolutions.push("Check that all required parameters are provided".to_string());
resolutions.push("Verify parameter types match the expected schema".to_string());
resolutions.push("Review the API documentation for valid values".to_string());
}
Self {
error_class: "validation_error".to_string(),
error_subtype: Some("invalid_parameters".to_string()),
diagnosis,
resolutions,
next_action: Some(NextAction {
tool: Some("api_query".to_string()),
params: serde_json::json!({ "operation_id": operation_id }),
reason_code: ReasonCode::GetParameterDetails,
}),
shell_hints: vec![],
retry_after: None,
}
}
pub fn policy_error(rule: Option<&str>, reason: Option<&str>) -> Self {
let diagnosis = reason
.map(|r| r.to_string())
.unwrap_or_else(|| "Operation blocked by policy".to_string());
let mut resolutions =
vec!["This operation is restricted by the configured policy".to_string()];
if let Some(rule_name) = rule {
resolutions.push(format!("Blocked by rule: {}", rule_name));
}
resolutions.push("Contact the administrator if you need access".to_string());
Self {
error_class: "policy_error".to_string(),
error_subtype: None,
diagnosis,
resolutions,
next_action: Some(NextAction {
tool: Some("api_help".to_string()),
params: serde_json::json!({}),
reason_code: ReasonCode::StartDiscovery,
}),
shell_hints: vec![],
retry_after: None,
}
}
}
pub fn classify_error(
status_code: u16,
headers: &HashMap<String, String>,
body: &str,
policy_active: bool,
) -> ErrorClass {
match status_code {
401 => ErrorClass::Auth(classify_401(headers, body)),
403 => {
let body_lower = body.to_lowercase();
if policy_active && (body_lower.contains("policy") || body_lower.contains("blocked")) {
ErrorClass::Policy
} else if body_lower.contains("scope") || body_lower.contains("permission") {
ErrorClass::Auth(AuthErrorSubtype::Scope)
} else if policy_active {
ErrorClass::Policy
} else {
ErrorClass::Auth(AuthErrorSubtype::Scope)
}
}
404 => ErrorClass::NotFound,
422 | 400 => ErrorClass::Validation,
429 => ErrorClass::RateLimit,
500..=599 => ErrorClass::Server,
_ => ErrorClass::Unknown,
}
}
fn classify_401(headers: &HashMap<String, String>, body: &str) -> AuthErrorSubtype {
let body_lower = body.to_lowercase();
let www_auth = headers
.get("www-authenticate")
.map(|s| s.to_lowercase())
.unwrap_or_default();
if body_lower.contains("expired") || www_auth.contains("expired") {
return AuthErrorSubtype::Expired;
}
if body_lower.contains("missing")
|| body_lower.contains("required")
|| body_lower.contains("no auth")
|| body_lower.contains("authentication required")
{
return AuthErrorSubtype::Missing;
}
AuthErrorSubtype::Invalid
}
pub struct CliToMcpTranslator;
impl CliToMcpTranslator {
pub fn translate(cli_command: &str) -> Option<NextAction> {
let cmd = cli_command.trim();
if cmd.starts_with("mrapids auth connect") {
let parts: Vec<&str> = cmd.split_whitespace().collect();
let scheme = parts.get(3).map(|s| s.to_string());
return Some(NextAction {
tool: Some("api_auth".to_string()),
params: serde_json::json!({
"action": "connect",
"scheme": scheme,
}),
reason_code: ReasonCode::ConfigureAuth,
});
}
if cmd.starts_with("mrapids auth validate") {
return Some(NextAction {
tool: Some("api_auth".to_string()),
params: serde_json::json!({ "action": "validate" }),
reason_code: ReasonCode::ConfigureAuth,
});
}
if cmd.starts_with("mrapids auth refresh") {
let parts: Vec<&str> = cmd.split_whitespace().collect();
let scheme = parts.get(3).map(|s| s.to_string());
return Some(NextAction {
tool: Some("api_auth".to_string()),
params: serde_json::json!({
"action": "refresh",
"scheme": scheme,
}),
reason_code: ReasonCode::ConfigureAuth,
});
}
if cmd.starts_with("mrapids auth detect") {
return Some(NextAction {
tool: Some("api_auth".to_string()),
params: serde_json::json!({ "action": "detect" }),
reason_code: ReasonCode::ConfigureAuth,
});
}
None
}
}
impl From<&AuthDiagnostic> for McpErrorGuidance {
fn from(diag: &AuthDiagnostic) -> Self {
let error_subtype = match diag.error_type {
AuthErrorType::MissingCredentials => "missing_credentials",
AuthErrorType::InvalidCredentials => "invalid_credentials",
AuthErrorType::ExpiredToken => "expired_token",
AuthErrorType::InsufficientScopes => "insufficient_scope",
AuthErrorType::NetworkError => "network_error",
AuthErrorType::ConfigurationError => "configuration_error",
AuthErrorType::UnsupportedScheme => "unsupported_scheme",
};
let next_action = diag
.help_commands
.iter()
.find_map(|cmd| CliToMcpTranslator::translate(cmd));
let shell_hints: Vec<String> = diag
.help_commands
.iter()
.filter(|cmd| CliToMcpTranslator::translate(cmd).is_none())
.cloned()
.collect();
McpErrorGuidance {
error_class: "auth_error".to_string(),
error_subtype: Some(error_subtype.to_string()),
diagnosis: if diag.details.is_empty() {
format!("Authentication failed: {:?}", diag.error_type)
} else {
diag.details.clone()
},
resolutions: diag.suggestions.clone(),
next_action,
shell_hints,
retry_after: None,
}
}
}
pub struct McpErrorGuidanceGenerator {
auth_diagnostics: Option<AuthDiagnostics>,
policy_active: bool,
}
impl McpErrorGuidanceGenerator {
pub fn new(
security_schemes: HashMap<String, SecuritySchemeDetails>,
policy_active: bool,
) -> Self {
let auth_diagnostics = if security_schemes.is_empty() {
None
} else {
Some(AuthDiagnostics::new(security_schemes))
};
Self {
auth_diagnostics,
policy_active,
}
}
pub fn generate(&self, response: &EnrichedResponseData) -> McpErrorGuidance {
let error_class = classify_error(
response.status_code,
&response.headers,
&response.body,
self.policy_active,
);
match error_class {
ErrorClass::Auth(_) => self.generate_auth_guidance(response),
ErrorClass::Policy => {
McpErrorGuidance::policy_error(None, Some("Operation blocked by policy"))
}
ErrorClass::Validation => {
McpErrorGuidance::validation_error(&response.body, &response.operation_id)
}
ErrorClass::RateLimit => {
McpErrorGuidance::rate_limit(&response.headers, &response.body)
}
ErrorClass::NotFound => {
McpErrorGuidance::not_found(&response.operation_id, &response.body)
}
ErrorClass::Server => {
McpErrorGuidance::server_error(response.status_code, &response.body)
}
ErrorClass::Unknown => McpErrorGuidance::unknown(response.status_code),
}
}
fn generate_auth_guidance(&self, response: &EnrichedResponseData) -> McpErrorGuidance {
if let Some(ref diagnostics) = self.auth_diagnostics {
let mut header_map = reqwest::header::HeaderMap::new();
for (key, value) in &response.headers {
if let (Ok(name), Ok(val)) = (
reqwest::header::HeaderName::from_bytes(key.as_bytes()),
reqwest::header::HeaderValue::from_str(value),
) {
header_map.insert(name, val);
}
}
let status = reqwest::StatusCode::from_u16(response.status_code)
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR);
let scheme_name = response.auth_scheme_used.as_deref().unwrap_or("unknown");
let diag = diagnostics.diagnose_from_response(
status,
&header_map,
&response.body,
scheme_name,
);
McpErrorGuidance::from(&diag)
} else {
self.generate_fallback_auth_guidance(response)
}
}
fn generate_fallback_auth_guidance(&self, response: &EnrichedResponseData) -> McpErrorGuidance {
let subtype = classify_401(&response.headers, &response.body);
let (diagnosis, resolutions) = match subtype {
AuthErrorSubtype::Expired => (
"Authentication token has expired".to_string(),
vec![
"Generate a new token from the API provider".to_string(),
"Update your environment variable with the new token".to_string(),
],
),
AuthErrorSubtype::Missing => (
"No authentication credentials provided".to_string(),
vec![
"Set up authentication for this API".to_string(),
"Check that the required environment variable is set".to_string(),
],
),
AuthErrorSubtype::Invalid => (
"Authentication credentials were rejected".to_string(),
vec![
"Verify your credentials are correct".to_string(),
"Check for extra spaces or quotes in your token".to_string(),
"Ensure the token hasn't been revoked".to_string(),
],
),
AuthErrorSubtype::Scope => (
"Insufficient permissions for this operation".to_string(),
vec![
"Your credentials don't have the required scope".to_string(),
"Request additional permissions from the API provider".to_string(),
],
),
};
McpErrorGuidance {
error_class: "auth_error".to_string(),
error_subtype: Some(subtype.as_str().to_string()),
diagnosis,
resolutions,
next_action: Some(NextAction {
tool: Some("api_auth".to_string()),
params: serde_json::json!({}),
reason_code: ReasonCode::ConfigureAuth,
}),
shell_hints: vec![],
retry_after: None,
}
}
}
pub fn extract_response_headers(headers: &reqwest::header::HeaderMap) -> HashMap<String, String> {
headers
.iter()
.filter_map(|(name, value)| {
value
.to_str()
.ok()
.map(|v| (name.as_str().to_lowercase(), v.to_string()))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classify_401_expired() {
let headers = HashMap::new();
let body = r#"{"error": "token_expired", "message": "Your token has expired"}"#;
let result = classify_401(&headers, body);
assert_eq!(result, AuthErrorSubtype::Expired);
}
#[test]
fn test_classify_401_missing() {
let headers = HashMap::new();
let body = r#"{"error": "authentication_required"}"#;
let result = classify_401(&headers, body);
assert_eq!(result, AuthErrorSubtype::Missing);
}
#[test]
fn test_classify_401_invalid() {
let headers = HashMap::new();
let body = r#"{"error": "invalid_token"}"#;
let result = classify_401(&headers, body);
assert_eq!(result, AuthErrorSubtype::Invalid);
}
#[test]
fn test_classify_error_rate_limit() {
let headers = HashMap::new();
let result = classify_error(429, &headers, "", false);
assert_eq!(result, ErrorClass::RateLimit);
}
#[test]
fn test_classify_error_validation() {
let headers = HashMap::new();
let result = classify_error(422, &headers, "", false);
assert_eq!(result, ErrorClass::Validation);
}
#[test]
fn test_cli_to_mcp_auth_connect() {
let cmd = "mrapids auth connect petstore_auth --auth-type bearer";
let result = CliToMcpTranslator::translate(cmd);
assert!(result.is_some());
let action = result.unwrap();
assert_eq!(action.tool, Some("api_auth".to_string()));
}
#[test]
fn test_cli_to_mcp_export_not_translated() {
let cmd = "export STRIPE_API_KEY=sk_live_xxx";
let result = CliToMcpTranslator::translate(cmd);
assert!(result.is_none());
}
#[test]
fn test_validation_error_guidance() {
let body = r#"{"errors": [{"field": "email", "message": "Invalid email format"}]}"#;
let guidance = McpErrorGuidance::validation_error(body, "createUser");
assert_eq!(guidance.error_class, "validation_error");
assert!(guidance.resolutions.iter().any(|r| r.contains("email")));
}
#[test]
fn test_rate_limit_with_retry_after() {
let mut headers = HashMap::new();
headers.insert("retry-after".to_string(), "120".to_string());
let guidance = McpErrorGuidance::rate_limit(&headers, "");
assert_eq!(guidance.retry_after, Some(120));
}
}