mcp_server_sqlite/
orchestrator.rs1use std::path::Path;
5
6use anyhow::Context;
7use rmcp::{
8 ServiceExt,
9 service::{RoleServer, RunningService},
10 transport::IntoTransport,
11};
12use rusqlite::OpenFlags;
13
14use crate::access_control::AuthorizationResolver;
15use crate::cli::Cli;
16use crate::mcp::McpServerSqlite;
17
18pub async fn serve<T, E, A>(
24 cli: Cli,
25 transport: T,
26) -> anyhow::Result<RunningService<RoleServer, McpServerSqlite>>
27where
28 T: IntoTransport<RoleServer, E, A>,
29 E: std::error::Error + Send + Sync + 'static,
30{
31 let Cli {
32 database,
33 init_sql,
34 preset,
35 allow,
36 deny,
37 timeout_ms,
38 } = cli;
39
40 let is_new = is_new_database(&database);
41
42 tracing::info!(database = %database, preset = %preset, "starting server");
43
44 let flags = OpenFlags::SQLITE_OPEN_URI
45 | OpenFlags::SQLITE_OPEN_READ_WRITE
46 | OpenFlags::SQLITE_OPEN_CREATE;
47 let manager =
48 r2d2_sqlite::SqliteConnectionManager::file(&database).with_flags(flags);
49 let pool = r2d2::Pool::new(manager)
50 .context("Failed to create the connection pool")?;
51
52 if is_new && !init_sql.is_empty() {
53 run_init_scripts(&pool, &init_sql)?;
54 }
55
56 tracing::info!(
57 allow_rules = allow.len(),
58 deny_rules = deny.len(),
59 "access control configured"
60 );
61
62 let resolver = allow
63 .into_iter()
64 .map(|selector| (selector, true))
65 .chain(deny.into_iter().map(|selector| (selector, false)))
66 .fold(
67 AuthorizationResolver::from(preset),
68 |resolver, (selector, allow)| {
69 resolver.with_selector(selector, allow)
70 },
71 );
72
73 let query_timeout = timeout_ms.map(std::time::Duration::from_millis);
74 if let Some(timeout) = query_timeout {
75 tracing::info!(
76 timeout_ms = timeout.as_millis(),
77 "query timeout configured"
78 );
79 }
80
81 let server = McpServerSqlite::new(pool, resolver, query_timeout);
82 let service = server.serve(transport).await?;
83
84 tracing::info!("server ready");
85
86 Ok(service)
87}
88
89fn is_new_database(database: &str) -> bool {
93 let path = database
94 .strip_prefix("file:")
95 .unwrap_or(database)
96 .split('?')
97 .next()
98 .unwrap_or(database);
99
100 path == ":memory:" || path.is_empty() || !Path::new(path).exists()
101}
102
103fn run_init_scripts(
107 pool: &r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>,
108 scripts: &[std::path::PathBuf],
109) -> anyhow::Result<()> {
110 let conn = pool
111 .get()
112 .context("Failed to acquire a connection for init scripts")?;
113
114 for path in scripts {
115 tracing::info!(path = %path.display(), "executing init script");
116 let sql = std::fs::read_to_string(path)
117 .with_context(|| format!("Failed to read {}", path.display()))?;
118 conn.execute_batch(&sql)
119 .with_context(|| format!("Failed to execute {}", path.display()))?;
120 }
121
122 Ok(())
123}