use std::path::PathBuf;
use serde_json::{json, Value};
use crate::errors::{McpError, McpErrorKind};
use crate::install::InstallOpts;
pub fn write(opts: &InstallOpts) -> Result<(), McpError> {
let path = config_path()?;
super::shared_json::write_mcp_servers_entry(&path, "Claude Desktop", opts)
}
pub fn config_path() -> Result<PathBuf, McpError> {
let home = dirs_home()?;
#[cfg(target_os = "macos")]
let p = home
.join("Library")
.join("Application Support")
.join("Claude")
.join("claude_desktop_config.json");
#[cfg(target_os = "windows")]
let p = std::env::var_os("APPDATA")
.map(PathBuf::from)
.unwrap_or_else(|| home.join("AppData").join("Roaming"))
.join("Claude")
.join("claude_desktop_config.json");
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
let p = home
.join(".config")
.join("Claude")
.join("claude_desktop_config.json");
Ok(p)
}
fn dirs_home() -> Result<PathBuf, McpError> {
directories::BaseDirs::new()
.map(|b| b.home_dir().to_path_buf())
.ok_or_else(|| {
McpError::new(
McpErrorKind::InternalError,
"could not resolve home directory",
)
})
}
pub(crate) fn build_entry(opts: &InstallOpts) -> Value {
let mut entry = json!({
"command": "tsafe-mcp",
"args": opts.server_args(),
});
if let Ok(sock) = std::env::var("TSAFE_AGENT_SOCK") {
if !sock.is_empty() {
entry["env"] = json!({"TSAFE_AGENT_SOCK": sock});
}
}
entry
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_entry_carries_command_and_args() {
let opts = InstallOpts {
profile: "demo".to_string(),
allowed_keys: vec!["demo/*".to_string()],
denied_keys: vec![],
contract: None,
allow_reveal: false,
name: None,
scope: crate::install::Scope::Global,
dry_run: false,
uninstall: false,
audit_source: None,
};
let e = build_entry(&opts);
assert_eq!(e["command"], "tsafe-mcp");
let args = e["args"].as_array().unwrap();
assert!(args.iter().any(|s| s == "--profile"));
}
}