use rmcp::handler::server::tool::IntoCallToolResult;
use rmcp::model::{CallToolResponse, CallToolResult};
use serde_json::json;
use slave_framework::turso;
/// Errors returned by MCP tools.
///
/// They become tool results with `isError: true` and structured content
/// `{"error": "<message>", "code": "<code>"}`, so clients and models can react
/// to them. Malformed arguments never reach a tool: the MCP layer rejects them
/// with a JSON-RPC `invalid params` error.
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("{0} not found")]
NotFound(String),
#[error("{0}")]
Validation(String),
#[error("{0}")]
Conflict(String),
#[error("authentication required")]
Unauthorized,
#[error("invalid email or password")]
InvalidCredentials,
#[error("you do not have permission to do this")]
Forbidden,
#[error("database error: {0}")]
Database(#[from] turso::Error),
#[error("{0}")]
Internal(String),
}
pub type AppResult<T> = Result<T, AppError>;
impl From<slave_framework::Error> for AppError {
fn from(err: slave_framework::Error) -> Self {
Self::Internal(err.to_string())
}
}
impl AppError {
/// Stable, machine-readable error code.
pub fn code(&self) -> &'static str {
match self {
Self::NotFound(_) => "not_found",
Self::Validation(_) => "validation_failed",
Self::Conflict(_) => "conflict",
Self::Unauthorized => "unauthenticated",
Self::InvalidCredentials => "invalid_credentials",
Self::Forbidden => "forbidden",
Self::Database(_) | Self::Internal(_) => "internal_error",
}
}
}
impl IntoCallToolResult for AppError {
fn into_call_tool_result(self) -> Result<CallToolResponse, rmcp::ErrorData> {
let code = self.code();
// Server-side failures are logged in full but never leaked to clients.
let message = if code == "internal_error" {
if self.to_string().contains("no such table") {
tracing::error!("{self} (have you run `slave migrate`?)");
} else {
tracing::error!("{self}");
}
"internal server error".to_owned()
} else {
self.to_string()
};
Ok(CallToolResult::structured_error(json!({ "error": message, "code": code })).into())
}
}