use std::path::PathBuf;
use clap::Parser;
use crate::access_control::{AccessControlSelector, Preset};
#[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 {
#[clap(
long,
default_value = "file::memory:?cache=shared",
env = "MCP_SQLITE_DATABASE"
)]
pub database: String,
#[clap(long, env = "MCP_SQLITE_INIT_SQL", value_delimiter = ',')]
pub init_sql: Vec<PathBuf>,
#[clap(short, long, default_value_t = Preset::ReadOnly, env = "MCP_SQLITE_PRESET")]
pub preset: Preset,
#[clap(short, long, env = "MCP_SQLITE_ALLOW", value_delimiter = ',')]
pub allow: Vec<AccessControlSelector>,
#[clap(short, long, env = "MCP_SQLITE_DENY", value_delimiter = ',')]
pub deny: Vec<AccessControlSelector>,
#[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() {
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");
}
let cli = Cli::try_parse_from(["mcp-server-sqlite"]).unwrap();
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)");
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");
}
}
}