use std::path::PathBuf;
#[must_use]
pub fn claude_config_path() -> PathBuf {
#[cfg(target_os = "macos")]
{
let home = dirs::home_dir();
let user = std::env::var("USER").unwrap_or_default();
claude_config_path_macos_from(home.as_deref(), &user)
}
#[cfg(target_os = "linux")]
{
let home = dirs::home_dir();
let xdg = std::env::var("XDG_CONFIG_HOME").ok();
let user = std::env::var("USER").unwrap_or_default();
claude_config_path_linux_from(home.as_deref(), xdg.as_deref(), &user)
}
#[cfg(target_os = "windows")]
{
let appdata = std::env::var("APPDATA").ok();
let userprofile = std::env::var("USERPROFILE").ok();
claude_config_path_windows_from(appdata.as_deref(), userprofile.as_deref())
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let home = dirs::home_dir();
let xdg = std::env::var("XDG_CONFIG_HOME").ok();
let user = std::env::var("USER").unwrap_or_default();
claude_config_path_linux_from(home.as_deref(), xdg.as_deref(), &user)
}
}
#[cfg(any(target_os = "macos", test))]
fn claude_config_path_macos_from(home: Option<&std::path::Path>, user: &str) -> PathBuf {
if let Some(home) = home {
return home
.join("Library")
.join("Application Support")
.join("Claude")
.join("claude_desktop_config.json");
}
PathBuf::from("/Users")
.join(user)
.join("Library")
.join("Application Support")
.join("Claude")
.join("claude_desktop_config.json")
}
#[cfg(any(
target_os = "linux",
not(any(target_os = "macos", target_os = "windows")),
test
))]
fn claude_config_path_linux_from(
home: Option<&std::path::Path>,
xdg_config_home: Option<&str>,
user: &str,
) -> PathBuf {
if let Some(home) = home {
let cfg_root = match xdg_config_home {
Some(v) if !v.is_empty() => PathBuf::from(v),
_ => home.join(".config"),
};
return cfg_root.join("Claude").join("claude_desktop_config.json");
}
PathBuf::from("/home")
.join(user)
.join(".config")
.join("Claude")
.join("claude_desktop_config.json")
}
#[cfg(any(target_os = "windows", test))]
fn claude_config_path_windows_from(appdata: Option<&str>, userprofile: Option<&str>) -> PathBuf {
if let Some(v) = appdata.filter(|s| !s.is_empty()) {
return PathBuf::from(v)
.join("Claude")
.join("claude_desktop_config.json");
}
if let Some(v) = userprofile.filter(|s| !s.is_empty()) {
return PathBuf::from(v)
.join("AppData")
.join("Roaming")
.join("Claude")
.join("claude_desktop_config.json");
}
PathBuf::from(r"C:\Users\Default\AppData\Roaming")
.join("Claude")
.join("claude_desktop_config.json")
}
#[must_use]
pub fn cursor_config_path() -> PathBuf {
#[cfg(target_os = "macos")]
{
let home = dirs::home_dir();
let user = std::env::var("USER").unwrap_or_default();
cursor_config_path_macos_from(home.as_deref(), &user)
}
#[cfg(target_os = "linux")]
{
let home = dirs::home_dir();
let user = std::env::var("USER").unwrap_or_default();
cursor_config_path_linux_from(home.as_deref(), &user)
}
#[cfg(target_os = "windows")]
{
let home = dirs::home_dir();
let userprofile = std::env::var("USERPROFILE").ok();
cursor_config_path_windows_from(home.as_deref(), userprofile.as_deref())
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let home = dirs::home_dir();
let user = std::env::var("USER").unwrap_or_default();
cursor_config_path_linux_from(home.as_deref(), &user)
}
}
#[must_use]
pub fn cursor_workspace_path() -> PathBuf {
cursor_workspace_path_from(std::env::current_dir().ok().as_deref())
}
#[cfg(any(target_os = "macos", test))]
fn cursor_config_path_macos_from(home: Option<&std::path::Path>, user: &str) -> PathBuf {
if let Some(home) = home {
return home.join(".cursor").join("mcp.json");
}
PathBuf::from("/Users")
.join(user)
.join(".cursor")
.join("mcp.json")
}
#[cfg(any(
target_os = "linux",
not(any(target_os = "macos", target_os = "windows")),
test
))]
fn cursor_config_path_linux_from(home: Option<&std::path::Path>, user: &str) -> PathBuf {
if let Some(home) = home {
return home.join(".cursor").join("mcp.json");
}
PathBuf::from("/home")
.join(user)
.join(".cursor")
.join("mcp.json")
}
#[cfg(any(target_os = "windows", test))]
fn cursor_config_path_windows_from(
home: Option<&std::path::Path>,
userprofile: Option<&str>,
) -> PathBuf {
if let Some(home) = home {
return home.join(".cursor").join("mcp.json");
}
if let Some(v) = userprofile.filter(|s| !s.is_empty()) {
return PathBuf::from(v).join(".cursor").join("mcp.json");
}
PathBuf::from(".cursor").join("mcp.json")
}
fn cursor_workspace_path_from(cwd: Option<&std::path::Path>) -> PathBuf {
if let Some(cwd) = cwd {
return cwd.join(".cursor").join("mcp.json");
}
PathBuf::from(".cursor").join("mcp.json")
}
#[must_use]
pub fn vscode_config_path() -> PathBuf {
#[cfg(target_os = "macos")]
{
let home = dirs::home_dir();
let user = std::env::var("USER").unwrap_or_default();
vscode_config_path_macos_from(home.as_deref(), &user)
}
#[cfg(target_os = "linux")]
{
let home = dirs::home_dir();
let user = std::env::var("USER").unwrap_or_default();
vscode_config_path_linux_from(home.as_deref(), &user)
}
#[cfg(target_os = "windows")]
{
let home = dirs::home_dir();
let userprofile = std::env::var("USERPROFILE").ok();
vscode_config_path_windows_from(home.as_deref(), userprofile.as_deref())
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let home = dirs::home_dir();
let user = std::env::var("USER").unwrap_or_default();
vscode_config_path_linux_from(home.as_deref(), &user)
}
}
#[must_use]
pub fn vscode_workspace_path() -> PathBuf {
vscode_workspace_path_from(std::env::current_dir().ok().as_deref())
}
#[cfg(any(target_os = "macos", test))]
fn vscode_config_path_macos_from(home: Option<&std::path::Path>, user: &str) -> PathBuf {
if let Some(home) = home {
return home
.join("Library")
.join("Application Support")
.join("Code")
.join("User")
.join("mcp.json");
}
PathBuf::from("/Users")
.join(user)
.join("Library")
.join("Application Support")
.join("Code")
.join("User")
.join("mcp.json")
}
#[cfg(any(
target_os = "linux",
not(any(target_os = "macos", target_os = "windows")),
test
))]
fn vscode_config_path_linux_from(home: Option<&std::path::Path>, user: &str) -> PathBuf {
if let Some(home) = home {
return home
.join(".config")
.join("Code")
.join("User")
.join("mcp.json");
}
PathBuf::from("/home")
.join(user)
.join(".config")
.join("Code")
.join("User")
.join("mcp.json")
}
#[cfg(any(target_os = "windows", test))]
fn vscode_config_path_windows_from(
home: Option<&std::path::Path>,
userprofile: Option<&str>,
) -> PathBuf {
if let Some(home) = home {
return home
.join("AppData")
.join("Roaming")
.join("Code")
.join("User")
.join("mcp.json");
}
if let Some(v) = userprofile.filter(|s| !s.is_empty()) {
return PathBuf::from(v)
.join("AppData")
.join("Roaming")
.join("Code")
.join("User")
.join("mcp.json");
}
PathBuf::from("AppData")
.join("Roaming")
.join("Code")
.join("User")
.join("mcp.json")
}
fn vscode_workspace_path_from(cwd: Option<&std::path::Path>) -> PathBuf {
if let Some(cwd) = cwd {
return cwd.join(".vscode").join("mcp.json");
}
PathBuf::from(".vscode").join("mcp.json")
}
#[must_use]
pub fn derive_server_name(executable_path: &std::path::Path) -> String {
let base = executable_path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
if let Some(idx) = base.rfind('.') {
if idx == 0 {
return base;
}
return base[..idx].to_string();
}
base
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn derive_server_name_plain_basename() {
assert_eq!(
derive_server_name(Path::new("/usr/local/bin/myapp")),
"myapp"
);
}
#[test]
fn derive_server_name_strips_exe() {
assert_eq!(
derive_server_name(Path::new("/usr/local/bin/myapp.exe")),
"myapp"
);
}
#[test]
fn derive_server_name_strips_one_extension_only() {
assert_eq!(derive_server_name(Path::new("/tmp/foo.tar.exe")), "foo.tar");
}
#[test]
fn derive_server_name_relative_path() {
assert_eq!(derive_server_name(Path::new("./myapp")), "myapp");
}
#[test]
fn derive_server_name_no_directory() {
assert_eq!(derive_server_name(Path::new("kubectl")), "kubectl");
}
#[test]
fn derive_server_name_dotfile_returned_verbatim() {
assert_eq!(derive_server_name(Path::new(".bashrc")), ".bashrc");
}
#[test]
fn macos_uses_application_support_when_home_resolves() {
let path = claude_config_path_macos_from(Some(Path::new("/Users/synthetic")), "synthetic");
assert_eq!(
path,
PathBuf::from(
"/Users/synthetic/Library/Application Support/Claude/claude_desktop_config.json"
)
);
}
#[test]
fn macos_falls_back_to_users_user_when_home_unresolved() {
let path = claude_config_path_macos_from(None, "fallback");
assert_eq!(
path,
PathBuf::from(
"/Users/fallback/Library/Application Support/Claude/claude_desktop_config.json"
)
);
}
#[test]
fn linux_uses_dollar_home_dot_config_when_xdg_unset() {
let path =
claude_config_path_linux_from(Some(Path::new("/home/synthetic")), None, "synthetic");
assert_eq!(
path,
PathBuf::from("/home/synthetic/.config/Claude/claude_desktop_config.json")
);
}
#[test]
fn linux_uses_dollar_home_dot_config_when_xdg_empty() {
let path = claude_config_path_linux_from(
Some(Path::new("/home/synthetic")),
Some(""),
"synthetic",
);
assert_eq!(
path,
PathBuf::from("/home/synthetic/.config/Claude/claude_desktop_config.json")
);
}
#[test]
fn linux_honors_xdg_config_home() {
let path = claude_config_path_linux_from(
Some(Path::new("/home/synthetic")),
Some("/custom/xdg"),
"synthetic",
);
assert_eq!(
path,
PathBuf::from("/custom/xdg/Claude/claude_desktop_config.json")
);
}
#[test]
fn linux_falls_back_to_home_user_when_home_unresolved() {
let path = claude_config_path_linux_from(None, None, "fallback");
assert_eq!(
path,
PathBuf::from("/home/fallback/.config/Claude/claude_desktop_config.json")
);
}
#[test]
fn windows_prefers_appdata() {
let path = claude_config_path_windows_from(
Some(r"C:\Users\synth\AppData\Roaming"),
Some(r"C:\Users\synth"),
);
let components: Vec<String> = path
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
assert!(components.contains(&"Claude".to_string()));
assert!(components.contains(&"claude_desktop_config.json".to_string()));
assert!(
components.iter().any(|c| c.contains("Roaming")),
"must include the Roaming segment from APPDATA, got {components:?}"
);
}
#[test]
fn windows_falls_back_to_userprofile_when_appdata_empty() {
let path = claude_config_path_windows_from(Some(""), Some(r"C:\Users\synth"));
let s = path.to_string_lossy();
assert!(s.contains("AppData"), "got {s}");
assert!(s.contains("Roaming"), "got {s}");
assert!(s.contains("Claude"), "got {s}");
}
#[test]
fn windows_falls_back_to_userprofile_when_appdata_none() {
let path = claude_config_path_windows_from(None, Some(r"C:\Users\synth"));
let s = path.to_string_lossy();
assert!(s.contains("AppData"), "got {s}");
assert!(s.contains("Roaming"), "got {s}");
assert!(s.contains("Claude"), "got {s}");
}
#[test]
fn windows_default_users_fallback() {
let path = claude_config_path_windows_from(None, None);
let s = path.to_string_lossy().into_owned();
assert!(s.contains("Default"), "got {s}");
assert!(s.contains("Claude"), "got {s}");
}
#[test]
fn cursor_macos_uses_home_dot_cursor_when_home_resolves() {
let path = cursor_config_path_macos_from(Some(Path::new("/Users/synthetic")), "synthetic");
assert_eq!(path, PathBuf::from("/Users/synthetic/.cursor/mcp.json"));
}
#[test]
fn cursor_macos_falls_back_to_users_user_when_home_unresolved() {
let path = cursor_config_path_macos_from(None, "fallback");
assert_eq!(path, PathBuf::from("/Users/fallback/.cursor/mcp.json"));
}
#[test]
fn cursor_linux_uses_home_dot_cursor_when_home_resolves() {
let path = cursor_config_path_linux_from(Some(Path::new("/home/synthetic")), "synthetic");
assert_eq!(path, PathBuf::from("/home/synthetic/.cursor/mcp.json"));
}
#[test]
fn cursor_linux_falls_back_to_home_user_when_home_unresolved() {
let path = cursor_config_path_linux_from(None, "fallback");
assert_eq!(path, PathBuf::from("/home/fallback/.cursor/mcp.json"));
}
#[test]
fn cursor_linux_does_not_consult_xdg() {
let path = cursor_config_path_linux_from(Some(Path::new("/home/synthetic")), "synthetic");
let s = path.to_string_lossy();
assert!(
!s.contains(".config"),
"must not route through .config: {s}"
);
}
#[test]
fn cursor_windows_uses_home_when_resolves() {
let path = cursor_config_path_windows_from(
Some(Path::new(r"C:\Users\synth")),
Some(r"C:\Users\synth"),
);
let s = path.to_string_lossy();
assert!(s.contains(".cursor"), "got {s}");
assert!(s.contains("mcp.json"), "got {s}");
}
#[test]
fn cursor_windows_falls_back_to_userprofile_when_home_unresolved() {
let path = cursor_config_path_windows_from(None, Some(r"C:\Users\synth"));
let s = path.to_string_lossy();
assert!(s.contains(r"C:\Users\synth"), "got {s}");
assert!(s.contains(".cursor"), "got {s}");
}
#[test]
fn cursor_windows_relative_fallback_when_all_unresolved() {
let path = cursor_config_path_windows_from(None, None);
assert_eq!(path, PathBuf::from(r".cursor").join("mcp.json"));
}
#[test]
fn cursor_workspace_uses_cwd_when_resolves() {
let path = cursor_workspace_path_from(Some(Path::new("/tmp/myproj")));
assert_eq!(path, PathBuf::from("/tmp/myproj/.cursor/mcp.json"));
}
#[test]
fn cursor_workspace_falls_back_to_relative_when_cwd_unresolved() {
let path = cursor_workspace_path_from(None);
assert_eq!(path, PathBuf::from(".cursor").join("mcp.json"));
}
#[test]
fn vscode_macos_uses_application_support_code_when_home_resolves() {
let path = vscode_config_path_macos_from(Some(Path::new("/Users/synthetic")), "synthetic");
assert_eq!(
path,
PathBuf::from("/Users/synthetic/Library/Application Support/Code/User/mcp.json")
);
}
#[test]
fn vscode_macos_falls_back_to_users_user_when_home_unresolved() {
let path = vscode_config_path_macos_from(None, "fallback");
assert_eq!(
path,
PathBuf::from("/Users/fallback/Library/Application Support/Code/User/mcp.json")
);
}
#[test]
fn vscode_linux_uses_home_dot_config_code_when_home_resolves() {
let path = vscode_config_path_linux_from(Some(Path::new("/home/synthetic")), "synthetic");
assert_eq!(
path,
PathBuf::from("/home/synthetic/.config/Code/User/mcp.json")
);
}
#[test]
fn vscode_linux_falls_back_to_home_user_when_home_unresolved() {
let path = vscode_config_path_linux_from(None, "fallback");
assert_eq!(
path,
PathBuf::from("/home/fallback/.config/Code/User/mcp.json")
);
}
#[test]
fn vscode_linux_does_not_consult_xdg() {
let path = vscode_config_path_linux_from(Some(Path::new("/home/synthetic")), "synthetic");
assert_eq!(
path,
PathBuf::from("/home/synthetic/.config/Code/User/mcp.json")
);
}
#[test]
fn vscode_windows_uses_home_when_resolves() {
let path = vscode_config_path_windows_from(
Some(Path::new(r"C:\Users\synth")),
Some(r"C:\Users\synth"),
);
let s = path.to_string_lossy();
assert!(s.contains("AppData"), "got {s}");
assert!(s.contains("Roaming"), "got {s}");
assert!(s.contains("Code"), "got {s}");
assert!(s.contains("User"), "got {s}");
assert!(s.contains("mcp.json"), "got {s}");
}
#[test]
fn vscode_windows_falls_back_to_userprofile_when_home_unresolved() {
let path = vscode_config_path_windows_from(None, Some(r"C:\Users\synth"));
let s = path.to_string_lossy();
assert!(s.contains(r"C:\Users\synth"), "got {s}");
assert!(s.contains("AppData"), "got {s}");
assert!(s.contains("Roaming"), "got {s}");
assert!(s.contains("Code"), "got {s}");
assert!(s.contains("User"), "got {s}");
assert!(s.contains("mcp.json"), "got {s}");
}
#[test]
fn vscode_windows_relative_fallback_when_all_unresolved() {
let path = vscode_config_path_windows_from(None, None);
assert_eq!(
path,
PathBuf::from("AppData")
.join("Roaming")
.join("Code")
.join("User")
.join("mcp.json")
);
assert!(path.is_relative(), "tertiary fallback must be relative");
}
#[test]
fn vscode_workspace_uses_cwd_when_resolves() {
let path = vscode_workspace_path_from(Some(Path::new("/tmp/myproj")));
assert_eq!(path, PathBuf::from("/tmp/myproj/.vscode/mcp.json"));
}
#[test]
fn vscode_workspace_falls_back_to_relative_when_cwd_unresolved() {
let path = vscode_workspace_path_from(None);
assert_eq!(path, PathBuf::from(".vscode").join("mcp.json"));
}
}