database_mcp_sqlite/
handler.rs1use database_mcp_config::DatabaseConfig;
9use database_mcp_server::{Server, server_info};
10use rmcp::RoleServer;
11use rmcp::handler::server::router::tool::ToolRouter;
12use rmcp::handler::server::tool::ToolCallContext;
13use rmcp::model::{CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, ServerInfo, Tool};
14use rmcp::service::RequestContext;
15use rmcp::{ErrorData, ServerHandler};
16
17use crate::connection::SqliteConnection;
18use crate::tools::{
19 DropTableTool, ExplainQueryTool, GetTableSchemaTool, ListTablesTool, ReadQueryTool, WriteQueryTool,
20};
21
22const DESCRIPTION: &str = "Database MCP Server for SQLite";
24
25const INSTRUCTIONS: &str = r"## Workflow
27
281. Call `listTables` to discover tables in the connected database.
292. Call `getTableSchema` with a `table` to inspect columns, types, and foreign keys before writing queries.
303. Use `readQuery` for read-only SQL (SELECT).
314. Use `writeQuery` for data changes (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP).
325. Use `explainQuery` to analyze query execution plans and diagnose slow queries.
336. Use `dropTable` to remove a table from the database.
34
35## Constraints
36
37- The `writeQuery` and `dropTable` tools are hidden when read-only mode is active.
38- Multi-statement queries are not supported. Send one statement per request.";
39
40#[derive(Clone)]
45pub struct SqliteHandler {
46 pub(crate) config: DatabaseConfig,
47 pub(crate) connection: SqliteConnection,
48 tool_router: ToolRouter<Self>,
49}
50
51impl std::fmt::Debug for SqliteHandler {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("SqliteHandler")
54 .field("read_only", &self.config.read_only)
55 .field("connection", &self.connection)
56 .finish_non_exhaustive()
57 }
58}
59
60impl SqliteHandler {
61 #[must_use]
66 pub fn new(config: &DatabaseConfig) -> Self {
67 Self {
68 config: config.clone(),
69 connection: SqliteConnection::new(config),
70 tool_router: build_tool_router(config.read_only),
71 }
72 }
73}
74
75impl From<SqliteHandler> for Server {
76 fn from(handler: SqliteHandler) -> Self {
78 Self::new(handler)
79 }
80}
81
82fn build_tool_router(read_only: bool) -> ToolRouter<SqliteHandler> {
84 let mut router = ToolRouter::new()
85 .with_async_tool::<ListTablesTool>()
86 .with_async_tool::<GetTableSchemaTool>()
87 .with_async_tool::<ReadQueryTool>()
88 .with_async_tool::<ExplainQueryTool>();
89
90 if !read_only {
91 router = router
92 .with_async_tool::<WriteQueryTool>()
93 .with_async_tool::<DropTableTool>();
94 }
95 router
96}
97
98impl ServerHandler for SqliteHandler {
99 fn get_info(&self) -> ServerInfo {
100 let mut info = server_info();
101 info.server_info.description = Some(DESCRIPTION.into());
102 info.instructions = Some(INSTRUCTIONS.into());
103 info
104 }
105
106 async fn call_tool(
107 &self,
108 request: CallToolRequestParams,
109 context: RequestContext<RoleServer>,
110 ) -> Result<CallToolResult, ErrorData> {
111 let tcc = ToolCallContext::new(self, request, context);
112 self.tool_router.call(tcc).await
113 }
114
115 async fn list_tools(
116 &self,
117 _request: Option<PaginatedRequestParams>,
118 _context: RequestContext<RoleServer>,
119 ) -> Result<ListToolsResult, ErrorData> {
120 Ok(ListToolsResult {
121 tools: self.tool_router.list_all(),
122 next_cursor: None,
123 meta: None,
124 })
125 }
126
127 fn get_tool(&self, name: &str) -> Option<Tool> {
128 self.tool_router.get(name).cloned()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use database_mcp_config::DatabaseBackend;
136
137 fn handler(read_only: bool) -> SqliteHandler {
138 SqliteHandler::new(&DatabaseConfig {
139 backend: DatabaseBackend::Sqlite,
140 name: Some(":memory:".into()),
141 read_only,
142 ..DatabaseConfig::default()
143 })
144 }
145
146 #[tokio::test]
147 async fn router_exposes_all_six_tools_in_read_write_mode() {
148 let router = handler(false).tool_router;
149 for name in [
150 "listTables",
151 "getTableSchema",
152 "dropTable",
153 "readQuery",
154 "writeQuery",
155 "explainQuery",
156 ] {
157 assert!(router.has_route(name), "missing tool: {name}");
158 }
159 }
160
161 #[tokio::test]
162 async fn router_hides_write_tools_in_read_only_mode() {
163 let router = handler(true).tool_router;
164 assert!(router.has_route("listTables"));
165 assert!(router.has_route("getTableSchema"));
166 assert!(router.has_route("readQuery"));
167 assert!(router.has_route("explainQuery"));
168 assert!(!router.has_route("writeQuery"));
169 assert!(!router.has_route("dropTable"));
170 }
171
172 #[tokio::test]
173 async fn list_tables_annotations() {
174 let router = handler(false).tool_router;
175 let tool = router.get("listTables").expect("listTables registered");
176
177 let annotations = tool.annotations.as_ref().expect("annotations present");
178 assert_eq!(annotations.read_only_hint, Some(true));
179 assert_eq!(annotations.destructive_hint, Some(false));
180 assert_eq!(annotations.idempotent_hint, Some(true));
181 assert_eq!(annotations.open_world_hint, Some(false));
182 }
183}