use crate::config::{
ConnectionConfig, DatabaseEngine, DatabasesConfig, MySqlConnectionConfig,
PostgresConnectionConfig, SslMode, SqliteConnectionConfig,
};
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
use serde_json::{json, Value};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Agent {
ClaudeDesktop,
ClaudeCode,
Cursor,
}
impl Agent {
fn name(&self) -> &'static str {
match self {
Agent::ClaudeDesktop => "Claude Desktop",
Agent::ClaudeCode => "Claude Code",
Agent::Cursor => "Cursor",
}
}
fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"claude-desktop" | "claude_desktop" | "claudedesktop" => Some(Agent::ClaudeDesktop),
"claude-code" | "claude_code" | "claudecode" | "claude" => Some(Agent::ClaudeCode),
"cursor" => Some(Agent::Cursor),
_ => None,
}
}
fn config_path(&self) -> Option<PathBuf> {
let home = dirs::home_dir()?;
match self {
Agent::ClaudeDesktop => {
#[cfg(target_os = "macos")]
{
Some(home.join("Library/Application Support/Claude/claude_desktop_config.json"))
}
#[cfg(target_os = "windows")]
{
Some(home.join("AppData/Roaming/Claude/claude_desktop_config.json"))
}
#[cfg(target_os = "linux")]
{
Some(home.join(".config/claude/claude_desktop_config.json"))
}
}
Agent::ClaudeCode => Some(home.join(".claude.json")),
Agent::Cursor => Some(home.join(".cursor/mcp.json")),
}
}
fn skill_path(&self) -> Option<PathBuf> {
if matches!(self, Agent::ClaudeCode) {
let home = dirs::home_dir()?;
Some(home.join(".claude/skills/sqlx/SKILL.md"))
} else {
None
}
}
}
pub fn run_init(agent_arg: Option<String>) -> anyhow::Result<()> {
println!("\n🔧 SQLx MCP Server - Configuration Wizard\n");
let operations = vec![
"Configure database connections",
"Configure MCP clients",
"Show current status",
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("What would you like to do?")
.items(&operations)
.default(0)
.interact()?;
match selection {
0 => configure_databases()?,
1 => configure_agents(agent_arg)?,
2 => show_status()?,
_ => unreachable!(),
}
Ok(())
}
fn configure_databases() -> anyhow::Result<()> {
println!("\n📦 Database Connection Setup\n");
let mut config = DatabasesConfig::load().unwrap_or_default();
if !config.databases.is_empty() {
println!("Existing connections:");
for conn in &config.databases {
let is_default = config.default_connection.as_ref() == Some(&conn.name().to_string());
let default_marker = if is_default { " (default)" } else { "" };
println!(" • {} ({}){}", conn.name(), conn.engine(), default_marker);
}
println!();
}
loop {
let add_more = if config.databases.is_empty() {
true
} else {
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Add another database connection?")
.default(false)
.interact()?
};
if !add_more {
break;
}
let conn = collect_database_config(&config)?;
let conn_name = conn.name().to_string();
if config.databases.iter().any(|c| c.name() == conn_name) {
println!("⚠️ Connection '{}' already exists. Replacing...", conn_name);
config.databases.retain(|c| c.name() != conn_name);
}
config.databases.push(conn);
if config.databases.len() == 1 {
config.default_connection = Some(conn_name);
} else {
let set_default = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Set as default connection?")
.default(false)
.interact()?;
if set_default {
config.default_connection = Some(config.databases.last().unwrap().name().to_string());
}
}
println!("✓ Connection added successfully\n");
}
if config.databases.is_empty() {
println!("⚠️ No connections configured. Exiting without saving.");
return Ok(());
}
let config_path = select_config_path()?;
config.save(&config_path)?;
println!("\n✅ Configuration saved to: {}", config_path.display());
let configure_ai = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Would you like to configure MCP clients now?")
.default(true)
.interact()?;
if configure_ai {
configure_agents(None)?;
}
Ok(())
}
fn collect_database_config(existing: &DatabasesConfig) -> anyhow::Result<ConnectionConfig> {
let engines = vec!["MySQL", "PostgreSQL", "SQLite"];
let engine_selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select database engine")
.items(&engines)
.default(0)
.interact()?;
let engine = match engine_selection {
0 => DatabaseEngine::MySQL,
1 => DatabaseEngine::Postgres,
2 => DatabaseEngine::SQLite,
_ => unreachable!(),
};
let default_name = format!(
"{}{}",
engine.as_str(),
if existing.databases.is_empty() {
"".to_string()
} else {
format!("_{}", existing.databases.len() + 1)
}
);
let name: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Connection name (unique identifier)")
.default(default_name)
.interact_text()?;
match engine {
DatabaseEngine::MySQL => {
let host: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("MySQL Host")
.default("localhost".to_string())
.interact_text()?;
let port: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("MySQL Port")
.default("3306".to_string())
.interact_text()?;
let username: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("MySQL Username")
.interact_text()?;
let password: String = Password::with_theme(&ColorfulTheme::default())
.with_prompt("MySQL Password")
.interact()?;
let database: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("MySQL Database (optional, press Enter to skip)")
.default("".to_string())
.interact_text()?;
Ok(ConnectionConfig::MySQL(MySqlConnectionConfig {
name,
host,
port: port.parse().unwrap_or(3306),
username,
password,
database: if database.is_empty() {
None
} else {
Some(database)
},
ssl_mode: SslMode::Preferred,
max_connections: 5,
min_connections: 1,
connect_timeout_secs: 30,
}))
}
DatabaseEngine::Postgres => {
let host: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("PostgreSQL Host")
.default("localhost".to_string())
.interact_text()?;
let port: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("PostgreSQL Port")
.default("5432".to_string())
.interact_text()?;
let username: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("PostgreSQL Username")
.default("postgres".to_string())
.interact_text()?;
let password: String = Password::with_theme(&ColorfulTheme::default())
.with_prompt("PostgreSQL Password")
.interact()?;
let database: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("PostgreSQL Database")
.default("postgres".to_string())
.interact_text()?;
Ok(ConnectionConfig::Postgres(PostgresConnectionConfig {
name,
host,
port: port.parse().unwrap_or(5432),
username,
password,
database: if database.is_empty() {
None
} else {
Some(database)
},
ssl_mode: SslMode::Preferred,
max_connections: 5,
min_connections: 1,
connect_timeout_secs: 30,
}))
}
DatabaseEngine::SQLite => {
let path: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("SQLite database path (or ':memory:' for in-memory)")
.interact_text()?;
Ok(ConnectionConfig::SQLite(SqliteConnectionConfig {
name,
path,
max_connections: 1,
}))
}
}
}
fn select_config_path() -> anyhow::Result<PathBuf> {
let paths = DatabasesConfig::config_paths();
let path_strings: Vec<String> = paths.iter().map(|p| p.display().to_string()).collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Where to save configuration?")
.items(&path_strings)
.default(0)
.interact()?;
Ok(paths[selection].clone())
}
fn configure_agents(agent_arg: Option<String>) -> anyhow::Result<()> {
println!("\n🤖 MCP Client Configuration\n");
let selected_agents: Vec<Agent> = match agent_arg {
Some(agent_name) => match Agent::from_str(&agent_name) {
Some(agent) => vec![agent],
None => {
eprintln!("❌ Unknown agent: {}", agent_name);
eprintln!();
eprintln!("Available agents:");
eprintln!(" claude-desktop - Claude Desktop app");
eprintln!(" claude-code - Claude Code CLI");
eprintln!(" cursor - Cursor editor");
std::process::exit(1);
}
},
None => {
let agents = vec!["Claude Desktop", "Claude Code", "Cursor", "All agents"];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select MCP client to configure")
.items(&agents)
.default(0)
.interact()?;
match selection {
0 => vec![Agent::ClaudeDesktop],
1 => vec![Agent::ClaudeCode],
2 => vec![Agent::Cursor],
3 => vec![Agent::ClaudeDesktop, Agent::ClaudeCode, Agent::Cursor],
_ => unreachable!(),
}
}
};
let binary_path = get_binary_path()?;
let mut successful = Vec::new();
let mut failed = Vec::new();
for agent in &selected_agents {
match configure_agent(*agent, &binary_path) {
Ok(_) => successful.push(agent),
Err(e) => {
eprintln!(" ⚠️ Failed to configure {}: {}", agent.name(), e);
failed.push((agent, e));
}
};
}
println!();
if !successful.is_empty() {
println!("✅ Successfully configured:");
for agent in &successful {
if let Some(path) = agent.config_path() {
println!(" • {} → {}", agent.name(), path.display());
}
}
}
if !failed.is_empty() {
println!();
println!("⚠️ Failed to configure:");
for (agent, _) in &failed {
println!(" • {}", agent.name());
}
}
if failed.is_empty() {
println!("\n✅ MCP client configuration complete!\n");
} else {
println!("\n⚠️ Configuration completed with {} error(s)\n", failed.len());
}
println!("Restart your MCP client to apply changes.\n");
Ok(())
}
fn get_binary_path() -> anyhow::Result<String> {
let current_exe = std::env::current_exe()?;
let exe_str = current_exe.to_string_lossy().to_string();
let default_path = if exe_str.contains("target/debug") {
exe_str.replace("target/debug", "target/release")
} else {
exe_str
};
let path: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Binary path")
.default(default_path)
.interact_text()?;
Ok(path)
}
fn configure_agent(agent: Agent, binary_path: &str) -> anyhow::Result<()> {
println!("\n📝 Configuring {}...", agent.name());
let config_path = agent
.config_path()
.ok_or_else(|| anyhow::anyhow!("Could not determine config path for {}", agent.name()))?;
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
let db_config_path = DatabasesConfig::config_paths()
.into_iter()
.find(|p| p.exists())
.map(|p| p.display().to_string())
.unwrap_or_else(|| ".databases.json".to_string());
let mcp_config = json!({
"command": binary_path,
"env": {
"SQLX_MCP_CONFIG": db_config_path,
"RUST_LOG": "info"
}
});
let mut root_config: Value = if config_path.exists() {
let content = fs::read_to_string(&config_path)?;
serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
} else {
json!({})
};
if root_config.get("mcpServers").is_none() {
root_config["mcpServers"] = json!({});
}
root_config["mcpServers"]["sqlx"] = mcp_config;
let formatted = serde_json::to_string_pretty(&root_config)?;
fs::write(&config_path, formatted)?;
println!(" ✓ Config written to: {}", config_path.display());
if let Some(skill_path) = agent.skill_path() {
if let Some(parent) = skill_path.parent() {
fs::create_dir_all(parent)?;
}
let skill_content = generate_skill_content();
fs::write(&skill_path, skill_content)?;
println!(" ✓ Skill written to: {}", skill_path.display());
}
Ok(())
}
fn generate_skill_content() -> String {
r#"---
description: Execute database operations via MCP (MySQL, PostgreSQL, SQLite)
tags: [database, sql, crud, mcp]
---
# SQLx MCP
Multi-database operations via MCP tools (MySQL, PostgreSQL, SQLite).
## Tools
| Tool | Use For |
|------|---------|
| `db_query` | SELECT queries |
| `db_insert` | INSERT records |
| `db_update` | UPDATE records |
| `db_delete` | DELETE records |
| `db_list_tables` | List tables |
| `db_describe_table` | Table schema |
| `db_list_connections` | Show connections |
| `db_health_check` | Test connectivity |
## Examples
| Request | Tool | JSON |
|---------|------|------|
| Get active users from mysql_main | `db_query` | `{"query":"SELECT * FROM users WHERE status = ?","params":["active"],"connection":"mysql_main"}` |
| Show last 10 orders from postgres | `db_query` | `{"query":"SELECT * FROM orders ORDER BY created_at DESC LIMIT ?","params":[10],"connection":"postgres"}` |
| List tables in sqlite_cache | `db_list_tables` | `{"connection":"sqlite_cache"}` |
| Describe users table in mydb | `db_describe_table` | `{"table":"users","connection":"mydb"}` |
| Add user John to mysql_main | `db_insert` | `{"query":"INSERT INTO users (name) VALUES (?)","params":["John"],"connection":"mysql_main"}` |
| Update user id=5 to Jane in postgres | `db_update` | `{"query":"UPDATE users SET name = ? WHERE id = ?","params":["Jane",5],"connection":"postgres"}` |
| Delete order id=10 from mydb | `db_delete` | `{"query":"DELETE FROM orders WHERE id = ?","params":[10],"connection":"mydb"}` |
## Workflow
1. **Parse** → Extract connection name, operation, table, conditions
2. **Tool** → query/insert/update/delete based on intent
3. **Build** → Use `?` placeholders, pass values via `params` array
4. **Execute** → Report results or affected rows
## Connection Detection
Extract connection name from user request. If omitted, use default. Unclear? List connections first.
## Parameters
- **query/insert/update/delete**: `{"query":"SQL ? placeholders","params":[],"connection":"name"}`
- **list_tables/health_check**: `{"connection":"name"}`
- **describe_table**: `{"table":"name","connection":"name"}`
## Security
- `?` placeholders required for dynamic values
- Blocked: DROP, TRUNCATE, ALTER, CREATE, GRANT
- Single statement only (no semicolons)
"#
.to_string()
}
pub fn show_status() -> anyhow::Result<()> {
println!("\n📊 SQLx MCP Configuration Status\n");
println!("Database Configuration:");
let paths = DatabasesConfig::config_paths();
let mut found_config = false;
for path in &paths {
if path.exists() {
found_config = true;
println!(" ✅ Found: {}", path.display());
if let Ok(config) = DatabasesConfig::load() {
println!(" Connections: {}", config.databases.len());
for conn in &config.databases {
let is_default =
config.default_connection.as_ref() == Some(&conn.name().to_string());
let default_marker = if is_default { " (default)" } else { "" };
println!(" • {} ({}){}", conn.name(), conn.engine(), default_marker);
}
}
break;
}
}
if !found_config {
println!(" ❌ No configuration found");
println!(" Expected locations:");
for path in &paths {
println!(" • {}", path.display());
}
}
println!("\nMCP Client Configuration:");
for agent in [Agent::ClaudeDesktop, Agent::ClaudeCode, Agent::Cursor] {
print!(" {} ", agent.name());
let padding = 16 - agent.name().len();
print!("{}", " ".repeat(padding));
if let Some(path) = agent.config_path() {
if path.exists() {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(config) = serde_json::from_str::<Value>(&content) {
if config
.get("mcpServers")
.and_then(|s| s.get("sqlx"))
.is_some()
{
println!("✅ Configured");
println!(" └─ {}", path.display());
continue;
}
}
}
println!("⚠️ Config exists but SQLx not configured");
println!(" └─ {}", path.display());
} else {
println!("❌ Not configured");
println!(" └─ {}", path.display());
}
} else {
println!("⚠️ Unknown config path");
}
}
println!();
println!("Run 'sqlx-mcp --init' to configure.");
println!();
Ok(())
}