pub mod config;
pub mod error;
pub mod registrars;
pub mod runner;
#[allow(unused_imports)]
pub use config::{
McpAgentTarget, McpRegisterConfig, McpRegistrationScope, McpServerSpec, McpServerTransport,
};
#[allow(unused_imports)]
pub use error::McpRegisterError;
#[allow(unused_imports)]
pub use runner::{ApplyReport, RemoveReport, apply, check, remove};
pub fn auto_detect_agents() -> Vec<McpAgentTarget> {
let Some(home) = dirs::home_dir() else {
return Vec::new();
};
let mut found = Vec::new();
if home.join(".claude.json").exists() || home.join(".claude").is_dir() {
found.push(McpAgentTarget::ClaudeCode);
}
if home.join(".cursor").is_dir() {
found.push(McpAgentTarget::Cursor);
}
if home.join(".codex").is_dir() {
found.push(McpAgentTarget::Codex);
}
if home.join(".codeium").join("windsurf").is_dir() {
found.push(McpAgentTarget::Windsurf);
}
if home.join(".continue").is_dir() {
found.push(McpAgentTarget::Continue);
}
found
}
pub fn synthesize_auto_register(agents: Vec<McpAgentTarget>) -> McpRegisterConfig {
McpRegisterConfig {
agents,
scope: McpRegistrationScope::User,
allow_custom_servers: false,
jarvy: None,
servers: Vec::new(),
library_sources: Vec::new(),
origin: crate::ai_hooks::ConfigOrigin::Local,
}
}
#[cfg(test)]
mod auto_detect_tests {
use super::*;
use std::sync::Mutex;
static HOME_MUTEX: Mutex<()> = Mutex::new(());
fn with_fake_home<F: FnOnce(&std::path::Path)>(f: F) {
let _guard = HOME_MUTEX.lock().unwrap_or_else(|p| p.into_inner());
let tmp = tempfile::TempDir::new().expect("tempdir");
let prev = std::env::var("HOME").ok();
#[allow(unsafe_code)]
unsafe {
std::env::set_var("HOME", tmp.path());
}
f(tmp.path());
#[allow(unsafe_code)]
unsafe {
match prev {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
}
}
#[test]
fn detects_no_agents_in_empty_home() {
with_fake_home(|_| {
let found = auto_detect_agents();
assert!(
found.is_empty(),
"empty home must yield no detected agents, got {found:?}"
);
});
}
#[test]
fn detects_claude_code_via_dotfile() {
with_fake_home(|home| {
std::fs::write(home.join(".claude.json"), "{}").unwrap();
let found = auto_detect_agents();
assert_eq!(found, vec![McpAgentTarget::ClaudeCode]);
});
}
#[test]
fn detects_claude_code_via_directory() {
with_fake_home(|home| {
std::fs::create_dir(home.join(".claude")).unwrap();
let found = auto_detect_agents();
assert_eq!(found, vec![McpAgentTarget::ClaudeCode]);
});
}
#[test]
fn detects_multiple_agents() {
with_fake_home(|home| {
std::fs::create_dir(home.join(".cursor")).unwrap();
std::fs::create_dir(home.join(".codex")).unwrap();
std::fs::create_dir_all(home.join(".codeium").join("windsurf")).unwrap();
std::fs::create_dir(home.join(".continue")).unwrap();
let found = auto_detect_agents();
assert_eq!(
found,
vec![
McpAgentTarget::Cursor,
McpAgentTarget::Codex,
McpAgentTarget::Windsurf,
McpAgentTarget::Continue,
]
);
});
}
#[test]
fn ignores_cline_even_when_vs_code_globalstorage_present() {
with_fake_home(|home| {
let bogus = home
.join("Library")
.join("Application Support")
.join("Code")
.join("User")
.join("globalStorage");
std::fs::create_dir_all(&bogus).unwrap();
let found = auto_detect_agents();
assert!(
!found.contains(&McpAgentTarget::Cline),
"auto-detect must not return Cline; got {found:?}"
);
});
}
#[test]
fn synthesize_auto_register_carries_local_origin_and_user_scope() {
let cfg = synthesize_auto_register(vec![McpAgentTarget::ClaudeCode]);
assert_eq!(cfg.scope, McpRegistrationScope::User);
assert!(!cfg.allow_custom_servers);
assert!(cfg.servers.is_empty());
assert!(cfg.jarvy.is_none());
assert!(matches!(cfg.origin, crate::ai_hooks::ConfigOrigin::Local));
}
}