use std::collections::HashMap;
use std::time::Duration;
use github_mcp::core::config_schema::{AuthMethod, Transport};
use github_mcp::core::credential_storage::save_credential;
fn to_env_key(key: &str) -> String {
key.to_uppercase()
}
async fn prompt_base_url() -> anyhow::Result<String> {
let url =
tokio::task::spawn_blocking(|| inquire::Text::new("GitHub v3 REST API base URL:").prompt())
.await??;
let client = reqwest::Client::new();
match client
.head(&url)
.timeout(Duration::from_secs(5))
.send()
.await
{
Ok(_) => println!("reachable"),
Err(_) => println!(
"could not reach that URL yet — continuing anyway (fix it later with `test-connection`)"
),
}
Ok(url)
}
async fn prompt_auth_method() -> anyhow::Result<AuthMethod> {
let choices = vec!["pat", "apiKey", "basic"];
let selection = tokio::task::spawn_blocking(move || {
inquire::Select::new("Authentication method:", choices).prompt()
})
.await??;
Ok(match selection {
"pat" => AuthMethod::Pat,
"apiKey" => AuthMethod::ApiKey,
"basic" => AuthMethod::Basic,
other => anyhow::bail!("unexpected auth method selection '{other}'"),
})
}
async fn prompt_api_version() -> anyhow::Result<String> {
let choices = vec![
"gh-2026-03-10 (default/latest)",
"ghec-2026-03-10",
"ghes-3.21",
"ghes-3.20",
"ghes-3.19",
"ghes-2.22",
];
let selection = tokio::task::spawn_blocking(move || {
inquire::Select::new("API version to use:", choices).prompt()
})
.await??;
Ok(match selection {
"gh-2026-03-10 (default/latest)" => "gh-2026-03-10".to_string(),
"ghec-2026-03-10" => "ghec-2026-03-10".to_string(),
"ghes-3.21" => "ghes-3.21".to_string(),
"ghes-3.20" => "ghes-3.20".to_string(),
"ghes-3.19" => "ghes-3.19".to_string(),
"ghes-2.22" => "ghes-2.22".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::Basic => {
credentials.insert(
"username".to_string(),
inquire::Text::new("Username:").prompt()?,
);
credentials.insert(
"password".to_string(),
inquire::Password::new("Password:")
.without_confirmation()
.prompt()?,
);
}
AuthMethod::Pat => {
credentials.insert(
"token".to_string(),
inquire::Password::new("Personal access token:")
.without_confirmation()
.prompt()?,
);
}
AuthMethod::ApiKey => {
credentials.insert(
"api_key".to_string(),
inquire::Password::new("API key:")
.without_confirmation()
.prompt()?,
);
}
}
Ok(credentials)
})
.await?
}
async fn prompt_persistence(env: &HashMap<String, String>) -> anyhow::Result<()> {
let choices = vec![
"Write a .env file",
"Write a config.json file",
"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");
}
"Write a config.json file" => {
std::fs::write("config.json", serde_json::to_string_pretty(env)?)?;
println!("Wrote config.json");
}
_ => {
let flags = env
.iter()
.map(|(key, value)| {
format!("--{} {:?}", key.to_lowercase().replace('_', "-"), value)
})
.collect::<Vec<_>>()
.join(" ");
println!("github-mcp start {flags}");
}
}
Ok(())
}
fn print_mcp_client_config(
transport: Transport,
auth_method: AuthMethod,
env: &HashMap<String, String>,
) {
match transport {
Transport::Stdio => {
let config = serde_json::json!({
"mcpServers": {
"github-mcp": {
"command": "github-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: `github-mcp start {cli_args}`)");
}
Transport::Http => {
let (_, header_name) = github_mcp::auth::auth_manager::header_location_for(auth_method);
let mut headers = serde_json::Map::new();
headers.insert(
header_name.to_string(),
serde_json::Value::String("<credential value here>".to_string()),
);
let config = serde_json::json!({
"mcpServers": {
"github-mcp": {
"url": "http://127.0.0.1:3000/mcp",
"headers": headers,
}
}
});
println!(
"\nMCP client config (http) — adjust the host/port to match how you run \
`github-mcp http`:"
);
println!("{}", serde_json::to_string_pretty(&config).unwrap());
println!(
"Credentials are never read from local config/env over HTTP transport — \
every request must carry its own \"{header_name}\" header."
);
}
}
}
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("GITHUB_MCP_URL".to_string(), url);
env.insert(
"GITHUB_MCP_AUTH_METHOD".to_string(),
serde_json::to_value(auth_method)?
.as_str()
.unwrap_or_default()
.to_string(),
);
env.insert("GITHUB_MCP_API_VERSION".to_string(), api_version);
env.insert(
"GITHUB_MCP_TRANSPORT".to_string(),
serde_json::to_value(transport)?
.as_str()
.unwrap_or_default()
.to_string(),
);
for (key, value) in &credentials {
env.insert(format!("GITHUB_MCP_{}", to_env_key(key)), value.clone());
}
prompt_persistence(&env).await?;
print_mcp_client_config(transport, auth_method, &env);
let run_command = match transport {
Transport::Stdio => "start",
Transport::Http => "http",
};
println!("Setup complete! Run: github-mcp {run_command}");
Ok(())
}