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    /// Pool cache is at capacity and no idle pool can be evicted.
35    #[error("pool cache is full ({cap} pools, all in use); retry later")]
36    PoolCacheFull {
37        /// Maximum number of cached pools configured.
38        cap: usize,
39    },
40
41    /// Query exceeded the configured timeout.
42    #[error("Query timed out after {elapsed_secs:.1}s: {sql}")]
43    QueryTimeout {
44        /// Wall-clock seconds elapsed before cancellation.
45        elapsed_secs: f64,
46        /// The SQL statement that was cancelled.
47        sql: String,
48    },
49
50    /// Database query execution failed.
51    #[error("Database error: {0}")]
52    Query(String),
53
54    /// Table isn't found in database.
55    #[error("Table not found: {0}")]
56    TableNotFound(String),
57
58    /// JSON serialization failed.
59    #[error("Serialization error: {0}")]
60    Serialization(String),
61}
62
63impl From<serde_json::Error> for AppError {
64    fn from(e: serde_json::Error) -> Self {
65        Self::Serialization(e.to_string())
66    }
67}
68
69impl From<AppError> for rmcp::model::ErrorData {
70    fn from(e: AppError) -> Self {
71        Self::internal_error(e.to_string(), None)
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn query_timeout_display_includes_elapsed_and_sql() {
81        let err = AppError::QueryTimeout {
82            elapsed_secs: 30.123_456,
83            sql: "SELECT * FROM big_table".into(),
84        };
85        let msg = err.to_string();
86        assert!(msg.contains("30.1"), "expected elapsed in message: {msg}");
87        assert!(
88            msg.contains("SELECT * FROM big_table"),
89            "expected SQL in message: {msg}"
90        );
91    }
92}