use std::path::PathBuf;
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, "Windsurf", opts)
}
pub fn config_path() -> Result<PathBuf, McpError> {
let home = directories::BaseDirs::new()
.map(|b| b.home_dir().to_path_buf())
.ok_or_else(|| {
McpError::new(
McpErrorKind::InternalError,
"could not resolve home directory",
)
})?;
Ok(home
.join(".codeium")
.join("windsurf")
.join("mcp_config.json"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::install::shared_json::write_mcp_servers_entry;
use crate::install::{InstallOpts, Scope};
use serde_json::Value;
fn opts_minimal(name: &str, uninstall: bool) -> InstallOpts {
InstallOpts {
profile: "demo".to_string(),
allowed_keys: vec!["demo/*".to_string()],
denied_keys: vec![],
contract: None,
allow_reveal: false,
name: Some(name.to_string()),
scope: Scope::Global,
dry_run: false,
uninstall,
audit_source: None,
}
}
#[test]
fn config_path_ends_with_codeium_windsurf_mcp_config_json() {
let path = config_path().expect("home dir resolved");
let s = path.to_string_lossy().replace('\\', "/");
assert!(
s.ends_with(".codeium/windsurf/mcp_config.json"),
"path should end with .codeium/windsurf/mcp_config.json: {s}"
);
}
#[test]
fn shared_json_writer_creates_windsurf_shaped_mcp_config() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp
.path()
.join(".codeium")
.join("windsurf")
.join("mcp_config.json");
write_mcp_servers_entry(&path, "Windsurf", &opts_minimal("testsrv", false)).unwrap();
assert!(path.exists(), "expected windsurf config at {path:?}");
let raw = std::fs::read_to_string(&path).unwrap();
let v: Value = serde_json::from_str(&raw).unwrap();
assert_eq!(v["mcpServers"]["testsrv"]["command"], "tsafe-mcp");
let args = v["mcpServers"]["testsrv"]["args"].as_array().unwrap();
assert!(args.iter().any(|s| s == "--profile"));
}
#[test]
fn shared_json_writer_preserves_other_existing_entries() {
let tmp = tempfile::tempdir().unwrap();
let cfg_dir = tmp.path().join(".codeium").join("windsurf");
std::fs::create_dir_all(&cfg_dir).unwrap();
let cfg_path = cfg_dir.join("mcp_config.json");
std::fs::write(
&cfg_path,
r#"{"mcpServers":{"other":{"command":"x","args":[]}}}"#,
)
.unwrap();
write_mcp_servers_entry(&cfg_path, "Windsurf", &opts_minimal("testsrv", false)).unwrap();
let raw = std::fs::read_to_string(&cfg_path).unwrap();
let v: Value = serde_json::from_str(&raw).unwrap();
let servers = v["mcpServers"].as_object().unwrap();
assert!(
servers.contains_key("other"),
"existing 'other' must survive"
);
assert!(servers.contains_key("testsrv"), "new entry must be present");
}
#[test]
fn shared_json_writer_rejects_malformed_config_with_install_config_malformed() {
let tmp = tempfile::tempdir().unwrap();
let cfg_dir = tmp.path().join(".codeium").join("windsurf");
std::fs::create_dir_all(&cfg_dir).unwrap();
let cfg_path = cfg_dir.join("mcp_config.json");
std::fs::write(&cfg_path, "{not valid json").unwrap();
let err = write_mcp_servers_entry(&cfg_path, "Windsurf", &opts_minimal("testsrv", false))
.expect_err("should reject malformed config");
assert_eq!(err.kind, McpErrorKind::InstallConfigMalformed);
}
#[test]
fn shared_json_writer_uninstall_removes_only_named_entry() {
let tmp = tempfile::tempdir().unwrap();
let cfg_dir = tmp.path().join(".codeium").join("windsurf");
std::fs::create_dir_all(&cfg_dir).unwrap();
let cfg_path = cfg_dir.join("mcp_config.json");
std::fs::write(
&cfg_path,
r#"{"mcpServers":{"other":{"command":"x"},"testsrv":{"command":"old"}}}"#,
)
.unwrap();
write_mcp_servers_entry(&cfg_path, "Windsurf", &opts_minimal("testsrv", true)).unwrap();
let raw = std::fs::read_to_string(&cfg_path).unwrap();
let v: Value = serde_json::from_str(&raw).unwrap();
let servers = v["mcpServers"].as_object().unwrap();
assert!(
servers.contains_key("other"),
"other entry must survive uninstall"
);
assert!(!servers.contains_key("testsrv"), "testsrv must be gone");
}
}