use clap::{Parser, Subcommand};
use sequel_mcp::config::ConfigStore;
#[derive(Parser)]
#[command(
name = "sequel-mcp",
version = sequel_mcp::PACKAGE_VERSION,
about = "Local-first, policy-gated MySQL/MariaDB and SQLite access for AI agents"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Serve,
Doctor {
#[arg(long)]
json: bool,
},
Approve {
#[arg(long)]
socket: Option<String>,
#[arg(long)]
choice: Option<String>,
},
Gui {
#[arg(long)]
socket: Option<String>,
#[arg(long, hide = true)]
smoke: Option<u32>,
},
}
fn main() {
let cli = Cli::parse();
let code = match cli.command {
Command::Serve => serve(),
Command::Doctor { json } => doctor(json),
Command::Approve { socket, choice } => approve(socket, choice),
Command::Gui { socket, smoke } => sequel_mcp::gui::run_gui(sequel_mcp::gui::GuiOptions {
socket: socket
.map(std::path::PathBuf::from)
.unwrap_or_else(sequel_mcp::gui::default_socket),
smoke_frames: smoke,
}),
};
std::process::exit(code);
}
fn serve() -> i32 {
if let Err(e) = sequel_mcp::app::test_mode::verify_startup() {
eprintln!("sequel-mcp: refusing to start (test mode): {e}");
return sequel_mcp::app::test_mode::EXIT_ISOLATION;
}
if sequel_mcp::app::test_mode::is_active()
&& let Ok(root) = std::env::var("SEQUEL_MCP_TEST_ROOT")
{
eprintln!("sequel-mcp: test mode active, isolated root {root}");
}
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let rt = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
eprintln!("sequel-mcp: failed to start runtime: {e}");
return 1;
}
};
rt.block_on(async {
use rmcp::ServiceExt;
let server = sequel_mcp::mcp::SequelServer::with_approval_ipc();
match server.serve(sequel_mcp::mcp::limits::limited_stdio()).await {
Ok(service) => {
eprintln!("sequel-mcp ready");
if let Err(e) = service.waiting().await {
eprintln!("sequel-mcp: service error: {e}");
return 1;
}
0
}
Err(e) => {
eprintln!("sequel-mcp: serve failed: {e}");
1
}
}
})
}
fn doctor(json: bool) -> i32 {
let store = ConfigStore::new();
let cfg = match store.load() {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("sequel-mcp doctor: config load failed: {e}");
return 1;
}
};
if json {
let report = serde_json::json!({
"app": sequel_mcp::PACKAGE_NAME,
"version": sequel_mcp::PACKAGE_VERSION,
"configPath": store.path().display().to_string(),
"configVersion": cfg.version,
"configRevision": cfg.revision,
"connectionCount": cfg.connections.len(),
"defaultConnection": cfg.default_connection,
});
println!("{}", serde_json::to_string_pretty(&report).unwrap());
} else {
println!(
"{} v{}",
sequel_mcp::PACKAGE_NAME,
sequel_mcp::PACKAGE_VERSION
);
println!(
"config : {} (v{}, revision {})",
store.path().display(),
cfg.version,
cfg.revision
);
println!(
"connections: {} (default: {})",
cfg.connections.len(),
cfg.default_connection.as_deref().unwrap_or("(none)")
);
}
0
}
fn approve(socket: Option<String>, choice: Option<String>) -> i32 {
use std::io::BufRead;
let path = socket
.map(std::path::PathBuf::from)
.unwrap_or_else(sequel_mcp::approval::ipc::ApprovalIpc::default_socket_path);
let Ok(mut stream) = std::os::unix::net::UnixStream::connect(&path) else {
eprintln!(
"sequel-mcp approve: no server at {} (is a server running?)",
path.display()
);
return 1;
};
fn say(stream: &mut std::os::unix::net::UnixStream, v: &serde_json::Value) {
use std::io::Write;
let _ = stream.write_all(serde_json::to_string(v).unwrap_or_default().as_bytes());
let _ = stream.write_all(b"\n");
let _ = stream.flush();
}
fn read(stream: &std::os::unix::net::UnixStream) -> Option<String> {
let mut reader = std::io::BufReader::new(stream.try_clone().ok()?);
let mut line = String::new();
reader.read_line(&mut line).ok()?;
let line = line.trim().to_string();
if line.is_empty() { None } else { Some(line) }
}
say(
&mut stream,
&serde_json::json!({"op": "wait", "timeoutMs": 30000}),
);
let Some(line) = read(&stream) else {
eprintln!("sequel-mcp approve: connection closed before a request arrived");
return 1;
};
let Ok(msg) = serde_json::from_str::<serde_json::Value>(&line) else {
eprintln!("sequel-mcp approve: malformed reply from server");
return 1;
};
if msg["op"] == "empty" {
eprintln!("sequel-mcp approve: no pending approval (timed out waiting)");
return 1;
}
if msg["op"] != "request" {
eprintln!("sequel-mcp approve: unexpected reply: {msg}");
return 1;
}
let req = &msg["request"];
println!("pending approval");
println!(" category : {}", req["category"].as_str().unwrap_or("?"));
println!(
" connection: {}",
req["connection"].as_str().unwrap_or("?")
);
if let Some(db) = req["database"].as_str() {
println!(" database : {db}");
}
let tables: Vec<&str> = req["tables"]
.as_array()
.map(|a| a.iter().filter_map(|t| t.as_str()).collect())
.unwrap_or_default();
if !tables.is_empty() {
println!(" tables : {}", tables.join(", "));
}
println!(" statement :");
for line_text in req["snippet"].as_str().unwrap_or("").lines() {
println!(" {line_text}");
}
let choice = match choice.as_deref() {
Some(c @ ("once" | "session" | "decline")) => c.to_string(),
Some(other) => {
eprintln!("sequel-mcp approve: invalid --choice {other:?} (once|session|decline)");
return 2;
}
None => loop {
println!("authorize? [once/session/decline]");
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_err() {
return 1;
}
match input.trim() {
"once" | "session" | "decline" => break input.trim().to_string(),
"" => {
eprintln!("sequel-mcp approve: EOF — treating as decline");
break "decline".to_string();
}
_ => continue,
}
},
};
say(
&mut stream,
&serde_json::json!({
"op": "reply",
"id": req["id"],
"choice": choice,
}),
);
match read(&stream)
.as_deref()
.map(serde_json::from_str::<serde_json::Value>)
{
Some(Ok(ack)) if ack["op"] == "ok" => {
println!("recorded: {choice}");
0
}
Some(Ok(ack)) => {
eprintln!("sequel-mcp approve: server rejected the reply: {ack}");
1
}
_ => {
eprintln!("sequel-mcp approve: no acknowledgement from server");
1
}
}
}