Skip to main content

mcp_server_sqlite/
orchestrator.rs

1//! Server orchestration. Handles database setup, access control configuration,
2//! and MCP server construction from CLI arguments.
3
4use 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
18/// Builds and serves the MCP server from the given CLI arguments over the
19/// provided transport. Creates the connection pool, runs init scripts on new
20/// databases, assembles the authorization resolver from the preset and
21/// allow/deny overrides, and starts the MCP service. Returns the running
22/// service handle.
23pub 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
89/// Returns `true` if the database needs initialization. In-memory databases are
90/// always new. File-backed databases are new only when the file does not yet
91/// exist. Handles both plain paths and `file:` URI format.
92fn 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
103/// Executes the SQL init scripts against the database in order. Each file is
104/// read to a string and executed as a single batch. Called only when the
105/// database is being created for the first time.
106fn 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}