use dialoguer::{Confirm, Input, Select};
use zeph_core::config::{McpOAuthConfig, McpServerConfig, McpTrustLevel, OAuthTokenStorage};
use super::WizardState;
#[allow(clippy::too_many_lines)]
pub(super) fn step_mcpls(state: &mut WizardState) -> anyhow::Result<()> {
println!("== MCP: LSP Code Intelligence ==\n");
let detected = mcpls_in_path();
if detected {
println!("mcpls detected.");
} else {
println!("mcpls not found. Install with: cargo install mcpls");
}
state.mcpls_enabled = Confirm::new()
.with_prompt("Enable LSP code intelligence via mcpls?")
.default(detected)
.interact()?;
if state.mcpls_enabled {
let roots_raw: String = Input::new()
.with_prompt(
"Workspace root paths (comma-separated, leave empty for current directory)",
)
.default(String::new())
.interact_text()?;
state.mcpls_workspace_roots = roots_raw
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect();
}
println!();
Ok(())
}
pub(super) fn mcpls_in_path() -> bool {
let path_var = std::env::var_os("PATH").unwrap_or_default();
let exe_name = if cfg!(windows) { "mcpls.exe" } else { "mcpls" };
std::env::split_paths(&path_var)
.map(|dir| dir.join(exe_name))
.any(|p| p.is_file())
}
pub(super) fn write_mcpls_config(
state: &WizardState,
config_path: &std::path::Path,
) -> anyhow::Result<()> {
let base = config_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let zeph_dir = base.join(".zeph");
std::fs::create_dir_all(&zeph_dir)?;
let roots = if state.mcpls_workspace_roots.is_empty() {
vec![".".to_owned()]
} else {
state.mcpls_workspace_roots.clone()
};
let roots_toml = roots
.iter()
.map(|r| format!("\"{}\"", r.replace('\\', "\\\\").replace('"', "\\\"")))
.collect::<Vec<_>>()
.join(", ");
let content = format!(
r#"[workspace]
roots = [{roots_toml}]
[[workspace.language_extensions]]
language_id = "rust"
extensions = ["rs"]
[[lsp_servers]]
language_id = "rust"
command = "rust-analyzer"
args = []
file_patterns = ["**/*.rs"]
"#
);
let mcpls_path = zeph_dir.join("mcpls.toml");
std::fs::write(&mcpls_path, content)?;
println!("mcpls config written to {}", mcpls_path.display());
Ok(())
}
#[allow(clippy::too_many_lines)]
pub(super) fn step_mcp_remote(state: &mut WizardState) -> anyhow::Result<()> {
println!("== MCP: Remote Servers ==\n");
println!(
"Configure remote MCP servers that require authentication (static headers or OAuth 2.1)."
);
println!("Skip this step if you have no remote MCP servers.\n");
loop {
let add = Confirm::new()
.with_prompt("Add a remote MCP server?")
.default(false)
.interact()?;
if !add {
break;
}
let id: String = Input::new()
.with_prompt("Server ID (unique slug, e.g. 'todoist')")
.interact_text()?;
let url: String = Input::new()
.with_prompt("Server URL (e.g. https://mcp.example.com)")
.interact_text()?;
let auth_choices = [
"None (no auth)",
"Static header (Bearer token)",
"OAuth 2.1 (interactive flow)",
];
let auth_sel = Select::new()
.with_prompt("Authentication method")
.items(auth_choices)
.default(0)
.interact()?;
let mut headers = std::collections::HashMap::new();
let mut oauth: Option<McpOAuthConfig> = None;
match auth_sel {
1 => {
println!("Header value supports vault references: ${{VAULT_KEY}}");
let header_name: String = Input::new()
.with_prompt("Header name")
.default("Authorization".into())
.interact_text()?;
let header_value: String = Input::new()
.with_prompt("Header value (e.g. 'Bearer ${{MY_TOKEN}}')")
.interact_text()?;
headers.insert(header_name, header_value);
}
2 => {
let storage_choices =
["vault (persisted in age vault)", "memory (lost on restart)"];
let storage_sel = Select::new()
.with_prompt("Token storage")
.items(storage_choices)
.default(0)
.interact()?;
let token_storage = if storage_sel == 0 {
OAuthTokenStorage::Vault
} else {
OAuthTokenStorage::Memory
};
let scopes_raw: String = Input::new()
.with_prompt("OAuth scopes (space-separated, leave empty for server default)")
.default(String::new())
.interact_text()?;
let scopes: Vec<String> =
scopes_raw.split_whitespace().map(str::to_owned).collect();
let callback_port: u16 = Input::new()
.with_prompt("Local callback port (0 = auto-assign)")
.default(18766)
.interact_text()?;
let client_name: String = Input::new()
.with_prompt("OAuth client name")
.default("Zeph".into())
.interact_text()?;
oauth = Some(McpOAuthConfig {
enabled: true,
token_storage,
scopes,
callback_port,
client_name,
});
}
_ => {}
}
let trust_choices = ["untrusted (default)", "trusted", "sandboxed"];
let trust_idx = Select::new()
.with_prompt("Trust level")
.items(trust_choices)
.default(0)
.interact()?;
let trust_level = match trust_idx {
1 => McpTrustLevel::Trusted,
2 => McpTrustLevel::Sandboxed,
_ => McpTrustLevel::Untrusted,
};
state.mcp_remote_servers.push(McpServerConfig {
id,
command: None,
args: Vec::new(),
env: std::collections::HashMap::new(),
url: Some(url),
timeout: 30,
policy: zeph_mcp::McpPolicy::default(),
headers,
oauth,
trust_level,
tool_allowlist: None,
expected_tools: Vec::new(),
roots: Vec::new(),
tool_metadata: std::collections::HashMap::new(),
elicitation_enabled: None,
env_isolation: None,
});
println!("Server added.");
}
println!();
Ok(())
}
#[allow(clippy::too_many_lines)]
pub(super) fn step_mcp_discovery(state: &mut WizardState) -> anyhow::Result<()> {
println!("== MCP: Tool Discovery ==\n");
println!("Controls how MCP tools are selected per turn when you have many tools configured.");
println!(" none — all tools passed to the LLM every turn (default, safest)");
println!(" embedding — cosine similarity via embedding; fast, no extra LLM call per turn");
println!(" llm — LLM-based pruning via mcp.pruning config\n");
let strategy_choices = ["none", "embedding", "llm"];
let default_idx = match state.mcp_discovery_strategy.as_str() {
"embedding" => 1,
"llm" => 2,
_ => 0,
};
let idx = Select::new()
.with_prompt("MCP tool discovery strategy")
.items(strategy_choices)
.default(default_idx)
.interact()?;
strategy_choices[idx].clone_into(&mut state.mcp_discovery_strategy);
if state.mcp_discovery_strategy == "embedding" {
let top_k: usize = Input::new()
.with_prompt("Max tools to select per turn (top_k)")
.default(10)
.interact_text()?;
state.mcp_discovery_top_k = top_k;
let provider: String = Input::new()
.with_prompt("Embedding provider name from [[llm.providers]] (leave empty for default)")
.default(String::new())
.interact_text()?;
state.mcp_discovery_provider = provider;
}
println!();
Ok(())
}