sqlx-mcp 0.1.0

SQLx MCP Server - Secure multi-database CRUD operations via Model Context Protocol
//! SQLx MCP Server
//!
//! A Model Context Protocol server providing secure database operations
//! for MySQL, PostgreSQL, and SQLite.
//!
//! ## Features
//! - Multi-database engine support (MySQL, PostgreSQL, SQLite)
//! - Multiple named connections via .databases.json
//! - CRUD operations via MCP tools
//! - Parameterized queries (SQL injection prevention)
//! - Connection pooling per database
//! - Security validation
//!
//! ## Usage
//!
//! ### Run as MCP Server
//! ```bash
//! sqlx-mcp
//! ```
//!
//! ### Initialize Configuration
//! ```bash
//! sqlx-mcp --init                  # Interactive configuration wizard
//! sqlx-mcp --init claude-desktop   # Configure Claude Desktop only
//! sqlx-mcp --init claude-code      # Configure Claude Code only
//! sqlx-mcp --init cursor           # Configure Cursor only
//! sqlx-mcp --status                # Show configuration status
//! ```
//!
//! ## Configuration
//!
//! Create a `.databases.json` file in the current directory or `~/.config/sqlx-mcp/`:
//!
//! ```json
//! {
//!   "version": "1.0",
//!   "databases": [
//!     {"name": "mydb", "engine": "mysql", "host": "localhost", "port": 3306, "username": "user", "password": "pass"},
//!     {"name": "cache", "engine": "sqlite", "path": "/data/cache.db"}
//!   ],
//!   "default_connection": "mydb"
//! }
//! ```

mod config;
mod db;
mod error;
mod init;
mod server;

use clap::Parser;
use config::DatabasesConfig;
use db::ConnectionManager;
use rmcp::transport::stdio;
use rmcp::ServiceExt;
use server::SqlxMcpServer;
use tracing::{error, info};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

/// SQLx MCP Server - Multi-database operations via Model Context Protocol
#[derive(Parser, Debug)]
#[command(name = "sqlx-mcp")]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Initialize configuration for databases and MCP clients
    ///
    /// Without argument: interactive configuration wizard
    /// With argument: configure specific agent (claude-desktop, claude-code, cursor)
    #[arg(long, value_name = "AGENT")]
    init: Option<Option<String>>,

    /// Show current configuration status
    #[arg(long)]
    status: bool,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();

    // Handle --status command
    if args.status {
        return init::show_status();
    }

    // Handle --init command
    if let Some(agent_arg) = args.init {
        return init::run_init(agent_arg);
    }

    // Normal MCP server mode
    run_server().await
}

async fn run_server() -> anyhow::Result<()> {
    // Load .env file if present
    let _ = dotenvy::dotenv();

    // Initialize tracing/logging
    // Use stderr for logs to keep stdout clean for MCP protocol
    init_logging();

    info!("Starting SQLx MCP Server v{}", env!("CARGO_PKG_VERSION"));

    // Load configuration from .databases.json
    let config = match DatabasesConfig::load() {
        Ok(cfg) => {
            info!(
                "Loaded configuration with {} database(s)",
                cfg.databases.len()
            );
            cfg
        }
        Err(e) => {
            error!("Configuration error: {}", e);
            eprintln!("Error: {}", e);
            eprintln!();
            eprintln!("SQLx MCP Server requires a .databases.json configuration file.");
            eprintln!();
            eprintln!("Expected locations:");
            for path in DatabasesConfig::config_paths() {
                eprintln!("  - {}", path.display());
            }
            eprintln!();
            eprintln!("Example .databases.json:");
            eprintln!(r#"{{
  "version": "1.0",
  "databases": [
    {{
      "name": "mydb",
      "engine": "mysql",
      "host": "localhost",
      "port": 3306,
      "username": "root",
      "password": "secret",
      "database": "myapp"
    }},
    {{
      "name": "cache",
      "engine": "sqlite",
      "path": "/tmp/cache.db"
    }}
  ],
  "default_connection": "mydb"
}}"#);
            eprintln!();
            eprintln!("Tip: Run 'sqlx-mcp --init' for interactive setup.");
            std::process::exit(1);
        }
    };

    // Initialize connection manager
    let conn_manager = match ConnectionManager::new(&config).await {
        Ok(cm) => {
            info!(
                "Connection manager initialized with {} connection(s)",
                cm.connection_count()
            );
            cm
        }
        Err(e) => {
            error!("Failed to initialize connections: {}", e);
            eprintln!("Error: Failed to connect to database(s): {}", e);
            eprintln!();
            eprintln!("Please check your .databases.json configuration.");
            std::process::exit(1);
        }
    };

    // Create and run MCP server
    let server = SqlxMcpServer::new(conn_manager);

    info!("MCP server ready, waiting for connections...");

    // Serve via stdio transport
    let service = server.serve(stdio()).await.inspect_err(|e| {
        error!("Error starting MCP server: {}", e);
    })?;

    // Wait for service to complete
    service.waiting().await?;

    info!("SQLx MCP Server shutting down");
    Ok(())
}

/// Initialize logging with environment filter
fn init_logging() {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));

    // Write logs to stderr to keep stdout clean for MCP protocol
    tracing_subscriber::registry()
        .with(filter)
        .with(fmt::layer().with_writer(std::io::stderr))
        .init();
}