use std::sync::Arc;
use rmcp::{
ServerHandler,
handler::server::tool::ToolRouter,
model::{ServerCapabilities, ServerInfo},
tool_handler,
};
use crate::{
access_control::AuthorizationResolver,
tools::{
backup_tool::BackupTool, create_fts_index_tool::CreateFtsIndexTool,
database_info_tool::DatabaseInfoTool,
describe_table_tool::DescribeTableTool, execute_tool::ExecuteTool,
explain_query_tool::ExplainQueryTool,
list_foreign_keys_tool::ListForeignKeysTool,
list_indexes_tool::ListIndexesTool, list_tables_tool::ListTablesTool,
list_triggers_tool::ListTriggersTool, list_views_tool::ListViewsTool,
search_fts_tool::SearchFtsTool, vacuum_tool::VacuumTool,
},
traits::SqliteServerTool,
};
pub struct McpServerSqlite {
pool: r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>,
authorization_resolver: Arc<AuthorizationResolver>,
query_timeout: Option<std::time::Duration>,
tool_router: ToolRouter<Self>,
}
impl McpServerSqlite {
pub fn new(
pool: r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>,
authorization_resolver: AuthorizationResolver,
query_timeout: Option<std::time::Duration>,
) -> Self {
let tool_router = Self::tool_router();
Self {
pool,
authorization_resolver: Arc::new(authorization_resolver),
query_timeout,
tool_router,
}
}
pub fn connection(
&self,
) -> Result<
r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>,
ConnectionError,
> {
tracing::debug!("checking out pooled connection");
let conn = self.pool.get().map_err(|source| {
tracing::error!(%source, "pool checkout failed");
ConnectionError::Pool(source)
})?;
let resolver = self.authorization_resolver.clone();
conn.authorizer(Some(move |ctx: rusqlite::hooks::AuthContext<'_>| {
resolver.authorization(ctx)
}))
.map_err(|source| {
tracing::error!(%source, "failed to install authorizer");
ConnectionError::Authorizer(source)
})?;
if let Some(timeout) = self.query_timeout {
let deadline = std::time::Instant::now() + timeout;
conn.progress_handler(
1000,
Some(move || std::time::Instant::now() > deadline),
)
.map_err(|source| {
tracing::error!(%source, "failed to install progress handler");
ConnectionError::ProgressHandler(source)
})?;
}
Ok(conn)
}
pub fn tool_router() -> ToolRouter<Self> {
ToolRouter::<Self>::new()
.with_route((ExecuteTool::tool(), ExecuteTool::handler_func()))
.with_route((
ListTablesTool::tool(),
ListTablesTool::handler_func(),
))
.with_route((
DescribeTableTool::tool(),
DescribeTableTool::handler_func(),
))
.with_route((
ListIndexesTool::tool(),
ListIndexesTool::handler_func(),
))
.with_route((
ListForeignKeysTool::tool(),
ListForeignKeysTool::handler_func(),
))
.with_route((ListViewsTool::tool(), ListViewsTool::handler_func()))
.with_route((
ListTriggersTool::tool(),
ListTriggersTool::handler_func(),
))
.with_route((
ExplainQueryTool::tool(),
ExplainQueryTool::handler_func(),
))
.with_route((BackupTool::tool(), BackupTool::handler_func()))
.with_route((
CreateFtsIndexTool::tool(),
CreateFtsIndexTool::handler_func(),
))
.with_route((SearchFtsTool::tool(), SearchFtsTool::handler_func()))
.with_route((VacuumTool::tool(), VacuumTool::handler_func()))
.with_route((
DatabaseInfoTool::tool(),
DatabaseInfoTool::handler_func(),
))
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConnectionError {
#[error("pool checkout failed: {0}")]
Pool(r2d2::Error),
#[error("failed to install authorizer: {0}")]
Authorizer(rusqlite::Error),
#[error("failed to install progress handler: {0}")]
ProgressHandler(rusqlite::Error),
}
#[tool_handler]
impl ServerHandler for McpServerSqlite {
fn get_info(&self) -> ServerInfo {
let mut info = ServerInfo::default();
info.server_info.name = env!("CARGO_PKG_NAME").into();
info.server_info.version = env!("CARGO_PKG_VERSION").into();
info.instructions = Some(INSTRUCTIONS.into());
info.capabilities =
ServerCapabilities::builder().enable_tools().build();
info
}
}
const INSTRUCTIONS: &str = "\
You are connected to an MCP SQLite server with fine-grained access control.
Available tools:
- execute: Run any SQL query (SELECT, INSERT, UPDATE, DELETE, DDL)
- list_tables: Show all tables with their CREATE TABLE schemas
- describe_table: Show columns, types, constraints, and defaults for a table
- list_indexes: Show indexes with columns, uniqueness, and partial predicates
- list_foreign_keys: Show foreign key constraints for a table
- list_views: Show all views with their defining SQL
- list_triggers: Show triggers with event, timing, and SQL
- explain_query: Show the EXPLAIN QUERY PLAN for a SQL statement
- create_fts_index: Create a full-text search index on a table
- search_fts: Search a full-text index with BM25 ranking and snippets
- backup: Back up the database to a file
- vacuum: Reclaim unused space and defragment the database
- database_info: Show database metadata (size, page count, journal mode, etc.)
Start by calling list_tables to discover the schema, then use describe_table \
for details on specific tables. Use explain_query to optimize slow queries. \
All operations respect the server's access control policy.";