magick_mcp/feature/
install.rs1use serde_json::{Value, json};
2use std::fs;
3use std::path::{Path, PathBuf};
4use thiserror::Error;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ClientType {
9 Cursor,
10 Claude,
11 Both,
12}
13
14#[derive(Debug, Clone)]
16pub struct ConfigPaths {
17 pub cursor_path: PathBuf,
18 pub claude_path: PathBuf,
19}
20
21impl ConfigPaths {
22 pub fn from_home_dir() -> Result<Self, InstallError> {
24 let home_dir = dirs::home_dir().ok_or(InstallError::HomeDirNotFound)?;
25
26 Ok(ConfigPaths {
27 cursor_path: home_dir.join(".cursor").join("mcp.json"),
28 claude_path: home_dir.join(".claude.json"),
29 })
30 }
31}
32
33pub struct MCPInstaller {
35 client_type: ClientType,
36 config_paths: ConfigPaths,
37}
38
39#[derive(Error, Debug)]
40pub enum InstallError {
41 #[error("Home directory not found")]
42 HomeDirNotFound,
43 #[error("IO error: {0}")]
44 IoError(#[from] std::io::Error),
45 #[error("Failed to parse JSON: {0}")]
46 ParseError(#[from] serde_json::Error),
47 #[error("Failed to get executable path: {0}")]
48 ExePathError(String),
49 #[error("Invalid config: {0}")]
50 InvalidConfig(String),
51}
52
53impl MCPInstaller {
54 pub fn new(client_type: ClientType, config_paths: ConfigPaths) -> Self {
56 MCPInstaller {
57 client_type,
58 config_paths,
59 }
60 }
61
62 pub fn install(&self) -> Result<(), InstallError> {
64 match self.client_type {
65 ClientType::Cursor => {
66 self.update_config(&self.config_paths.cursor_path)?;
67 }
68 ClientType::Claude => {
69 self.update_config(&self.config_paths.claude_path)?;
70 }
71 ClientType::Both => {
72 self.update_config(&self.config_paths.cursor_path)?;
73 self.update_config(&self.config_paths.claude_path)?;
74 }
75 }
76 Ok(())
77 }
78
79 fn update_config(&self, path: &Path) -> Result<(), InstallError> {
81 let exe_path =
83 std::env::current_exe().map_err(|e| InstallError::ExePathError(e.to_string()))?;
84
85 let mut config: Value = if path.exists() {
87 let contents = fs::read_to_string(path)?;
88 if contents.trim().is_empty() {
89 json!({})
90 } else {
91 serde_json::from_str(&contents)?
92 }
93 } else {
94 json!({})
95 };
96
97 if config.get("mcpServers").is_none() {
99 config["mcpServers"] = json!({});
100 }
101
102 let mcp_servers = config
104 .get_mut("mcpServers")
105 .and_then(|v| v.as_object_mut())
106 .ok_or_else(|| {
107 InstallError::InvalidConfig("mcpServers is not an object".to_string())
108 })?;
109
110 mcp_servers.insert(
112 "magick-mcp".to_string(),
113 json!({
114 "command": exe_path.to_string_lossy().to_string(),
115 "args": ["mcp"]
116 }),
117 );
118
119 if let Some(parent) = path.parent() {
121 fs::create_dir_all(parent)?;
122 }
123
124 let pretty_json = serde_json::to_string_pretty(&config)?;
126 fs::write(path, pretty_json)?;
127
128 Ok(())
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::fs;
136 use tempfile::TempDir;
137
138 fn create_temp_config(dir: &TempDir, filename: &str, content: &str) -> PathBuf {
139 let path = dir.path().join(filename);
140 fs::write(&path, content).unwrap();
141 path
142 }
143
144 #[test]
145 fn test_install_cursor_only() {
146 let temp_dir = TempDir::new().unwrap();
147 let cursor_path = create_temp_config(
148 &temp_dir,
149 "mcp.json",
150 r#"{"mcpServers": {"existing-server": {"command": "existing"}}}"#,
151 );
152 let claude_path = temp_dir.path().join("claude.json");
153
154 let config_paths = ConfigPaths {
155 cursor_path: cursor_path.clone(),
156 claude_path,
157 };
158
159 let installer = MCPInstaller::new(ClientType::Cursor, config_paths);
160 installer.install().unwrap();
161
162 let contents = fs::read_to_string(&cursor_path).unwrap();
163 let config: Value = serde_json::from_str(&contents).unwrap();
164
165 assert!(config["mcpServers"]["magick-mcp"].is_object());
166 assert!(config["mcpServers"]["existing-server"].is_object());
167 assert_eq!(config["mcpServers"]["magick-mcp"]["args"], json!(["mcp"]));
168 }
169
170 #[test]
171 fn test_install_claude_only() {
172 let temp_dir = TempDir::new().unwrap();
173 let cursor_path = temp_dir.path().join("mcp.json");
174 let claude_path = create_temp_config(
175 &temp_dir,
176 "claude.json",
177 r#"{"mcpServers": {"existing-server": {"command": "existing"}}}"#,
178 );
179
180 let config_paths = ConfigPaths {
181 cursor_path,
182 claude_path: claude_path.clone(),
183 };
184
185 let installer = MCPInstaller::new(ClientType::Claude, config_paths);
186 installer.install().unwrap();
187
188 let contents = fs::read_to_string(&claude_path).unwrap();
189 let config: Value = serde_json::from_str(&contents).unwrap();
190
191 assert!(config["mcpServers"]["magick-mcp"].is_object());
192 assert!(config["mcpServers"]["existing-server"].is_object());
193 }
194
195 #[test]
196 fn test_install_both() {
197 let temp_dir = TempDir::new().unwrap();
198 let cursor_path = create_temp_config(
199 &temp_dir,
200 "mcp.json",
201 r#"{"mcpServers": {"cursor-server": {"command": "cursor"}}}"#,
202 );
203 let claude_path = create_temp_config(
204 &temp_dir,
205 "claude.json",
206 r#"{"mcpServers": {"claude-server": {"command": "claude"}}}"#,
207 );
208
209 let config_paths = ConfigPaths {
210 cursor_path: cursor_path.clone(),
211 claude_path: claude_path.clone(),
212 };
213
214 let installer = MCPInstaller::new(ClientType::Both, config_paths);
215 installer.install().unwrap();
216
217 let cursor_contents = fs::read_to_string(&cursor_path).unwrap();
219 let cursor_config: Value = serde_json::from_str(&cursor_contents).unwrap();
220 assert!(cursor_config["mcpServers"]["magick-mcp"].is_object());
221 assert!(cursor_config["mcpServers"]["cursor-server"].is_object());
222
223 let claude_contents = fs::read_to_string(&claude_path).unwrap();
225 let claude_config: Value = serde_json::from_str(&claude_contents).unwrap();
226 assert!(claude_config["mcpServers"]["magick-mcp"].is_object());
227 assert!(claude_config["mcpServers"]["claude-server"].is_object());
228 }
229
230 #[test]
231 fn test_preserve_existing_servers() {
232 let temp_dir = TempDir::new().unwrap();
233 let cursor_path = create_temp_config(
234 &temp_dir,
235 "mcp.json",
236 r#"{
237 "mcpServers": {
238 "server1": {"command": "cmd1", "args": ["arg1"]},
239 "server2": {"command": "cmd2"}
240 }
241 }"#,
242 );
243 let claude_path = temp_dir.path().join("claude.json");
244
245 let config_paths = ConfigPaths {
246 cursor_path: cursor_path.clone(),
247 claude_path,
248 };
249
250 let installer = MCPInstaller::new(ClientType::Cursor, config_paths);
251 installer.install().unwrap();
252
253 let contents = fs::read_to_string(&cursor_path).unwrap();
254 let config: Value = serde_json::from_str(&contents).unwrap();
255
256 assert!(config["mcpServers"]["magick-mcp"].is_object());
257 assert!(config["mcpServers"]["server1"].is_object());
258 assert!(config["mcpServers"]["server2"].is_object());
259 assert_eq!(config["mcpServers"]["server1"]["args"], json!(["arg1"]));
260 }
261
262 #[test]
263 fn test_create_config_when_not_exists() {
264 let temp_dir = TempDir::new().unwrap();
265 let cursor_path = temp_dir.path().join("mcp.json");
266 let claude_path = temp_dir.path().join("claude.json");
267
268 let config_paths = ConfigPaths {
269 cursor_path: cursor_path.clone(),
270 claude_path,
271 };
272
273 let installer = MCPInstaller::new(ClientType::Cursor, config_paths);
274 installer.install().unwrap();
275
276 assert!(cursor_path.exists());
277 let contents = fs::read_to_string(&cursor_path).unwrap();
278 let config: Value = serde_json::from_str(&contents).unwrap();
279
280 assert!(config["mcpServers"]["magick-mcp"].is_object());
281 }
282
283 #[test]
284 fn test_update_existing_magick_mcp() {
285 let temp_dir = TempDir::new().unwrap();
286 let cursor_path = create_temp_config(
287 &temp_dir,
288 "mcp.json",
289 r#"{
290 "mcpServers": {
291 "magick-mcp": {"command": "old-path", "args": ["old"]}
292 }
293 }"#,
294 );
295 let claude_path = temp_dir.path().join("claude.json");
296
297 let config_paths = ConfigPaths {
298 cursor_path: cursor_path.clone(),
299 claude_path,
300 };
301
302 let installer = MCPInstaller::new(ClientType::Cursor, config_paths);
303 installer.install().unwrap();
304
305 let contents = fs::read_to_string(&cursor_path).unwrap();
306 let config: Value = serde_json::from_str(&contents).unwrap();
307
308 assert_eq!(config["mcpServers"]["magick-mcp"]["args"], json!(["mcp"]));
309 assert!(
311 config["mcpServers"]["magick-mcp"]["command"]
312 .as_str()
313 .unwrap()
314 .contains("magick-mcp")
315 );
316 }
317}