Skip to main content

database_mcp_sqlite/
handler.rs

1//! MCP handler for the `SQLite` backend.
2//!
3//! [`SqliteHandler`] wraps [`SqliteBackend`] and implements
4//! [`ServerHandler`] using rmcp tool macros.
5
6use database_mcp_backend::types::{GetTableSchemaRequest, ListTablesRequest, QueryRequest};
7use database_mcp_config::DatabaseConfig;
8use database_mcp_server::tools;
9use rmcp::ServerHandler;
10use rmcp::handler::server::router::tool::ToolRouter;
11use rmcp::handler::server::wrapper::Parameters;
12use rmcp::model::ErrorData;
13
14use super::SqliteBackend;
15
16/// MCP handler for `SQLite` databases.
17///
18/// Owns a [`SqliteBackend`] and a pre-filtered [`ToolRouter`].
19/// Write tools are removed when the backend is in read-only mode.
20#[derive(Clone, Debug)]
21pub struct SqliteHandler {
22    backend: SqliteBackend,
23    tool_router: ToolRouter<Self>,
24}
25
26impl SqliteHandler {
27    /// Creates a new `SQLite` handler.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if the database connection cannot be established.
32    pub async fn new(config: &DatabaseConfig) -> Result<Self, database_mcp_backend::AppError> {
33        let backend = SqliteBackend::new(config).await?;
34        let mut tool_router = Self::tool_router();
35        if backend.read_only {
36            tool_router.remove_route("write_query");
37        }
38        Ok(Self { backend, tool_router })
39    }
40}
41
42#[rmcp::tool_router]
43impl SqliteHandler {
44    /// List all tables in a specific database.
45    #[rmcp::tool(
46        name = "list_tables",
47        description = "List all tables in a specific database. Requires database_name from list_databases.",
48        annotations(
49            read_only_hint = true,
50            destructive_hint = false,
51            idempotent_hint = true,
52            open_world_hint = false
53        )
54    )]
55    async fn list_tables(&self, Parameters(req): Parameters<ListTablesRequest>) -> Result<String, ErrorData> {
56        tools::list_tables(self.backend.list_tables(&req.database_name), &req.database_name).await
57    }
58
59    /// Get column definitions for a table.
60    #[rmcp::tool(
61        name = "get_table_schema",
62        description = "Get column definitions (type, nullable, key, default) and foreign key relationships for a table. Requires database_name and table_name.",
63        annotations(
64            read_only_hint = true,
65            destructive_hint = false,
66            idempotent_hint = true,
67            open_world_hint = false
68        )
69    )]
70    async fn get_table_schema(&self, Parameters(req): Parameters<GetTableSchemaRequest>) -> Result<String, ErrorData> {
71        tools::get_table_schema(
72            self.backend.get_table_schema(&req.database_name, &req.table_name),
73            &req.database_name,
74            &req.table_name,
75        )
76        .await
77    }
78
79    /// Execute a read-only SQL query.
80    #[rmcp::tool(
81        name = "read_query",
82        description = "Execute a read-only SQL query (SELECT, SHOW, DESCRIBE, USE, EXPLAIN).",
83        annotations(
84            read_only_hint = true,
85            destructive_hint = false,
86            idempotent_hint = true,
87            open_world_hint = true
88        )
89    )]
90    async fn read_query(&self, Parameters(req): Parameters<QueryRequest>) -> Result<String, ErrorData> {
91        let db = tools::resolve_database(&req.database_name);
92        tools::read_query(
93            self.backend.execute_query(&req.sql_query, db),
94            &req.sql_query,
95            &req.database_name,
96            |sql| {
97                database_mcp_backend::validation::validate_read_only_with_dialect(
98                    sql,
99                    &sqlparser::dialect::SQLiteDialect {},
100                )
101            },
102        )
103        .await
104    }
105
106    /// Execute a write SQL query.
107    #[rmcp::tool(
108        name = "write_query",
109        description = "Execute a write SQL query (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP).",
110        annotations(
111            read_only_hint = false,
112            destructive_hint = true,
113            idempotent_hint = false,
114            open_world_hint = true
115        )
116    )]
117    async fn write_query(&self, Parameters(req): Parameters<QueryRequest>) -> Result<String, ErrorData> {
118        let db = tools::resolve_database(&req.database_name);
119        tools::write_query(
120            self.backend.execute_query(&req.sql_query, db),
121            &req.sql_query,
122            &req.database_name,
123        )
124        .await
125    }
126}
127
128#[rmcp::tool_handler(router = self.tool_router)]
129impl ServerHandler for SqliteHandler {
130    fn get_info(&self) -> rmcp::model::ServerInfo {
131        database_mcp_server::server_info()
132    }
133}