Skip to main content

database_mcp_server/
error.rs

1//! Application error types for the MCP server.
2//!
3//! Defines [`AppError`] with variants for connection, security validation,
4//! and query execution failures. Configuration errors live in the
5//! `config` crate.
6
7/// Errors that can occur during MCP server operation.
8#[derive(Debug, thiserror::Error)]
9pub enum AppError {
10    /// Database connection failed.
11    #[error("Database connection error: {0}")]
12    Connection(String),
13
14    /// Query blocked by read-only mode.
15    #[error("Query blocked: only SELECT, SHOW, DESC, DESCRIBE, USE queries are allowed in read-only mode")]
16    ReadOnlyViolation,
17
18    /// `LOAD_FILE()` function blocked for security.
19    #[error("Operation forbidden: LOAD_FILE() is not allowed for security reasons")]
20    LoadFileBlocked,
21
22    /// INTO OUTFILE/DUMPFILE blocked for security.
23    #[error("Operation forbidden: SELECT INTO OUTFILE/DUMPFILE is not allowed for security reasons")]
24    IntoOutfileBlocked,
25
26    /// Multiple SQL statements blocked.
27    #[error("Query blocked: only single statements are allowed")]
28    MultiStatement,
29
30    /// Invalid database or table name identifier.
31    #[error("Invalid identifier '{0}': must not be empty, whitespace-only, or contain control characters")]
32    InvalidIdentifier(String),
33
34    /// Query exceeded the configured timeout.
35    #[error("Query timed out after {elapsed_secs:.1}s: {sql}")]
36    QueryTimeout {
37        /// Wall-clock seconds elapsed before cancellation.
38        elapsed_secs: f64,
39        /// The SQL statement that was cancelled.
40        sql: String,
41    },
42
43    /// Database query execution failed.
44    #[error("Database error: {0}")]
45    Query(String),
46
47    /// Table isn't found in database.
48    #[error("Table not found: {0}")]
49    TableNotFound(String),
50
51    /// JSON serialization failed.
52    #[error("Serialization error: {0}")]
53    Serialization(String),
54}
55
56impl From<serde_json::Error> for AppError {
57    fn from(e: serde_json::Error) -> Self {
58        Self::Serialization(e.to_string())
59    }
60}
61
62impl From<AppError> for rmcp::model::ErrorData {
63    fn from(e: AppError) -> Self {
64        Self::internal_error(e.to_string(), None)
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn query_timeout_display_includes_elapsed_and_sql() {
74        let err = AppError::QueryTimeout {
75            elapsed_secs: 30.123_456,
76            sql: "SELECT * FROM big_table".into(),
77        };
78        let msg = err.to_string();
79        assert!(msg.contains("30.1"), "expected elapsed in message: {msg}");
80        assert!(
81            msg.contains("SELECT * FROM big_table"),
82            "expected SQL in message: {msg}"
83        );
84    }
85}