use std::collections::HashMap;
use sqlserver_mcp_catalog::core::config_schema::{AuthMethod, Transport};
use sqlserver_mcp_catalog::core::credential_storage::save_credential;
fn to_env_key(key: &str) -> String {
key.to_uppercase()
}
async fn prompt_base_url() -> anyhow::Result<String> {
tokio::task::spawn_blocking(|| {
inquire::Text::new("SQL Server host (or host:port -- defaults to port 1433 if omitted):")
.prompt()
})
.await?
.map_err(anyhow::Error::from)
}
async fn prompt_auth_method() -> anyhow::Result<AuthMethod> {
let choices = vec!["sql_server", "windows", "azure_ad"];
let selection = tokio::task::spawn_blocking(move || {
inquire::Select::new("Authentication method:", choices).prompt()
})
.await??;
Ok(match selection {
"sql_server" => AuthMethod::SqlServer,
"windows" => AuthMethod::Windows,
"azure_ad" => AuthMethod::AzureAd,
other => anyhow::bail!("unexpected auth method selection '{other}'"),
})
}
async fn prompt_api_version() -> anyhow::Result<String> {
let choices = vec!["2025 (default/latest)", "2022", "2019", "2017"];
let selection = tokio::task::spawn_blocking(move || {
inquire::Select::new("API version to use:", choices).prompt()
})
.await??;
Ok(match selection {
"2025 (default/latest)" => "2025".to_string(),
"2022" => "2022".to_string(),
"2019" => "2019".to_string(),
"2017" => "2017".to_string(),
other => other.to_string(),
})
}
async fn prompt_transport() -> anyhow::Result<Transport> {
let choices = vec![
"stdio (spawned as a subprocess by the MCP client)",
"http (a standalone server the MCP client connects to over the network)",
];
let selection = tokio::task::spawn_blocking(move || {
inquire::Select::new("Which transport will this deployment use?", choices).prompt()
})
.await??;
Ok(if selection.starts_with("stdio") {
Transport::Stdio
} else {
Transport::Http
})
}
async fn prompt_credentials(auth_method: AuthMethod) -> anyhow::Result<HashMap<String, String>> {
tokio::task::spawn_blocking(move || -> anyhow::Result<HashMap<String, String>> {
let mut credentials = HashMap::new();
match auth_method {
AuthMethod::SqlServer => {
credentials.insert(
"username".to_string(),
inquire::Text::new("SQL login username:").prompt()?,
);
credentials.insert(
"password".to_string(),
inquire::Password::new("Password:")
.without_confirmation()
.prompt()?,
);
}
AuthMethod::Windows => {
credentials.insert(
"username".to_string(),
inquire::Text::new("Windows username (optionally DOMAIN\\user):").prompt()?,
);
credentials.insert(
"password".to_string(),
inquire::Password::new("Password:")
.without_confirmation()
.prompt()?,
);
}
AuthMethod::AzureAd => {
credentials.insert(
"client_id".to_string(),
inquire::Text::new("Azure AD application (client) ID:").prompt()?,
);
credentials.insert(
"client_secret".to_string(),
inquire::Password::new("Azure AD client secret:")
.without_confirmation()
.prompt()?,
);
let tenant_id = inquire::Text::new("Azure AD tenant ID:").prompt()?;
if !tenant_id.is_empty() {
credentials.insert("tenant_id".to_string(), tenant_id);
}
}
}
Ok(credentials)
})
.await?
}
async fn write_config_yaml(global: bool, config_fields: &serde_json::Value) -> anyhow::Result<()> {
let yaml = serde_yaml::to_string(config_fields)?;
let path = if global {
let dir = sqlserver_mcp_catalog::core::credential_storage::resolve_home_dir()
.join(".sqlserver-mcp");
std::fs::create_dir_all(&dir)?;
dir.join("config.yml")
} else {
std::path::PathBuf::from("sqlserver-mcp.config.yml")
};
std::fs::write(&path, yaml)?;
println!("Wrote {}", path.display());
println!(
"Credentials are never written to this file — they're stored via the OS \
keychain/encrypted-file fallback from the credential prompt above."
);
Ok(())
}
async fn prompt_persistence(
env: &HashMap<String, String>,
config_fields: &serde_json::Value,
) -> anyhow::Result<()> {
let choices = vec![
"Write a .env file",
"Write a config.yml file (global — ~/.sqlserver-mcp/config.yml, read on every invocation on this machine)",
"Write a config.yml file (local — ./sqlserver-mcp.config.yml, read only from this directory)",
"Print a ready-to-run CLI invocation (nothing written to disk)",
];
let selection = tokio::task::spawn_blocking(move || {
inquire::Select::new("How should these settings be saved?", choices).prompt()
})
.await??;
match selection {
"Write a .env file" => {
let contents = env
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(".env", format!("{contents}\n"))?;
println!("Wrote .env");
}
s if s.starts_with("Write a config.yml file (global") => {
write_config_yaml(true, config_fields).await?;
}
s if s.starts_with("Write a config.yml file (local") => {
write_config_yaml(false, config_fields).await?;
}
_ => {
let flags = env
.iter()
.map(|(key, value)| {
format!("--{} {:?}", key.to_lowercase().replace('_', "-"), value)
})
.collect::<Vec<_>>()
.join(" ");
println!("sqlserver-mcp start {flags}");
}
}
Ok(())
}
fn print_mcp_client_config(transport: Transport, env: &HashMap<String, String>) {
match transport {
Transport::Stdio => {
let config = serde_json::json!({
"mcpServers": {
"sqlserver-mcp": {
"command": "sqlserver-mcp",
"args": ["start"],
"env": env,
}
}
});
println!("\nMCP client config (stdio):");
println!("{}", serde_json::to_string_pretty(&config).unwrap());
let cli_args = env
.iter()
.map(|(key, value)| format!("--{} {value:?}", key.to_lowercase().replace('_', "-")))
.collect::<Vec<_>>()
.join(" ");
println!("(equivalently: `sqlserver-mcp start {cli_args}`)");
}
Transport::Http => {
let config = serde_json::json!({
"mcpServers": {
"sqlserver-mcp": {
"url": "http://127.0.0.1:3000/mcp",
}
}
});
println!(
"\nMCP client config (http) — adjust the host/port to match how you run \
`sqlserver-mcp http`:"
);
println!("{}", serde_json::to_string_pretty(&config).unwrap());
println!(
"This server connects to SQL Server with its own configured credentials \
(set via env/config/keychain on the machine running it) — every caller who \
reaches this HTTP endpoint gets the same DB access. Put it behind your own \
network-level access control (reverse proxy, VPN, firewall rule) if it's \
reachable beyond localhost."
);
}
}
}
pub async fn run_setup_wizard() -> anyhow::Result<()> {
let url = prompt_base_url().await?;
let auth_method = prompt_auth_method().await?;
let credentials = prompt_credentials(auth_method).await?;
let api_version = prompt_api_version().await?;
let transport = prompt_transport().await?;
save_credential("active-credentials", &serde_json::to_string(&credentials)?)?;
let mut env: HashMap<String, String> = HashMap::new();
env.insert("SQLSERVER_URL".to_string(), url);
env.insert(
"SQLSERVER_AUTH_METHOD".to_string(),
serde_json::to_value(auth_method)?
.as_str()
.unwrap_or_default()
.to_string(),
);
env.insert("SQLSERVER_API_VERSION".to_string(), api_version);
env.insert(
"SQLSERVER_TRANSPORT".to_string(),
serde_json::to_value(transport)?
.as_str()
.unwrap_or_default()
.to_string(),
);
for (key, value) in &credentials {
env.insert(format!("SQLSERVER_{}", to_env_key(key)), value.clone());
}
let config_fields = serde_json::json!({
"url": env.get("SQLSERVER_URL"),
"auth_method": env.get("SQLSERVER_AUTH_METHOD"),
"api_version": env.get("SQLSERVER_API_VERSION"),
"transport": env.get("SQLSERVER_TRANSPORT"),
});
prompt_persistence(&env, &config_fields).await?;
print_mcp_client_config(transport, &env);
let run_command = match transport {
Transport::Stdio => "start",
Transport::Http => "http",
};
println!("Setup complete! Run: sqlserver-mcp {run_command}");
Ok(())
}