1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum IdeType {
9 ClaudeDesktop,
10 ClaudeCode,
11 VsCode,
12 Cursor,
13 Zed,
14 Neovim,
15 Unknown,
16}
17
18impl IdeType {
19 pub fn as_str(&self) -> &str {
20 match self {
21 Self::ClaudeDesktop => "Claude Desktop",
22 Self::ClaudeCode => "Claude Code",
23 Self::VsCode => "VS Code",
24 Self::Cursor => "Cursor",
25 Self::Zed => "Zed",
26 Self::Neovim => "Neovim",
27 Self::Unknown => "Unknown",
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33pub enum ConfigFormat {
34 Json,
35 JsonLocal,
36 Toml,
37 Yaml,
38}
39
40impl ConfigFormat {
41 pub fn extension(&self) -> &str {
42 match self {
43 Self::Json => "json",
44 Self::JsonLocal => "json.local",
45 Self::Toml => "toml",
46 Self::Yaml => "yaml",
47 }
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ConfigLocation {
53 pub path: PathBuf,
54 pub format: ConfigFormat,
55 pub exists: bool,
56 pub ide: IdeType,
57}
58
59impl ConfigLocation {
60 fn new(path: PathBuf, format: ConfigFormat, ide: IdeType) -> Self {
61 let exists = path.exists();
62 Self {
63 path,
64 format,
65 exists,
66 ide,
67 }
68 }
69}
70
71pub struct McpDetector {
72 platform: Platform,
73}
74
75#[derive(Debug, Clone, Copy)]
76enum Platform {
77 Windows,
78 MacOs,
79 Linux,
80}
81
82impl McpDetector {
83 pub fn new() -> Self {
84 let platform = if cfg!(target_os = "windows") {
85 Platform::Windows
86 } else if cfg!(target_os = "macos") {
87 Platform::MacOs
88 } else {
89 Platform::Linux
90 };
91
92 Self { platform }
93 }
94
95 pub fn detect_all(&self) -> Result<Vec<ConfigLocation>> {
97 let mut locations = Vec::new();
98
99 locations.extend(self.get_claude_desktop_configs()?);
101
102 locations.extend(self.get_claude_code_configs()?);
104
105 locations.extend(self.get_vscode_configs()?);
107
108 locations.extend(self.get_cursor_configs()?);
110
111 locations.extend(self.get_zed_configs()?);
113
114 locations.extend(self.get_neovim_configs()?);
116
117 locations.extend(self.get_global_mcp_configs()?);
119
120 Ok(locations)
121 }
122
123 pub fn detect_active_ide(&self) -> Result<IdeType> {
125 if env::var("CLAUDE_CODE").is_ok() {
127 return Ok(IdeType::ClaudeCode);
128 }
129
130 if env::var("CLAUDE_DESKTOP").is_ok() {
131 return Ok(IdeType::ClaudeDesktop);
132 }
133
134 if env::var("VSCODE_PID").is_ok() {
135 return Ok(IdeType::VsCode);
136 }
137
138 if env::var("CURSOR_PID").is_ok() {
139 return Ok(IdeType::Cursor);
140 }
141
142 if env::var("NVIM").is_ok() || env::var("NVIM_LISTEN_ADDRESS").is_ok() {
143 return Ok(IdeType::Neovim);
144 }
145
146 match self.platform {
148 Platform::Windows => self.detect_windows_processes(),
149 Platform::MacOs => self.detect_macos_processes(),
150 Platform::Linux => self.detect_linux_processes(),
151 }
152 }
153
154 pub fn get_config_location(&self, ide: IdeType) -> Result<ConfigLocation> {
156 let path = self.get_config_path(ide)?;
157 let format = ConfigFormat::Json; Ok(ConfigLocation::new(path, format, ide))
159 }
160
161 pub fn get_config_path(&self, ide: IdeType) -> Result<PathBuf> {
163 match ide {
164 IdeType::ClaudeDesktop => self.get_claude_desktop_config_path(),
165 IdeType::ClaudeCode => self.get_claude_code_config_path(),
166 IdeType::VsCode => self.get_vscode_config_path(),
167 IdeType::Cursor => self.get_cursor_config_path(),
168 IdeType::Zed => self.get_zed_config_path(),
169 IdeType::Neovim => self.get_neovim_config_path(),
170 IdeType::Unknown => Err(anyhow::anyhow!("Cannot get config path for unknown IDE")),
171 }
172 }
173
174 pub fn detect_claude_configs(&self) -> Result<Vec<ConfigLocation>> {
176 let mut configs = Vec::new();
177 configs.extend(self.get_claude_desktop_configs()?);
178 configs.extend(self.get_claude_code_configs()?);
179 Ok(configs)
180 }
181
182 fn get_home_dir(&self) -> Result<PathBuf> {
184 match self.platform {
185 Platform::Windows => env::var("USERPROFILE")
186 .or_else(|_| {
187 env::var("HOMEDRIVE").and_then(|drive| {
188 env::var("HOMEPATH").map(|path| format!("{}{}", drive, path))
189 })
190 })
191 .map(PathBuf::from)
192 .context("Failed to get Windows home directory"),
193 Platform::MacOs | Platform::Linux => env::var("HOME")
194 .map(PathBuf::from)
195 .context("Failed to get Unix home directory"),
196 }
197 }
198
199 fn get_config_dir(&self) -> Result<PathBuf> {
200 match self.platform {
201 Platform::Windows => env::var("APPDATA")
202 .map(PathBuf::from)
203 .context("Failed to get Windows config directory"),
204 Platform::MacOs => {
205 let home = self.get_home_dir()?;
206 Ok(home.join("Library").join("Application Support"))
207 },
208 Platform::Linux => env::var("XDG_CONFIG_HOME")
209 .map(PathBuf::from)
210 .or_else(|_| self.get_home_dir().map(|home| home.join(".config")))
211 .context("Failed to get Linux config directory"),
212 }
213 }
214
215 fn get_claude_desktop_configs(&self) -> Result<Vec<ConfigLocation>> {
217 let mut configs = Vec::new();
218
219 match self.platform {
220 Platform::Windows => {
221 let appdata = env::var("APPDATA").map(PathBuf::from)?;
222 configs.push(ConfigLocation::new(
223 appdata.join("Claude").join("claude_desktop_config.json"),
224 ConfigFormat::Json,
225 IdeType::ClaudeDesktop,
226 ));
227 },
228 Platform::MacOs => {
229 let home = self.get_home_dir()?;
230 configs.push(ConfigLocation::new(
231 home.join("Library")
232 .join("Application Support")
233 .join("Claude")
234 .join("claude_desktop_config.json"),
235 ConfigFormat::Json,
236 IdeType::ClaudeDesktop,
237 ));
238 },
239 Platform::Linux => {
240 let config = self.get_config_dir()?;
241 configs.push(ConfigLocation::new(
242 config.join("claude").join("claude_desktop_config.json"),
243 ConfigFormat::Json,
244 IdeType::ClaudeDesktop,
245 ));
246 },
247 }
248
249 Ok(configs)
250 }
251
252 fn get_claude_code_configs(&self) -> Result<Vec<ConfigLocation>> {
254 let mut configs = Vec::new();
255 let home = self.get_home_dir()?;
256
257 configs.push(ConfigLocation::new(
259 home.join(".mcp.json"),
260 ConfigFormat::Json,
261 IdeType::ClaudeCode,
262 ));
263
264 configs.push(ConfigLocation::new(
266 home.join(".mcp.json.local"),
267 ConfigFormat::JsonLocal,
268 IdeType::ClaudeCode,
269 ));
270
271 match self.platform {
273 Platform::Windows => {
274 let appdata = env::var("APPDATA").map(PathBuf::from)?;
275 configs.push(ConfigLocation::new(
276 appdata.join("claude-code").join("mcp.json"),
277 ConfigFormat::Json,
278 IdeType::ClaudeCode,
279 ));
280 },
281 Platform::MacOs => {
282 configs.push(ConfigLocation::new(
283 home.join("Library")
284 .join("Application Support")
285 .join("claude-code")
286 .join("mcp.json"),
287 ConfigFormat::Json,
288 IdeType::ClaudeCode,
289 ));
290 },
291 Platform::Linux => {
292 let config = self.get_config_dir()?;
293 configs.push(ConfigLocation::new(
294 config.join("claude-code").join("mcp.json"),
295 ConfigFormat::Json,
296 IdeType::ClaudeCode,
297 ));
298 },
299 }
300
301 Ok(configs)
302 }
303
304 fn get_vscode_configs(&self) -> Result<Vec<ConfigLocation>> {
306 let mut configs = Vec::new();
307
308 match self.platform {
309 Platform::Windows => {
310 let appdata = env::var("APPDATA").map(PathBuf::from)?;
311 configs.push(ConfigLocation::new(
312 appdata.join("Code").join("User").join("mcp.json"),
313 ConfigFormat::Json,
314 IdeType::VsCode,
315 ));
316 },
317 Platform::MacOs => {
318 let home = self.get_home_dir()?;
319 configs.push(ConfigLocation::new(
320 home.join("Library")
321 .join("Application Support")
322 .join("Code")
323 .join("User")
324 .join("mcp.json"),
325 ConfigFormat::Json,
326 IdeType::VsCode,
327 ));
328 },
329 Platform::Linux => {
330 let config = self.get_config_dir()?;
331 configs.push(ConfigLocation::new(
332 config.join("Code").join("User").join("mcp.json"),
333 ConfigFormat::Json,
334 IdeType::VsCode,
335 ));
336 },
337 }
338
339 Ok(configs)
340 }
341
342 fn get_cursor_configs(&self) -> Result<Vec<ConfigLocation>> {
344 let mut configs = Vec::new();
345
346 match self.platform {
347 Platform::Windows => {
348 let appdata = env::var("APPDATA").map(PathBuf::from)?;
349 configs.push(ConfigLocation::new(
350 appdata.join("Cursor").join("User").join("mcp.json"),
351 ConfigFormat::Json,
352 IdeType::Cursor,
353 ));
354 },
355 Platform::MacOs => {
356 let home = self.get_home_dir()?;
357 configs.push(ConfigLocation::new(
358 home.join("Library")
359 .join("Application Support")
360 .join("Cursor")
361 .join("User")
362 .join("mcp.json"),
363 ConfigFormat::Json,
364 IdeType::Cursor,
365 ));
366 },
367 Platform::Linux => {
368 let config = self.get_config_dir()?;
369 configs.push(ConfigLocation::new(
370 config.join("Cursor").join("User").join("mcp.json"),
371 ConfigFormat::Json,
372 IdeType::Cursor,
373 ));
374 },
375 }
376
377 Ok(configs)
378 }
379
380 fn get_zed_configs(&self) -> Result<Vec<ConfigLocation>> {
382 let mut configs = Vec::new();
383
384 match self.platform {
385 Platform::Windows => {
386 let appdata = env::var("APPDATA").map(PathBuf::from)?;
387 configs.push(ConfigLocation::new(
388 appdata.join("Zed").join("mcp.json"),
389 ConfigFormat::Json,
390 IdeType::Zed,
391 ));
392 },
393 Platform::MacOs => {
394 let home = self.get_home_dir()?;
395 configs.push(ConfigLocation::new(
396 home.join("Library")
397 .join("Application Support")
398 .join("Zed")
399 .join("mcp.json"),
400 ConfigFormat::Json,
401 IdeType::Zed,
402 ));
403 },
404 Platform::Linux => {
405 let config_home = env::var("XDG_CONFIG_HOME")
406 .map(PathBuf::from)
407 .unwrap_or_else(|_| {
408 let home = self.get_home_dir().unwrap_or_default();
409 home.join(".config")
410 });
411 configs.push(ConfigLocation::new(
412 config_home.join("zed").join("mcp.json"),
413 ConfigFormat::Json,
414 IdeType::Zed,
415 ));
416 },
417 }
418
419 Ok(configs)
420 }
421
422 fn get_neovim_configs(&self) -> Result<Vec<ConfigLocation>> {
424 let mut configs = Vec::new();
425 let home = self.get_home_dir()?;
426
427 configs.push(ConfigLocation::new(
429 home.join(".config").join("nvim").join("mcp.json"),
430 ConfigFormat::Json,
431 IdeType::Neovim,
432 ));
433
434 configs.push(ConfigLocation::new(
435 home.join(".config").join("nvim").join("mcp.toml"),
436 ConfigFormat::Toml,
437 IdeType::Neovim,
438 ));
439
440 configs.push(ConfigLocation::new(
442 home.join(".nvim").join("mcp.json"),
443 ConfigFormat::Json,
444 IdeType::Neovim,
445 ));
446
447 Ok(configs)
448 }
449
450 fn get_global_mcp_configs(&self) -> Result<Vec<ConfigLocation>> {
452 let mut configs = Vec::new();
453 let home = self.get_home_dir()?;
454
455 configs.push(ConfigLocation::new(
457 home.join(".mcp").join("config.json"),
458 ConfigFormat::Json,
459 IdeType::Unknown,
460 ));
461
462 configs.push(ConfigLocation::new(
463 home.join(".mcp").join("config.toml"),
464 ConfigFormat::Toml,
465 IdeType::Unknown,
466 ));
467
468 configs.push(ConfigLocation::new(
469 home.join(".mcp").join("config.yaml"),
470 ConfigFormat::Yaml,
471 IdeType::Unknown,
472 ));
473
474 match self.platform {
476 Platform::Windows => {
477 let programdata = env::var("PROGRAMDATA")
478 .map(PathBuf::from)
479 .unwrap_or_else(|_| PathBuf::from("C:\\ProgramData"));
480 configs.push(ConfigLocation::new(
481 programdata.join("mcp").join("config.json"),
482 ConfigFormat::Json,
483 IdeType::Unknown,
484 ));
485 },
486 Platform::MacOs | Platform::Linux => {
487 configs.push(ConfigLocation::new(
488 PathBuf::from("/etc/mcp/config.json"),
489 ConfigFormat::Json,
490 IdeType::Unknown,
491 ));
492 configs.push(ConfigLocation::new(
493 PathBuf::from("/etc/mcp/config.toml"),
494 ConfigFormat::Toml,
495 IdeType::Unknown,
496 ));
497 },
498 }
499
500 Ok(configs)
501 }
502
503 fn detect_windows_processes(&self) -> Result<IdeType> {
505 use std::process::Command;
506
507 let output = Command::new("tasklist")
508 .output()
509 .context("Failed to run tasklist")?;
510
511 let processes = String::from_utf8_lossy(&output.stdout);
512
513 if processes.contains("Claude.exe") || processes.contains("claude.exe") {
514 return Ok(IdeType::ClaudeDesktop);
515 }
516
517 if processes.contains("ClaudeCode.exe") || processes.contains("claude-code.exe") {
518 return Ok(IdeType::ClaudeCode);
519 }
520
521 if processes.contains("Code.exe") {
522 return Ok(IdeType::VsCode);
523 }
524
525 if processes.contains("Cursor.exe") {
526 return Ok(IdeType::Cursor);
527 }
528
529 if processes.contains("nvim.exe") || processes.contains("nvim-qt.exe") {
530 return Ok(IdeType::Neovim);
531 }
532
533 Ok(IdeType::Unknown)
534 }
535
536 fn detect_macos_processes(&self) -> Result<IdeType> {
537 use std::process::Command;
538
539 let output = Command::new("ps")
540 .args(&["-ax"])
541 .output()
542 .context("Failed to run ps")?;
543
544 let processes = String::from_utf8_lossy(&output.stdout);
545
546 if processes.contains("Claude.app") || processes.contains("Claude Desktop") {
547 return Ok(IdeType::ClaudeDesktop);
548 }
549
550 if processes.contains("Claude Code") || processes.contains("claude-code") {
551 return Ok(IdeType::ClaudeCode);
552 }
553
554 if processes.contains("Visual Studio Code.app") || processes.contains("Code Helper") {
555 return Ok(IdeType::VsCode);
556 }
557
558 if processes.contains("Cursor.app") || processes.contains("Cursor Helper") {
559 return Ok(IdeType::Cursor);
560 }
561
562 if processes.contains("nvim") {
563 return Ok(IdeType::Neovim);
564 }
565
566 Ok(IdeType::Unknown)
567 }
568
569 fn detect_linux_processes(&self) -> Result<IdeType> {
570 use std::process::Command;
571
572 let output = Command::new("ps")
573 .args(&["aux"])
574 .output()
575 .context("Failed to run ps")?;
576
577 let processes = String::from_utf8_lossy(&output.stdout);
578
579 if processes.contains("claude-desktop") || processes.contains("Claude") {
580 return Ok(IdeType::ClaudeDesktop);
581 }
582
583 if processes.contains("claude-code") {
584 return Ok(IdeType::ClaudeCode);
585 }
586
587 if processes.contains("code") && !processes.contains("claude-code") {
588 return Ok(IdeType::VsCode);
589 }
590
591 if processes.contains("cursor") {
592 return Ok(IdeType::Cursor);
593 }
594
595 if processes.contains("nvim") {
596 return Ok(IdeType::Neovim);
597 }
598
599 Ok(IdeType::Unknown)
600 }
601
602 fn get_claude_desktop_config_path(&self) -> Result<PathBuf> {
604 match self.platform {
605 Platform::Windows => {
606 let appdata = env::var("APPDATA").map(PathBuf::from)?;
607 Ok(appdata.join("Claude").join("claude_desktop_config.json"))
608 },
609 Platform::MacOs => {
610 let home = self.get_home_dir()?;
611 Ok(home
612 .join("Library")
613 .join("Application Support")
614 .join("Claude")
615 .join("claude_desktop_config.json"))
616 },
617 Platform::Linux => {
618 let config = self.get_config_dir()?;
619 Ok(config.join("claude").join("claude_desktop_config.json"))
620 },
621 }
622 }
623
624 fn get_claude_code_config_path(&self) -> Result<PathBuf> {
625 let home = self.get_home_dir()?;
626 Ok(home.join(".mcp.json"))
627 }
628
629 fn get_vscode_config_path(&self) -> Result<PathBuf> {
630 match self.platform {
631 Platform::Windows => {
632 let appdata = env::var("APPDATA").map(PathBuf::from)?;
633 Ok(appdata.join("Code").join("User").join("mcp.json"))
634 },
635 Platform::MacOs => {
636 let home = self.get_home_dir()?;
637 Ok(home
638 .join("Library")
639 .join("Application Support")
640 .join("Code")
641 .join("User")
642 .join("mcp.json"))
643 },
644 Platform::Linux => {
645 let config = self.get_config_dir()?;
646 Ok(config.join("Code").join("User").join("mcp.json"))
647 },
648 }
649 }
650
651 fn get_cursor_config_path(&self) -> Result<PathBuf> {
652 match self.platform {
653 Platform::Windows => {
654 let appdata = env::var("APPDATA").map(PathBuf::from)?;
655 Ok(appdata.join("Cursor").join("User").join("mcp.json"))
656 },
657 Platform::MacOs => {
658 let home = self.get_home_dir()?;
659 Ok(home
660 .join("Library")
661 .join("Application Support")
662 .join("Cursor")
663 .join("User")
664 .join("mcp.json"))
665 },
666 Platform::Linux => {
667 let config = self.get_config_dir()?;
668 Ok(config.join("Cursor").join("User").join("mcp.json"))
669 },
670 }
671 }
672
673 fn get_zed_config_path(&self) -> Result<PathBuf> {
674 match self.platform {
675 Platform::Windows => {
676 let appdata = env::var("APPDATA").map(PathBuf::from)?;
677 Ok(appdata.join("Zed").join("mcp.json"))
678 },
679 Platform::MacOs => {
680 let home = self.get_home_dir()?;
681 Ok(home
682 .join("Library")
683 .join("Application Support")
684 .join("Zed")
685 .join("mcp.json"))
686 },
687 Platform::Linux => {
688 let config_home = env::var("XDG_CONFIG_HOME")
689 .map(PathBuf::from)
690 .unwrap_or_else(|_| {
691 let home = self.get_home_dir().unwrap_or_default();
692 home.join(".config")
693 });
694 Ok(config_home.join("zed").join("mcp.json"))
695 },
696 }
697 }
698
699 fn get_neovim_config_path(&self) -> Result<PathBuf> {
700 let home = self.get_home_dir()?;
701 Ok(home.join(".config").join("nvim").join("mcp.json"))
702 }
703
704 pub fn is_server_configured(&self, server_name: &str) -> Result<bool> {
706 let configs = self.detect_claude_configs()?;
707
708 for config in configs.iter().filter(|c| c.exists) {
709 if self.check_server_in_config(&config.path, server_name)? {
710 return Ok(true);
711 }
712 }
713
714 Ok(false)
715 }
716
717 fn check_server_in_config(&self, path: &Path, server_name: &str) -> Result<bool> {
718 let content = fs::read_to_string(path)?;
719
720 Ok(content.contains(server_name))
722 }
723
724 pub fn get_status_summary(&self) -> Result<String> {
726 let active_ide = self.detect_active_ide()?;
727 let all_configs = self.detect_all()?;
728 let existing_configs: Vec<_> = all_configs.iter().filter(|c| c.exists).collect();
729
730 let mut summary = format!("MCP Configuration Status\n");
731 summary.push_str(&format!("========================\n"));
732 summary.push_str(&format!("Active IDE: {}\n", active_ide.as_str()));
733 summary.push_str(&format!("Platform: {:?}\n", self.platform));
734 summary.push_str(&format!(
735 "Total config locations checked: {}\n",
736 all_configs.len()
737 ));
738 summary.push_str(&format!(
739 "Existing configurations: {}\n\n",
740 existing_configs.len()
741 ));
742
743 if !existing_configs.is_empty() {
744 summary.push_str("Found configurations:\n");
745 for config in existing_configs {
746 summary.push_str(&format!(
747 " - {} ({:?}): {}\n",
748 config.ide.as_str(),
749 config.format,
750 config.path.display()
751 ));
752 }
753 }
754
755 Ok(summary)
756 }
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762
763 #[test]
764 fn test_detector_creation() {
765 let detector = McpDetector::new();
766 assert!(matches!(
767 detector.platform,
768 Platform::Windows | Platform::MacOs | Platform::Linux
769 ));
770 }
771
772 #[test]
773 fn test_config_format_extensions() {
774 assert_eq!(ConfigFormat::Json.extension(), "json");
775 assert_eq!(ConfigFormat::JsonLocal.extension(), "json.local");
776 assert_eq!(ConfigFormat::Toml.extension(), "toml");
777 assert_eq!(ConfigFormat::Yaml.extension(), "yaml");
778 }
779
780 #[test]
781 fn test_ide_type_strings() {
782 assert_eq!(IdeType::ClaudeDesktop.as_str(), "Claude Desktop");
783 assert_eq!(IdeType::ClaudeCode.as_str(), "Claude Code");
784 assert_eq!(IdeType::VsCode.as_str(), "VS Code");
785 assert_eq!(IdeType::Cursor.as_str(), "Cursor");
786 assert_eq!(IdeType::Neovim.as_str(), "Neovim");
787 assert_eq!(IdeType::Unknown.as_str(), "Unknown");
788 }
789}