pub mod claude;
pub mod cursor;
pub mod paths;
pub mod vscode;
pub mod zed;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::{Error, Result};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Input {
#[serde(rename = "type")]
pub(crate) kind: String,
pub(crate) id: String,
pub(crate) description: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) password: bool,
}
pub trait EditorConfig: serde::Serialize + serde::de::DeserializeOwned + Default + Clone {
type Server: serde::Serialize + serde::de::DeserializeOwned + Clone;
fn has_server(&self, name: &str) -> bool;
fn add_server(&mut self, name: String, server: Self::Server);
fn remove_server(&mut self, name: &str);
fn server_names(&self) -> Box<dyn Iterator<Item = &str> + '_>;
#[must_use]
fn preprocess(bytes: Vec<u8>) -> Vec<u8> {
bytes
}
}
pub struct Manager<C: EditorConfig> {
path: PathBuf,
config: C,
}
impl<C: EditorConfig> Manager<C> {
pub(crate) fn load(path: PathBuf) -> Result<Self> {
println!("Using config path \"{}\"", path.display());
let exists = path.exists();
if !exists {
return Ok(Self {
path,
config: C::default(),
});
}
let data = std::fs::read(&path).map_err(|e| Error::EditorConfigRead {
path: path.clone(),
source: e,
})?;
let data = C::preprocess(data);
let config: C = serde_json::from_slice(&data).map_err(|e| Error::EditorConfigJson {
path: path.clone(),
source: e,
})?;
Ok(Self { path, config })
}
pub(crate) fn save(&self) -> Result<()> {
if let Some(parent) = self.path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent).map_err(|e| Error::EditorConfigWrite {
path: self.path.clone(),
source: e,
})?;
}
let bytes =
serde_json::to_vec_pretty(&self.config).map_err(|e| Error::EditorConfigJson {
path: self.path.clone(),
source: e,
})?;
self.backup()?;
std::fs::write(&self.path, &bytes).map_err(|e| Error::EditorConfigWrite {
path: self.path.clone(),
source: e,
})?;
Ok(())
}
fn backup(&self) -> Result<()> {
if !self.path.exists() {
return Ok(());
}
println!("Backing up config file at \"{}\"", self.path.display());
let dest = backup_path(&self.path);
std::fs::copy(&self.path, &dest).map_err(|e| Error::EditorConfigBackup {
path: dest,
source: e,
})?;
Ok(())
}
pub(crate) fn enable_server(&mut self, name: &str, server: C::Server) -> Result<()> {
if self.config.has_server(name) {
println!("MCP server \"{name}\" already exists and will be overwritten");
}
self.config.add_server(name.to_string(), server);
self.save()?;
println!("Successfully enabled MCP server: \"{name}\"");
Ok(())
}
pub(crate) fn disable_server(&mut self, name: &str) -> Result<()> {
if !self.config.has_server(name) {
println!("MCP server \"{name}\" does not exist");
return Ok(());
}
self.config.remove_server(name);
self.save()
}
pub(crate) fn print(&self) {
if !self.path.exists() {
println!("(no servers configured at {})", self.path.display());
return;
}
let names: Vec<&str> = self.config.server_names().collect();
if names.is_empty() {
println!("(no servers configured at {})", self.path.display());
return;
}
for name in names {
println!("{name}");
}
}
}
fn backup_path(primary: &Path) -> PathBuf {
let parent = primary.parent().unwrap_or_else(|| Path::new(""));
let stem = primary
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
parent.join(format!("{stem}.backup.json"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backup_path_strips_extension() {
assert_eq!(
backup_path(Path::new("/tmp/claude_desktop_config.json")),
PathBuf::from("/tmp/claude_desktop_config.backup.json")
);
}
#[test]
fn backup_path_handles_no_extension() {
assert_eq!(
backup_path(Path::new("/tmp/config")),
PathBuf::from("/tmp/config.backup.json")
);
}
}