use super::agent_config::{
ConfigFormat, json_add_cora, json_has_cora, read_json_config, write_json_config,
};
use anyhow::{Context, Result};
use colored::Colorize;
use std::path::PathBuf;
#[derive(Debug, Clone)]
struct AgentInfo {
name: &'static str,
config_path: PathBuf,
format: ConfigFormat,
}
pub struct InstallOptions {
pub list: bool,
pub agents: Option<String>,
pub dry_run: bool,
pub force: bool,
#[allow(dead_code)]
pub yes: bool,
pub remove: bool,
pub validate: bool,
}
fn known_agents(home: &std::path::Path) -> Vec<AgentInfo> {
vec![
AgentInfo {
name: "cline",
config_path: home.join(".cline/cline_mcp_settings.json"),
format: ConfigFormat::Json,
},
AgentInfo {
name: "cursor",
config_path: home.join(".cursor/mcp.json"),
format: ConfigFormat::Jsonc,
},
AgentInfo {
name: "windsurf",
config_path: home.join(".codeium/windsurf/mcp_config.json"),
format: ConfigFormat::Json,
},
AgentInfo {
name: "roo",
config_path: home.join(".roo/mcp.json"),
format: ConfigFormat::Json,
},
AgentInfo {
name: "copilot",
config_path: home.join(".github-copilot/mcp.json"),
format: ConfigFormat::Json,
},
AgentInfo {
name: "continue",
config_path: home.join(".continue/config.json"),
format: ConfigFormat::Json,
},
AgentInfo {
name: "amp",
config_path: home.join(".amp/amprc.json"),
format: ConfigFormat::Json,
},
AgentInfo {
name: "trae",
config_path: home.join(".trae/mcp.json"),
format: ConfigFormat::Json,
},
AgentInfo {
name: "kodu",
config_path: home.join(".kodu/settings.json"),
format: ConfigFormat::Json,
},
AgentInfo {
name: "fabric",
config_path: home.join(".fabric/mcp.json"),
format: ConfigFormat::Json,
},
]
}
pub fn known_agent_names() -> Vec<&'static str> {
known_agents(std::path::Path::new(""))
.into_iter()
.map(|a| a.name)
.collect()
}
fn detect_agents() -> Result<Vec<AgentInfo>> {
let home = dirs::home_dir().context("Could not determine home directory")?;
let candidates = known_agents(&home);
let detected: Vec<AgentInfo> = candidates
.into_iter()
.filter(|a| a.config_path.exists())
.collect();
Ok(detected)
}
fn validate_json_config(path: &std::path::Path) -> Result<()> {
let config = read_json_config(path)?;
let _ = serde_json::to_string(&config)?;
Ok(())
}
fn install_json_agent(path: &std::path::Path, force: bool, dry_run: bool) -> Result<String> {
let mut config = read_json_config(path)?;
if json_has_cora(&config) && !force {
return Ok(format!(
" {} {} — cora entry already exists (use --force to overwrite)",
"⏭ ".dimmed(),
path.display()
));
}
json_add_cora(&mut config)?;
if dry_run {
Ok(format!(
" {} {} — would write cora MCP server entry",
"🔍 ".cyan(),
path.display()
))
} else {
write_json_config(path, &config)?;
Ok(format!(
" {} {} — cora MCP server entry added",
"✓ ".green(),
path.display()
))
}
}
fn uninstall_json_agent(path: &std::path::Path, dry_run: bool) -> Result<String> {
use super::agent_config;
let mut config = read_json_config(path)?;
if !agent_config::json_has_cora(&config) {
return Ok(format!(
" {} {} — no cora entry found",
"⏭ ".dimmed(),
path.display()
));
}
agent_config::json_remove_cora(&mut config);
if dry_run {
Ok(format!(
" {} {} — would remove cora MCP server entry",
"🔍 ".cyan(),
path.display()
))
} else {
write_json_config(path, &config)?;
Ok(format!(
" {} {} — cora MCP server entry removed",
"✓ ".green(),
path.display()
))
}
}
fn install_agent(agent: &AgentInfo, opts: &InstallOptions) -> Result<String> {
match agent.format {
ConfigFormat::Json | ConfigFormat::Jsonc => {
if opts.remove {
uninstall_json_agent(&agent.config_path, opts.dry_run)
} else {
install_json_agent(&agent.config_path, opts.force, opts.dry_run)
}
}
ConfigFormat::Yaml => {
use super::agent_config;
let mut config = agent_config::read_yaml_config(&agent.config_path)?;
if opts.remove {
if !agent_config::yaml_has_cora(&config) {
return Ok(format!(
" {} {} — no cora entry found",
"⏭ ".dimmed(),
agent.config_path.display()
));
}
agent_config::yaml_remove_cora(&mut config);
if opts.dry_run {
Ok(format!(
" {} {} — would remove cora MCP server entry",
"🔍 ".cyan(),
agent.config_path.display()
))
} else {
agent_config::write_yaml_config(&agent.config_path, &config)?;
Ok(format!(
" {} {} — cora MCP server entry removed",
"✓ ".green(),
agent.config_path.display()
))
}
} else {
if agent_config::yaml_has_cora(&config) && !opts.force {
return Ok(format!(
" {} {} — cora entry already exists (use --force to overwrite)",
"⏭ ".dimmed(),
agent.config_path.display()
));
}
agent_config::yaml_add_cora(&mut config)?;
if opts.dry_run {
Ok(format!(
" {} {} — would write cora MCP server entry",
"🔍 ".cyan(),
agent.config_path.display()
))
} else {
agent_config::write_yaml_config(&agent.config_path, &config)?;
Ok(format!(
" {} {} — cora MCP server entry added",
"✓ ".green(),
agent.config_path.display()
))
}
}
}
}
}
pub fn execute_install(opts: &InstallOptions) -> Result<String> {
let all_agents = detect_agents()?;
if all_agents.is_empty() {
return Ok("No AI coding agents detected. Install an agent (e.g. Cline, Cursor, Windsurf) and run again.".to_string());
}
let agents: Vec<&AgentInfo> = if let Some(ref agent_list) = opts.agents {
let requested: std::collections::HashSet<&str> =
agent_list.split(',').map(|s| s.trim()).collect();
all_agents
.iter()
.filter(|a| requested.contains(a.name))
.collect()
} else {
all_agents.iter().collect()
};
if agents.is_empty() {
if let Some(ref agent_list) = opts.agents {
return Ok(format!(
"None of the specified agents found: {agent_list}\nDetected agents: {}",
all_agents
.iter()
.map(|a| a.name)
.collect::<Vec<_>>()
.join(", ")
));
}
unreachable!();
}
if opts.list {
let mut lines = vec![format!("Detected {} AI coding agent(s):\n", agents.len())];
for agent in &agents {
lines.push(format!(
" {} {} {}",
"● ".cyan(),
agent.name.bold(),
agent.config_path.display()
));
}
return Ok(lines.join("\n"));
}
let action = if opts.remove {
"Removing"
} else {
"Configuring"
};
let noun = if opts.remove { "from" } else { "for" };
let mut lines = vec![format!(
"{} cora MCP {} {} agent(s)…{}",
action,
noun,
agents.len(),
if opts.dry_run { " (dry run)" } else { "" }
)];
lines.push(String::new());
for agent in &agents {
let result = install_agent(agent, opts)?;
lines.push(format!("{} {}", agent.name.bold(), result));
}
if opts.validate && !opts.dry_run {
lines.push(String::new());
lines.push("Validating agent configs…".to_string());
let mut errors = 0;
for agent in &agents {
match agent.format {
ConfigFormat::Json | ConfigFormat::Jsonc => {
if let Err(e) = validate_json_config(&agent.config_path) {
lines.push(format!(
" {} {} — INVALID: {}",
"✗ ".red(),
agent.config_path.display(),
e
));
errors += 1;
} else {
lines.push(format!(
" {} {} — valid JSON",
"✓ ".green(),
agent.config_path.display()
));
}
}
ConfigFormat::Yaml => {
lines.push(format!(
" {} {} — skipped (YAML validation not implemented)",
"⏭ ".dimmed(),
agent.config_path.display()
));
}
}
}
if errors > 0 {
lines.push(format!(
"\n{} {errors} config(s) failed validation!",
"⚠ ".yellow()
));
}
}
lines.push(String::new());
if opts.remove {
lines.push("Done. Restart your AI agent to pick up the changes.".to_string());
} else {
lines.push("Done. Restart your AI agent to pick up the new MCP server.".to_string());
}
Ok(lines.join("\n"))
}