mcp-server-sqlite 1.0.0

An MCP server for SQLite with fine-grained access control
Documentation
//! Command-line interface definition for the MCP SQLite server.

use std::path::PathBuf;

use clap::Parser;

use crate::access_control::{AccessControlSelector, Preset};

/// An MCP server that exposes a SQLite database over MCP (Model Context
/// Protocol), with fine-grained access control for every SQL operation SQLite
/// can perform.
///
/// Access control is built on a preset system layered with explicit overrides.
/// Start by choosing a --preset (defaults to read-only), then refine with
/// --allow and --deny flags:
///
/// - Read             all column reads
/// - Read(Students)   reads on the Students table
/// - Read(*.ssn)      reads on any ssn column
/// - Function(count)  the count() SQL function
///
/// More specific selectors (more pinned fields) override less specific ones.
/// When allow and deny conflict at the same specificity, deny wins.
#[derive(Parser)]
#[command(
    author,
    version,
    about = "MCP server for SQLite with fine-grained access control",
    term_width = 80,
    after_long_help = "\
EXAMPLES:
  Read-only server (default) with in-memory database:
    mcp-server-sqlite

  Read-only on a persistent database with schema init:
    mcp-server-sqlite --database ./app.db --init-sql schema.sql

  Read-write server that blocks one table:
    mcp-server-sqlite --preset read-write --deny Delete(AuditLog)

  Read-only with a carve-out denying sensitive columns:
    mcp-server-sqlite --database ./app.db --deny Read(Users.ssn)

  Deny-everything baseline, selectively allowing reads:
    mcp-server-sqlite --preset deny-everything \\
      --allow Read --allow Function(count)"
)]
pub struct Cli {
    /// The SQLite database URI. Defaults to a shared in-memory database. Use a
    /// file URI for persistence (e.g. `file:./app.db`). Query parameters like
    /// `?mode=ro` and `?cache=shared` are supported.
    #[clap(
        long,
        default_value = "file::memory:?cache=shared",
        env = "MCP_SQLITE_DATABASE"
    )]
    pub database: String,

    /// Paths to SQL files executed once when creating a new database. Skipped
    /// entirely if the database file already exists. Use this to set up schemas
    /// and seed data on first run. May be specified multiple times on the CLI
    /// or as a comma-separated list in the environment variable.
    #[clap(long, env = "MCP_SQLITE_INIT_SQL", value_delimiter = ',')]
    pub init_sql: Vec<PathBuf>,

    /// The baseline permission preset that determines which SQL
    /// operations are allowed or denied before any --allow / --deny
    /// overrides are applied.
    #[clap(short, long, default_value_t = Preset::ReadOnly, env = "MCP_SQLITE_PRESET")]
    pub preset: Preset,

    /// Allow a specific SQL operation. Accepts a selector in the form Action or
    /// Action(field1.field2) where * is a wildcard. More specific rules
    /// override less specific ones. May be specified multiple times on the CLI
    /// or as a comma-separated list in the environment variable.
    #[clap(short, long, env = "MCP_SQLITE_ALLOW", value_delimiter = ',')]
    pub allow: Vec<AccessControlSelector>,

    /// Deny a specific SQL operation. Same selector syntax as --allow. When an
    /// allow and deny rule match at the same specificity level, deny wins. May
    /// be specified multiple times on the CLI or as a comma-separated list in
    /// the environment variable.
    #[clap(short, long, env = "MCP_SQLITE_DENY", value_delimiter = ',')]
    pub deny: Vec<AccessControlSelector>,

    /// Maximum time in milliseconds that any single SQL operation is allowed to
    /// run before being interrupted. When set, a progress handler is installed
    /// on each connection that aborts queries exceeding this duration. Omit for
    /// no timeout.
    #[clap(long, env = "MCP_SQLITE_TIMEOUT_MS")]
    pub timeout_ms: Option<u64>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_complex_selectors_from_env_vars() {
        // Arrange
        unsafe {
            std::env::set_var("MCP_SQLITE_DATABASE", "file:./prod.db?mode=ro");
            std::env::set_var(
                "MCP_SQLITE_INIT_SQL",
                "schema.sql,seed.sql,migrations/v2.sql",
            );
            std::env::set_var("MCP_SQLITE_PRESET", "deny-everything");
            std::env::set_var(
                "MCP_SQLITE_ALLOW",
                "Read(Students.name),Read(*.id),Insert,Function(count),Select",
            );
            std::env::set_var(
                "MCP_SQLITE_DENY",
                "Read(Secrets.ssn),DropTable,Delete(AuditLog),Read(*.password)",
            );
            std::env::set_var("MCP_SQLITE_TIMEOUT_MS", "30000");
        }

        // Act
        let cli = Cli::try_parse_from(["mcp-server-sqlite"]).unwrap();

        // Assert
        assert_eq!(cli.database, "file:./prod.db?mode=ro");
        assert_eq!(cli.preset, Preset::DenyEverything);
        assert_eq!(cli.timeout_ms, Some(30000));

        assert_eq!(
            cli.init_sql,
            vec![
                PathBuf::from("schema.sql"),
                PathBuf::from("seed.sql"),
                PathBuf::from("migrations/v2.sql"),
            ]
        );

        assert_eq!(cli.allow.len(), 5);
        assert_eq!(cli.allow[0].to_string(), "Read(Students.name)");
        assert_eq!(cli.allow[1].to_string(), "Read(*.id)");
        assert_eq!(cli.allow[2].to_string(), "Insert");
        assert_eq!(cli.allow[3].to_string(), "Function(count)");
        assert_eq!(cli.allow[4].to_string(), "Select");

        assert_eq!(cli.deny.len(), 4);
        assert_eq!(cli.deny[0].to_string(), "Read(Secrets.ssn)");
        assert_eq!(cli.deny[1].to_string(), "DropTable");
        assert_eq!(cli.deny[2].to_string(), "Delete(AuditLog)");
        assert_eq!(cli.deny[3].to_string(), "Read(*.password)");

        // Cleanup
        unsafe {
            std::env::remove_var("MCP_SQLITE_DATABASE");
            std::env::remove_var("MCP_SQLITE_INIT_SQL");
            std::env::remove_var("MCP_SQLITE_PRESET");
            std::env::remove_var("MCP_SQLITE_ALLOW");
            std::env::remove_var("MCP_SQLITE_DENY");
            std::env::remove_var("MCP_SQLITE_TIMEOUT_MS");
        }
    }
}