1use std::path::PathBuf;
4
5use clap::Parser;
6
7use crate::access_control::{AccessControlSelector, Preset};
8
9#[derive(Parser)]
25#[command(
26 author,
27 version,
28 about = "MCP server for SQLite with fine-grained access control",
29 term_width = 80,
30 after_long_help = "\
31EXAMPLES:
32 Read-only server (default) with in-memory database:
33 mcp-server-sqlite
34
35 Read-only on a persistent database with schema init:
36 mcp-server-sqlite --database ./app.db --init-sql schema.sql
37
38 Read-write server that blocks one table:
39 mcp-server-sqlite --preset read-write --deny Delete(AuditLog)
40
41 Read-only with a carve-out denying sensitive columns:
42 mcp-server-sqlite --database ./app.db --deny Read(Users.ssn)
43
44 Deny-everything baseline, selectively allowing reads:
45 mcp-server-sqlite --preset deny-everything \\
46 --allow Read --allow Function(count)"
47)]
48pub struct Cli {
49 #[clap(
53 long,
54 default_value = "file::memory:?cache=shared",
55 env = "MCP_SQLITE_DATABASE"
56 )]
57 pub database: String,
58
59 #[clap(long, env = "MCP_SQLITE_INIT_SQL", value_delimiter = ',')]
64 pub init_sql: Vec<PathBuf>,
65
66 #[clap(short, long, default_value_t = Preset::ReadOnly, env = "MCP_SQLITE_PRESET")]
70 pub preset: Preset,
71
72 #[clap(short, long, env = "MCP_SQLITE_ALLOW", value_delimiter = ',')]
77 pub allow: Vec<AccessControlSelector>,
78
79 #[clap(short, long, env = "MCP_SQLITE_DENY", value_delimiter = ',')]
84 pub deny: Vec<AccessControlSelector>,
85
86 #[clap(long, env = "MCP_SQLITE_TIMEOUT_MS")]
91 pub timeout_ms: Option<u64>,
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn parses_complex_selectors_from_env_vars() {
100 unsafe {
102 std::env::set_var("MCP_SQLITE_DATABASE", "file:./prod.db?mode=ro");
103 std::env::set_var(
104 "MCP_SQLITE_INIT_SQL",
105 "schema.sql,seed.sql,migrations/v2.sql",
106 );
107 std::env::set_var("MCP_SQLITE_PRESET", "deny-everything");
108 std::env::set_var(
109 "MCP_SQLITE_ALLOW",
110 "Read(Students.name),Read(*.id),Insert,Function(count),Select",
111 );
112 std::env::set_var(
113 "MCP_SQLITE_DENY",
114 "Read(Secrets.ssn),DropTable,Delete(AuditLog),Read(*.password)",
115 );
116 std::env::set_var("MCP_SQLITE_TIMEOUT_MS", "30000");
117 }
118
119 let cli = Cli::try_parse_from(["mcp-server-sqlite"]).unwrap();
121
122 assert_eq!(cli.database, "file:./prod.db?mode=ro");
124 assert_eq!(cli.preset, Preset::DenyEverything);
125 assert_eq!(cli.timeout_ms, Some(30000));
126
127 assert_eq!(
128 cli.init_sql,
129 vec![
130 PathBuf::from("schema.sql"),
131 PathBuf::from("seed.sql"),
132 PathBuf::from("migrations/v2.sql"),
133 ]
134 );
135
136 assert_eq!(cli.allow.len(), 5);
137 assert_eq!(cli.allow[0].to_string(), "Read(Students.name)");
138 assert_eq!(cli.allow[1].to_string(), "Read(*.id)");
139 assert_eq!(cli.allow[2].to_string(), "Insert");
140 assert_eq!(cli.allow[3].to_string(), "Function(count)");
141 assert_eq!(cli.allow[4].to_string(), "Select");
142
143 assert_eq!(cli.deny.len(), 4);
144 assert_eq!(cli.deny[0].to_string(), "Read(Secrets.ssn)");
145 assert_eq!(cli.deny[1].to_string(), "DropTable");
146 assert_eq!(cli.deny[2].to_string(), "Delete(AuditLog)");
147 assert_eq!(cli.deny[3].to_string(), "Read(*.password)");
148
149 unsafe {
151 std::env::remove_var("MCP_SQLITE_DATABASE");
152 std::env::remove_var("MCP_SQLITE_INIT_SQL");
153 std::env::remove_var("MCP_SQLITE_PRESET");
154 std::env::remove_var("MCP_SQLITE_ALLOW");
155 std::env::remove_var("MCP_SQLITE_DENY");
156 std::env::remove_var("MCP_SQLITE_TIMEOUT_MS");
157 }
158 }
159}