Skip to main content

kagi_mcp/
error.rs

1use kagi_sdk::{CredentialKind, KagiError};
2
3#[derive(Debug, thiserror::Error)]
4pub enum StartupError {
5    #[error(
6        "invalid `{env_var}` value `{value}`; expected one of `auto`, `official`, or `session`"
7    )]
8    InvalidBackendMode {
9        env_var: &'static str,
10        value: String,
11    },
12
13    #[error("missing required credential `{env_var}` for `{mode}` backend mode{hint_suffix}")]
14    MissingCredential {
15        env_var: &'static str,
16        mode: &'static str,
17        hint_suffix: String,
18    },
19
20    #[error("invalid credential in `{env_var}`: {reason}")]
21    InvalidCredential {
22        env_var: &'static str,
23        reason: String,
24    },
25
26    #[error("failed to construct Kagi client: {reason}")]
27    ClientConstruction { reason: String },
28}
29
30#[derive(Debug, thiserror::Error)]
31#[error("{message}")]
32pub struct ToolFailure {
33    message: String,
34}
35
36impl ToolFailure {
37    pub fn message(&self) -> &str {
38        &self.message
39    }
40
41    pub fn parse_drift(reason: impl Into<String>) -> Self {
42        Self {
43            message: format!(
44                "Kagi returned an unexpected response shape for this capability ({})",
45                reason.into()
46            ),
47        }
48    }
49
50    pub fn from_kagi_error(error: KagiError) -> Self {
51        match error {
52            KagiError::InvalidCredential { kind, reason } => Self {
53                message: format!(
54                    "Server credential is invalid for {kind}. Update the credential and restart ({reason})."
55                ),
56            },
57            KagiError::MissingCredentialConfiguration { reason }
58            | KagiError::InvalidClientConfiguration { reason } => Self {
59                message: format!("Server startup configuration is invalid ({reason})."),
60            },
61            KagiError::ConflictingCredentialConfiguration { .. } => Self {
62                message: "Server startup configuration is invalid (conflicting credential configuration)."
63                    .to_string(),
64            },
65            KagiError::InvalidInput { field, reason } => Self {
66                message: format!(
67                    "The server generated an invalid upstream request for `{field}` ({reason})."
68                ),
69            },
70            KagiError::UnsupportedAuthSurface { .. } | KagiError::UnsupportedCapability { .. } => {
71                Self {
72                    message: "Selected backend mode does not support this capability. Update `KAGI_MCP_BACKEND` and restart.".to_string(),
73                }
74            }
75            KagiError::UnauthorizedBotToken { .. } => Self {
76                message: auth_failure_message(Some(CredentialKind::BotToken)),
77            },
78            KagiError::InvalidSession { .. } => Self {
79                message: auth_failure_message(Some(CredentialKind::SessionToken)),
80            },
81            KagiError::Transport { source, .. } => {
82                if source.is_timeout() {
83                    return Self {
84                        message: "Kagi request timed out. Retry shortly.".to_string(),
85                    };
86                }
87
88                Self {
89                    message: "Kagi transport request failed. Check network connectivity and retry.".to_string(),
90                }
91            }
92            KagiError::ResponseParse { reason, .. } => Self::parse_drift(reason),
93            KagiError::ApiFailure {
94                endpoint,
95                status,
96                code,
97                message,
98                ..
99            } => {
100                if api_failure_indicates_auth_failure(status, code.as_deref(), &message) {
101                    return Self {
102                        message: auth_failure_message(Some(endpoint.spec().allowed_credential)),
103                    };
104                }
105
106                if status == 429 {
107                    return Self {
108                        message:
109                            "Kagi rate-limited this request (HTTP 429). Retry after a short delay."
110                                .to_string(),
111                    };
112                }
113
114                if status >= 500 {
115                    return Self {
116                        message: format!(
117                            "Kagi upstream service is currently failing (HTTP {status}). Retry later."
118                        ),
119                    };
120                }
121
122                if status < 400 {
123                    let detail = code
124                        .map(|code| format!("{code}: {message}"))
125                        .unwrap_or(message);
126
127                    return Self {
128                        message: format!(
129                            "Kagi reported an application-level failure (HTTP {status}): {detail}"
130                        ),
131                    };
132                }
133
134                Self {
135                    message: format!(
136                        "Kagi rejected the upstream request (HTTP {status}). Verify input and retry."
137                    ),
138                }
139            }
140        }
141    }
142}
143
144fn auth_failure_message(expected_kind: Option<CredentialKind>) -> String {
145    let base =
146        "Authentication failed with Kagi. Verify the configured credential and restart the server.";
147
148    let guidance = match expected_kind {
149        Some(CredentialKind::BotToken) => {
150            " This backend expects an official bot token in `KAGI_API_KEY`; the configured value may belong to session-web auth (`KAGI_SESSION_TOKEN`) instead."
151        }
152        Some(CredentialKind::SessionToken) => {
153            " This backend expects a session-web token in `KAGI_SESSION_TOKEN`; the configured value may belong to official bot-token auth (`KAGI_API_KEY`) instead."
154        }
155        None => {
156            " `KAGI_API_KEY` should be used only for official bot tokens, and `KAGI_SESSION_TOKEN` should be used only for session-web tokens."
157        }
158    };
159
160    format!("{base}{guidance}")
161}
162
163fn api_failure_indicates_auth_failure(status: u16, code: Option<&str>, message: &str) -> bool {
164    if matches!(status, 401 | 403) {
165        return true;
166    }
167
168    if code.is_some_and(|raw_code| {
169        let normalized = raw_code.trim().to_ascii_lowercase();
170        matches!(normalized.as_str(), "unauthorized" | "invalid_session")
171    }) {
172        return true;
173    }
174
175    let normalized_message = message.trim().to_ascii_lowercase();
176    normalized_message == "unauthorized"
177        || normalized_message == "unauthorized: unauthorized"
178        || normalized_message.contains("invalid session")
179}