1pub mod handlers;
14
15use anyhow::{Context, Result, anyhow};
16use chrono::Utc;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::collections::HashMap;
20use std::path::Path;
21
22#[derive(Debug, Default, Serialize, Deserialize)]
25pub struct ClaudeSettings {
26 #[serde(rename = "mcpServers", skip_serializing_if = "Option::is_none")]
28 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub hooks: Option<Value>,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub permissions: Option<Value>,
37
38 #[serde(flatten)]
40 pub other: HashMap<String, Value>,
41}
42
43#[derive(Debug, Default, Serialize, Deserialize)]
49pub struct McpConfig {
50 #[serde(rename = "mcpServers")]
52 pub mcp_servers: HashMap<String, McpServerConfig>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct McpServerConfig {
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub command: Option<String>,
64
65 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 pub args: Vec<String>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub env: Option<HashMap<String, Value>>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub r#type: Option<String>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub url: Option<String>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub headers: Option<HashMap<String, Value>>,
84
85 #[serde(rename = "_agpm", skip_serializing_if = "Option::is_none")]
87 pub agpm_metadata: Option<AgpmMetadata>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct AgpmMetadata {
96 pub managed: bool,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub source: Option<String>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub version: Option<String>,
106
107 pub installed_at: String,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub dependency_name: Option<String>,
113}
114
115impl ClaudeSettings {
116 pub fn load_or_default(path: &Path) -> Result<Self> {
120 if path.exists() {
121 crate::utils::read_json_file(path).with_context(|| {
122 format!(
123 "Failed to parse settings file: {}\n\
124 The file may be malformed or contain invalid JSON.",
125 path.display()
126 )
127 })
128 } else {
129 Ok(Self::default())
130 }
131 }
132
133 pub fn save(&self, path: &Path) -> Result<()> {
137 if path.exists() {
139 let agpm_dir =
141 path.parent().ok_or_else(|| anyhow!("Invalid settings path"))?.join("agpm");
142
143 if !agpm_dir.exists() {
145 std::fs::create_dir_all(&agpm_dir).with_context(|| {
146 format!("Failed to create directory: {}", agpm_dir.display())
147 })?;
148 }
149
150 let backup_path = agpm_dir.join("settings.local.json.backup");
151 std::fs::copy(path, &backup_path).with_context(|| {
152 format!("Failed to create backup of settings at: {}", backup_path.display())
153 })?;
154 }
155
156 crate::utils::write_json_file(path, self, true)
158 .with_context(|| format!("Failed to write settings to: {}", path.display()))?;
159
160 Ok(())
161 }
162
163 pub fn update_mcp_servers(&mut self, mcp_servers_dir: &Path) -> Result<()> {
168 if !mcp_servers_dir.exists() {
169 return Ok(());
170 }
171
172 let mut agpm_servers = HashMap::new();
173
174 for entry in std::fs::read_dir(mcp_servers_dir).with_context(|| {
176 format!("Failed to read MCP servers directory: {}", mcp_servers_dir.display())
177 })? {
178 let entry = entry?;
179 let path = entry.path();
180
181 if path.extension().is_some_and(|ext| ext == "json") {
182 let server_config: McpServerConfig = crate::utils::read_json_file(&path)
183 .with_context(|| {
184 format!("Failed to parse MCP server file: {}", path.display())
185 })?;
186
187 if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
189 agpm_servers.insert(name.to_string(), server_config);
190 }
191 }
192 }
193
194 if self.mcp_servers.is_none() {
196 self.mcp_servers = Some(HashMap::new());
197 }
198
199 if let Some(servers) = &mut self.mcp_servers {
201 servers
203 .retain(|_, config| config.agpm_metadata.as_ref().is_none_or(|meta| !meta.managed));
204
205 servers.extend(agpm_servers);
207 }
208
209 Ok(())
210 }
211}
212
213impl McpConfig {
214 pub fn load_or_default(path: &Path) -> Result<Self> {
219 if path.exists() {
220 crate::utils::read_json_file(path).with_context(|| {
222 format!(
223 "Failed to parse MCP configuration file: {}\n\
224 The file may be malformed or contain invalid JSON.",
225 path.display()
226 )
227 })
228 } else {
229 Ok(Self::default())
230 }
231 }
232
233 pub fn save(&self, path: &Path) -> Result<()> {
237 if path.exists() {
239 let agpm_dir = path
241 .parent()
242 .ok_or_else(|| anyhow!("Invalid MCP config path"))?
243 .join(".claude")
244 .join("agpm");
245
246 if !agpm_dir.exists() {
248 std::fs::create_dir_all(&agpm_dir).with_context(|| {
249 format!("Failed to create directory: {}", agpm_dir.display())
250 })?;
251 }
252
253 let backup_path = agpm_dir.join(".mcp.json.backup");
254 std::fs::copy(path, &backup_path).with_context(|| {
255 format!(
256 "Failed to create backup of MCP configuration at: {}",
257 backup_path.display()
258 )
259 })?;
260 }
261
262 crate::utils::write_json_file(path, self, true)
264 .with_context(|| format!("Failed to write MCP configuration to: {}", path.display()))?;
265
266 Ok(())
267 }
268
269 pub fn update_managed_servers(
276 &mut self,
277 updates: HashMap<String, McpServerConfig>,
278 ) -> Result<()> {
279 let updating_names: std::collections::HashSet<_> = updates.keys().cloned().collect();
281
282 self.mcp_servers.retain(|name, config| {
284 config
288 .agpm_metadata
289 .as_ref()
290 .is_none_or(|meta| !meta.managed || updating_names.contains(name))
291 });
292
293 for (name, config) in updates {
295 self.mcp_servers.insert(name, config);
296 }
297
298 Ok(())
299 }
300
301 #[must_use]
306 pub fn check_conflicts(&self, new_servers: &HashMap<String, McpServerConfig>) -> Vec<String> {
307 let mut conflicts = Vec::new();
308
309 for name in new_servers.keys() {
310 if let Some(existing) = self.mcp_servers.get(name) {
311 if existing.agpm_metadata.is_none()
313 || !existing.agpm_metadata.as_ref().unwrap().managed
314 {
315 conflicts.push(name.clone());
316 }
317 }
318 }
319
320 conflicts
321 }
322
323 pub fn remove_all_managed(&mut self) {
327 self.mcp_servers
328 .retain(|_, config| config.agpm_metadata.as_ref().is_none_or(|meta| !meta.managed));
329 }
330
331 #[must_use]
333 pub fn get_managed_servers(&self) -> HashMap<String, &McpServerConfig> {
334 self.mcp_servers
335 .iter()
336 .filter(|(_, config)| config.agpm_metadata.as_ref().is_some_and(|meta| meta.managed))
337 .map(|(name, config)| (name.clone(), config))
338 .collect()
339 }
340}
341
342pub async fn merge_mcp_servers(
347 mcp_config_path: &Path,
348 agpm_servers: HashMap<String, McpServerConfig>,
349) -> Result<()> {
350 if agpm_servers.is_empty() {
351 return Ok(());
352 }
353
354 let mut mcp_config = McpConfig::load_or_default(mcp_config_path)?;
356
357 let conflicts = mcp_config.check_conflicts(&agpm_servers);
359 if !conflicts.is_empty() {
360 return Err(anyhow::anyhow!(
361 "The following MCP servers already exist and are not managed by AGPM: {}\n\
362 Please rename your servers or remove the existing ones from .mcp.json",
363 conflicts.join(", ")
364 ));
365 }
366
367 mcp_config.update_managed_servers(agpm_servers)?;
369
370 mcp_config.save(mcp_config_path)?;
372
373 Ok(())
374}
375
376pub async fn configure_mcp_servers(project_root: &Path, mcp_servers_dir: &Path) -> Result<()> {
377 if !mcp_servers_dir.exists() {
378 return Ok(());
379 }
380
381 let mcp_config_path = project_root.join(".mcp.json");
382
383 let mut agpm_servers = HashMap::new();
385 let mut entries = tokio::fs::read_dir(mcp_servers_dir).await?;
386 while let Some(entry) = entries.next_entry().await? {
387 let path = entry.path();
388
389 if path.extension().is_some_and(|ext| ext == "json")
390 && let Some(name) = path.file_stem().and_then(|s| s.to_str())
391 {
392 let config: McpServerConfig = crate::utils::read_json_file(&path)
394 .with_context(|| format!("Failed to parse MCP server file: {}", path.display()))?;
395
396 let mut config_with_metadata = config;
398 if config_with_metadata.agpm_metadata.is_none() {
399 config_with_metadata.agpm_metadata = Some(AgpmMetadata {
400 managed: true,
401 source: Some("agpm".to_string()),
402 version: None,
403 installed_at: Utc::now().to_rfc3339(),
404 dependency_name: Some(name.to_string()),
405 });
406 }
407
408 agpm_servers.insert(name.to_string(), config_with_metadata);
409 }
410 }
411
412 merge_mcp_servers(&mcp_config_path, agpm_servers).await
414}
415
416pub fn clean_mcp_servers(project_root: &Path) -> Result<()> {
418 let claude_dir = project_root.join(".claude");
419 let agpm_dir = claude_dir.join("agpm");
420 let mcp_servers_dir = agpm_dir.join("mcp-servers");
421 let mcp_config_path = project_root.join(".mcp.json");
422
423 let mut removed_count = 0;
425 if mcp_servers_dir.exists() {
426 for entry in std::fs::read_dir(&mcp_servers_dir)? {
427 let entry = entry?;
428 let path = entry.path();
429 if path.extension().is_some_and(|ext| ext == "json") {
430 std::fs::remove_file(&path).with_context(|| {
431 format!("Failed to remove MCP server file: {}", path.display())
432 })?;
433 removed_count += 1;
434 }
435 }
436 }
437
438 if mcp_config_path.exists() {
440 let mut mcp_config = McpConfig::load_or_default(&mcp_config_path)?;
441 mcp_config.remove_all_managed();
442 mcp_config.save(&mcp_config_path)?;
443 }
444
445 if removed_count == 0 {
446 println!("No AGPM-managed MCP servers found");
447 } else {
448 println!("✓ Removed {removed_count} AGPM-managed MCP server(s)");
449 }
450
451 Ok(())
452}
453
454pub fn list_mcp_servers(project_root: &Path) -> Result<()> {
456 let mcp_config_path = project_root.join(".mcp.json");
457
458 if !mcp_config_path.exists() {
459 println!("No .mcp.json file found");
460 return Ok(());
461 }
462
463 let mcp_config = McpConfig::load_or_default(&mcp_config_path)?;
464
465 if mcp_config.mcp_servers.is_empty() {
466 println!("No MCP servers configured");
467 return Ok(());
468 }
469
470 let servers = &mcp_config.mcp_servers;
471 println!("MCP Servers:");
472 println!("â•─────────────────────┬──────────┬───────────╮");
473 println!("│ Name │ Managed │ Version │");
474 println!("├─────────────────────┼──────────┼───────────┤");
475
476 for (name, server) in servers {
477 let (managed, version) = if let Some(meta) = &server.agpm_metadata {
478 if meta.managed {
479 ("✓ (agpm)", meta.version.as_deref().unwrap_or("-"))
480 } else {
481 ("✗", "-")
482 }
483 } else {
484 ("✗", "-")
485 };
486
487 println!("│ {name:<19} │ {managed:<8} │ {version:<9} │");
488 }
489
490 println!("╰─────────────────────┴──────────┴───────────╯");
491
492 Ok(())
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use serde_json::json;
499 use std::fs;
500 use tempfile::tempdir;
501
502 #[test]
503 fn test_claude_settings_load_save() {
504 let temp = tempdir().unwrap();
505 let settings_path = temp.path().join("settings.local.json");
506
507 let mut settings = ClaudeSettings::default();
508 let mut servers = HashMap::new();
509 servers.insert(
510 "test-server".to_string(),
511 McpServerConfig {
512 command: Some("node".to_string()),
513 args: vec!["server.js".to_string()],
514 env: None,
515 r#type: None,
516 url: None,
517 headers: None,
518 agpm_metadata: None,
519 },
520 );
521 settings.mcp_servers = Some(servers);
522
523 settings.save(&settings_path).unwrap();
524
525 let loaded = ClaudeSettings::load_or_default(&settings_path).unwrap();
526 assert!(loaded.mcp_servers.is_some());
527 let servers = loaded.mcp_servers.unwrap();
528 assert_eq!(servers.len(), 1);
529 assert!(servers.contains_key("test-server"));
530 }
531
532 #[test]
533 fn test_claude_settings_load_nonexistent_file() {
534 let temp = tempdir().unwrap();
535 let settings_path = temp.path().join("nonexistent.json");
536
537 let settings = ClaudeSettings::load_or_default(&settings_path).unwrap();
538 assert!(settings.mcp_servers.is_none());
539 assert!(settings.permissions.is_none());
540 assert!(settings.other.is_empty());
541 }
542
543 #[test]
544 fn test_claude_settings_load_invalid_json() {
545 let temp = tempdir().unwrap();
546 let settings_path = temp.path().join("invalid.json");
547 fs::write(&settings_path, "invalid json {").unwrap();
548
549 let result = ClaudeSettings::load_or_default(&settings_path);
550 assert!(result.is_err());
551 assert!(result.unwrap_err().to_string().contains("Failed to parse"));
552 }
553
554 #[test]
555 fn test_claude_settings_save_creates_backup() {
556 let temp = tempdir().unwrap();
557 let settings_path = temp.path().join("settings.local.json");
558 let backup_path = temp.path().join("agpm").join("settings.local.json.backup");
559
560 fs::write(&settings_path, r#"{"test": "value"}"#).unwrap();
562
563 let settings = ClaudeSettings::default();
564 settings.save(&settings_path).unwrap();
565
566 assert!(backup_path.exists());
568 let backup_content = fs::read_to_string(backup_path).unwrap();
569 assert_eq!(backup_content, r#"{"test": "value"}"#);
570 }
571
572 #[test]
573 fn test_claude_settings_update_mcp_servers_empty_dir() {
574 let temp = tempdir().unwrap();
575 let nonexistent_dir = temp.path().join("nonexistent");
576
577 let mut settings = ClaudeSettings::default();
578 settings.update_mcp_servers(&nonexistent_dir).unwrap();
580 }
581
582 #[test]
583 fn test_update_mcp_servers_from_directory() {
584 let temp = tempdir().unwrap();
585 let mcp_servers_dir = temp.path().join("mcp-servers");
586 std::fs::create_dir(&mcp_servers_dir).unwrap();
587
588 let server_config = McpServerConfig {
590 command: Some("managed".to_string()),
591 args: vec![],
592 env: None,
593 r#type: None,
594 url: None,
595 headers: None,
596 agpm_metadata: Some(AgpmMetadata {
597 managed: true,
598 source: Some("test".to_string()),
599 version: Some("v1.0.0".to_string()),
600 installed_at: "2024-01-01T00:00:00Z".to_string(),
601 dependency_name: Some("agpm-server".to_string()),
602 }),
603 };
604 let config_path = mcp_servers_dir.join("agpm-server.json");
605 let json = serde_json::to_string_pretty(&server_config).unwrap();
606 std::fs::write(&config_path, json).unwrap();
607
608 let mut settings = ClaudeSettings::default();
610 let mut servers = HashMap::new();
611 servers.insert(
612 "user-server".to_string(),
613 McpServerConfig {
614 command: Some("custom".to_string()),
615 args: vec![],
616 env: None,
617 r#type: None,
618 url: None,
619 headers: None,
620 agpm_metadata: None,
621 },
622 );
623 settings.mcp_servers = Some(servers);
624
625 settings.update_mcp_servers(&mcp_servers_dir).unwrap();
627
628 assert!(settings.mcp_servers.is_some());
630 let servers = settings.mcp_servers.as_ref().unwrap();
631 assert!(servers.contains_key("user-server"));
632 assert!(servers.contains_key("agpm-server"));
633 assert_eq!(servers.len(), 2);
634 }
635
636 #[test]
637 fn test_update_mcp_servers_replaces_old_managed() {
638 let temp = tempdir().unwrap();
639 let mcp_servers_dir = temp.path().join("mcp-servers");
640 std::fs::create_dir(&mcp_servers_dir).unwrap();
641
642 let mut settings = ClaudeSettings::default();
644 let mut servers = HashMap::new();
645
646 servers.insert(
648 "user-server".to_string(),
649 McpServerConfig {
650 command: Some("user-command".to_string()),
651 args: vec![],
652 env: None,
653 r#type: None,
654 url: None,
655 headers: None,
656 agpm_metadata: None,
657 },
658 );
659
660 servers.insert(
662 "old-managed".to_string(),
663 McpServerConfig {
664 command: Some("old-command".to_string()),
665 args: vec![],
666 env: None,
667 r#type: None,
668 url: None,
669 headers: None,
670 agpm_metadata: Some(AgpmMetadata {
671 managed: true,
672 source: Some("old-source".to_string()),
673 version: Some("v0.1.0".to_string()),
674 installed_at: "2023-01-01T00:00:00Z".to_string(),
675 dependency_name: Some("old-managed".to_string()),
676 }),
677 },
678 );
679
680 settings.mcp_servers = Some(servers);
681
682 let server_config = McpServerConfig {
684 command: Some("new-managed".to_string()),
685 args: vec![],
686 env: None,
687 r#type: None,
688 url: None,
689 headers: None,
690 agpm_metadata: Some(AgpmMetadata {
691 managed: true,
692 source: Some("new-source".to_string()),
693 version: Some("v1.0.0".to_string()),
694 installed_at: "2024-01-01T00:00:00Z".to_string(),
695 dependency_name: Some("new-managed".to_string()),
696 }),
697 };
698 let config_path = mcp_servers_dir.join("new-managed.json");
699 let json = serde_json::to_string_pretty(&server_config).unwrap();
700 std::fs::write(&config_path, json).unwrap();
701
702 settings.update_mcp_servers(&mcp_servers_dir).unwrap();
704
705 let servers = settings.mcp_servers.as_ref().unwrap();
706 assert!(servers.contains_key("user-server")); assert!(servers.contains_key("new-managed")); assert!(!servers.contains_key("old-managed")); assert_eq!(servers.len(), 2);
710 }
711
712 #[test]
713 fn test_update_mcp_servers_invalid_json_file() {
714 let temp = tempdir().unwrap();
715 let mcp_servers_dir = temp.path().join("mcp-servers");
716 std::fs::create_dir(&mcp_servers_dir).unwrap();
717
718 let invalid_path = mcp_servers_dir.join("invalid.json");
720 std::fs::write(&invalid_path, "invalid json {").unwrap();
721
722 let mut settings = ClaudeSettings::default();
723 let result = settings.update_mcp_servers(&mcp_servers_dir);
724 assert!(result.is_err());
725 assert!(result.unwrap_err().to_string().contains("Failed to parse"));
726 }
727
728 #[test]
729 fn test_update_mcp_servers_ignores_non_json_files() {
730 let temp = tempdir().unwrap();
731 let mcp_servers_dir = temp.path().join("mcp-servers");
732 std::fs::create_dir(&mcp_servers_dir).unwrap();
733
734 let txt_path = mcp_servers_dir.join("readme.txt");
736 std::fs::write(&txt_path, "This is not a JSON file").unwrap();
737
738 let server_config = McpServerConfig {
740 command: Some("test".to_string()),
741 args: vec![],
742 env: None,
743 r#type: None,
744 url: None,
745 headers: None,
746 agpm_metadata: None,
747 };
748 let json_path = mcp_servers_dir.join("valid.json");
749 let json = serde_json::to_string_pretty(&server_config).unwrap();
750 std::fs::write(&json_path, json).unwrap();
751
752 let mut settings = ClaudeSettings::default();
753 settings.update_mcp_servers(&mcp_servers_dir).unwrap();
754
755 let servers = settings.mcp_servers.as_ref().unwrap();
756 assert!(servers.contains_key("valid"));
757 assert_eq!(servers.len(), 1);
758 }
759
760 #[test]
761 fn test_settings_preserves_other_fields() {
762 let temp = tempdir().unwrap();
763 let settings_path = temp.path().join("settings.local.json");
764
765 let json = r#"{
767 "permissions": {
768 "allow": ["Bash(ls)"],
769 "deny": []
770 },
771 "customField": "value",
772 "mcpServers": {
773 "test": {
774 "command": "node",
775 "args": []
776 }
777 }
778 }"#;
779 std::fs::write(&settings_path, json).unwrap();
780
781 let settings = ClaudeSettings::load_or_default(&settings_path).unwrap();
783 assert!(settings.permissions.is_some());
784 assert!(settings.mcp_servers.is_some());
785 assert!(settings.other.contains_key("customField"));
786
787 settings.save(&settings_path).unwrap();
788
789 let reloaded = ClaudeSettings::load_or_default(&settings_path).unwrap();
791 assert!(reloaded.permissions.is_some());
792 assert!(reloaded.mcp_servers.is_some());
793 assert!(reloaded.other.contains_key("customField"));
794 }
795
796 #[test]
798 fn test_mcp_config_load_save() {
799 let temp = tempdir().unwrap();
800 let config_path = temp.path().join("mcp.json");
801
802 let mut config = McpConfig::default();
803 config.mcp_servers.insert(
804 "test-server".to_string(),
805 McpServerConfig {
806 command: Some("node".to_string()),
807 args: vec!["server.js".to_string()],
808 env: Some({
809 let mut env = HashMap::new();
810 env.insert("NODE_ENV".to_string(), json!("production"));
811 env
812 }),
813 r#type: None,
814 url: None,
815 headers: None,
816 agpm_metadata: None,
817 },
818 );
819
820 config.save(&config_path).unwrap();
821
822 let loaded = McpConfig::load_or_default(&config_path).unwrap();
823 assert!(loaded.mcp_servers.contains_key("test-server"));
824 let server = &loaded.mcp_servers["test-server"];
825 assert_eq!(server.command, Some("node".to_string()));
826 assert_eq!(server.args, vec!["server.js"]);
827 assert!(server.env.is_some());
828 }
829
830 #[test]
831 fn test_mcp_config_load_nonexistent() {
832 let temp = tempdir().unwrap();
833 let config_path = temp.path().join("nonexistent.json");
834
835 let config = McpConfig::load_or_default(&config_path).unwrap();
836 assert!(config.mcp_servers.is_empty());
837 }
838
839 #[test]
840 fn test_mcp_config_load_invalid_json() {
841 let temp = tempdir().unwrap();
842 let config_path = temp.path().join("invalid.json");
843 fs::write(&config_path, "invalid json {").unwrap();
844
845 let result = McpConfig::load_or_default(&config_path);
846 assert!(result.is_err());
847 }
848
849 #[test]
850 fn test_mcp_config_save_creates_backup() {
851 let temp = tempdir().unwrap();
852 let config_path = temp.path().join("mcp.json");
853 let backup_path = temp.path().join(".claude").join("agpm").join(".mcp.json.backup");
854
855 fs::write(&config_path, r#"{"mcpServers": {"old": {"command": "old"}}}"#).unwrap();
857
858 let config = McpConfig::default();
859 config.save(&config_path).unwrap();
860
861 assert!(backup_path.exists());
863 let backup_content = fs::read_to_string(backup_path).unwrap();
864 assert!(backup_content.contains("old"));
865 }
866
867 #[test]
868 fn test_mcp_config_update_managed_servers() {
869 let mut config = McpConfig::default();
870
871 config.mcp_servers.insert(
873 "user-server".to_string(),
874 McpServerConfig {
875 command: Some("user-command".to_string()),
876 args: vec![],
877 env: None,
878 r#type: None,
879 url: None,
880 headers: None,
881 agpm_metadata: None,
882 },
883 );
884
885 config.mcp_servers.insert(
887 "old-managed".to_string(),
888 McpServerConfig {
889 command: Some("old-command".to_string()),
890 args: vec![],
891 env: None,
892 r#type: None,
893 url: None,
894 headers: None,
895 agpm_metadata: Some(AgpmMetadata {
896 managed: true,
897 source: None,
898 version: None,
899 installed_at: "old-time".to_string(),
900 dependency_name: None,
901 }),
902 },
903 );
904
905 let mut updates = HashMap::new();
907 updates.insert(
908 "new-managed".to_string(),
909 McpServerConfig {
910 command: Some("new-command".to_string()),
911 args: vec![],
912 env: None,
913 r#type: None,
914 url: None,
915 headers: None,
916 agpm_metadata: Some(AgpmMetadata {
917 managed: true,
918 source: None,
919 version: None,
920 installed_at: "new-time".to_string(),
921 dependency_name: None,
922 }),
923 },
924 );
925
926 config.update_managed_servers(updates).unwrap();
927
928 assert!(config.mcp_servers.contains_key("user-server"));
930 assert!(config.mcp_servers.contains_key("new-managed"));
931 assert!(!config.mcp_servers.contains_key("old-managed"));
932 assert_eq!(config.mcp_servers.len(), 2);
933 }
934
935 #[test]
936 fn test_mcp_config_update_managed_servers_preserves_updating_servers() {
937 let mut config = McpConfig::default();
938
939 config.mcp_servers.insert(
941 "updating-server".to_string(),
942 McpServerConfig {
943 command: Some("old-command".to_string()),
944 args: vec![],
945 env: None,
946 r#type: None,
947 url: None,
948 headers: None,
949 agpm_metadata: Some(AgpmMetadata {
950 managed: true,
951 source: None,
952 version: Some("v1.0.0".to_string()),
953 installed_at: "old-time".to_string(),
954 dependency_name: None,
955 }),
956 },
957 );
958
959 let mut updates = HashMap::new();
961 updates.insert(
962 "updating-server".to_string(),
963 McpServerConfig {
964 command: Some("new-command".to_string()),
965 args: vec![],
966 env: None,
967 r#type: None,
968 url: None,
969 headers: None,
970 agpm_metadata: Some(AgpmMetadata {
971 managed: true,
972 source: None,
973 version: Some("v2.0.0".to_string()),
974 installed_at: "new-time".to_string(),
975 dependency_name: None,
976 }),
977 },
978 );
979
980 config.update_managed_servers(updates).unwrap();
981
982 assert!(config.mcp_servers.contains_key("updating-server"));
983 let server = &config.mcp_servers["updating-server"];
984 assert_eq!(server.command, Some("new-command".to_string()));
985 assert_eq!(server.agpm_metadata.as_ref().unwrap().version, Some("v2.0.0".to_string()));
986 }
987
988 #[test]
989 fn test_mcp_config_check_conflicts() {
990 let mut config = McpConfig::default();
991
992 config.mcp_servers.insert(
994 "user-server".to_string(),
995 McpServerConfig {
996 command: Some("user-command".to_string()),
997 args: vec![],
998 env: None,
999 r#type: None,
1000 url: None,
1001 headers: None,
1002 agpm_metadata: None,
1003 },
1004 );
1005
1006 config.mcp_servers.insert(
1008 "managed-server".to_string(),
1009 McpServerConfig {
1010 command: Some("managed-command".to_string()),
1011 args: vec![],
1012 env: None,
1013 r#type: None,
1014 url: None,
1015 headers: None,
1016 agpm_metadata: Some(AgpmMetadata {
1017 managed: true,
1018 source: None,
1019 version: None,
1020 installed_at: "time".to_string(),
1021 dependency_name: None,
1022 }),
1023 },
1024 );
1025
1026 let mut new_servers = HashMap::new();
1027 new_servers.insert(
1028 "user-server".to_string(), McpServerConfig {
1030 command: Some("new-command".to_string()),
1031 args: vec![],
1032 env: None,
1033 r#type: None,
1034 url: None,
1035 headers: None,
1036 agpm_metadata: Some(AgpmMetadata {
1037 managed: true,
1038 source: None,
1039 version: None,
1040 installed_at: "time".to_string(),
1041 dependency_name: None,
1042 }),
1043 },
1044 );
1045 new_servers.insert(
1046 "managed-server".to_string(), McpServerConfig {
1048 command: Some("updated-command".to_string()),
1049 args: vec![],
1050 env: None,
1051 r#type: None,
1052 url: None,
1053 headers: None,
1054 agpm_metadata: Some(AgpmMetadata {
1055 managed: true,
1056 source: None,
1057 version: None,
1058 installed_at: "time".to_string(),
1059 dependency_name: None,
1060 }),
1061 },
1062 );
1063 new_servers.insert(
1064 "new-server".to_string(), McpServerConfig {
1066 command: Some("new-command".to_string()),
1067 args: vec![],
1068 env: None,
1069 r#type: None,
1070 url: None,
1071 headers: None,
1072 agpm_metadata: Some(AgpmMetadata {
1073 managed: true,
1074 source: None,
1075 version: None,
1076 installed_at: "time".to_string(),
1077 dependency_name: None,
1078 }),
1079 },
1080 );
1081
1082 let conflicts = config.check_conflicts(&new_servers);
1083 assert_eq!(conflicts, vec!["user-server"]);
1084 }
1085
1086 #[test]
1087 fn test_mcp_config_check_conflicts_unmanaged_metadata() {
1088 let mut config = McpConfig::default();
1089
1090 config.mcp_servers.insert(
1092 "unmanaged-server".to_string(),
1093 McpServerConfig {
1094 command: Some("user-command".to_string()),
1095 args: vec![],
1096 env: None,
1097 r#type: None,
1098 url: None,
1099 headers: None,
1100 agpm_metadata: Some(AgpmMetadata {
1101 managed: false,
1102 source: None,
1103 version: None,
1104 installed_at: "time".to_string(),
1105 dependency_name: None,
1106 }),
1107 },
1108 );
1109
1110 let mut new_servers = HashMap::new();
1111 new_servers.insert(
1112 "unmanaged-server".to_string(),
1113 McpServerConfig {
1114 command: Some("new-command".to_string()),
1115 args: vec![],
1116 env: None,
1117 r#type: None,
1118 url: None,
1119 headers: None,
1120 agpm_metadata: Some(AgpmMetadata {
1121 managed: true,
1122 source: None,
1123 version: None,
1124 installed_at: "time".to_string(),
1125 dependency_name: None,
1126 }),
1127 },
1128 );
1129
1130 let conflicts = config.check_conflicts(&new_servers);
1131 assert_eq!(conflicts, vec!["unmanaged-server"]);
1132 }
1133
1134 #[test]
1135 fn test_mcp_config_remove_all_managed() {
1136 let mut config = McpConfig::default();
1137
1138 config.mcp_servers.insert(
1140 "user-server".to_string(),
1141 McpServerConfig {
1142 command: Some("user-command".to_string()),
1143 args: vec![],
1144 env: None,
1145 r#type: None,
1146 url: None,
1147 headers: None,
1148 agpm_metadata: None,
1149 },
1150 );
1151
1152 config.mcp_servers.insert(
1153 "managed-server".to_string(),
1154 McpServerConfig {
1155 command: Some("managed-command".to_string()),
1156 args: vec![],
1157 env: None,
1158 r#type: None,
1159 url: None,
1160 headers: None,
1161 agpm_metadata: Some(AgpmMetadata {
1162 managed: true,
1163 source: None,
1164 version: None,
1165 installed_at: "time".to_string(),
1166 dependency_name: None,
1167 }),
1168 },
1169 );
1170
1171 config.mcp_servers.insert(
1172 "unmanaged-with-metadata".to_string(),
1173 McpServerConfig {
1174 command: Some("unmanaged-command".to_string()),
1175 args: vec![],
1176 env: None,
1177 r#type: None,
1178 url: None,
1179 headers: None,
1180 agpm_metadata: Some(AgpmMetadata {
1181 managed: false,
1182 source: None,
1183 version: None,
1184 installed_at: "time".to_string(),
1185 dependency_name: None,
1186 }),
1187 },
1188 );
1189
1190 config.remove_all_managed();
1191
1192 assert!(config.mcp_servers.contains_key("user-server"));
1193 assert!(config.mcp_servers.contains_key("unmanaged-with-metadata"));
1194 assert!(!config.mcp_servers.contains_key("managed-server"));
1195 assert_eq!(config.mcp_servers.len(), 2);
1196 }
1197
1198 #[test]
1199 fn test_mcp_config_get_managed_servers() {
1200 let mut config = McpConfig::default();
1201
1202 config.mcp_servers.insert(
1204 "user-server".to_string(),
1205 McpServerConfig {
1206 command: Some("user-command".to_string()),
1207 args: vec![],
1208 env: None,
1209 r#type: None,
1210 url: None,
1211 headers: None,
1212 agpm_metadata: None,
1213 },
1214 );
1215
1216 config.mcp_servers.insert(
1217 "managed-server1".to_string(),
1218 McpServerConfig {
1219 command: Some("managed-command1".to_string()),
1220 args: vec![],
1221 env: None,
1222 r#type: None,
1223 url: None,
1224 headers: None,
1225 agpm_metadata: Some(AgpmMetadata {
1226 managed: true,
1227 source: None,
1228 version: None,
1229 installed_at: "time".to_string(),
1230 dependency_name: None,
1231 }),
1232 },
1233 );
1234
1235 config.mcp_servers.insert(
1236 "managed-server2".to_string(),
1237 McpServerConfig {
1238 command: Some("managed-command2".to_string()),
1239 args: vec![],
1240 env: None,
1241 r#type: None,
1242 url: None,
1243 headers: None,
1244 agpm_metadata: Some(AgpmMetadata {
1245 managed: true,
1246 source: Some("source".to_string()),
1247 version: Some("v1.0.0".to_string()),
1248 installed_at: "time".to_string(),
1249 dependency_name: Some("dep".to_string()),
1250 }),
1251 },
1252 );
1253
1254 let managed = config.get_managed_servers();
1255 assert_eq!(managed.len(), 2);
1256 assert!(managed.contains_key("managed-server1"));
1257 assert!(managed.contains_key("managed-server2"));
1258 assert!(!managed.contains_key("user-server"));
1259 }
1260
1261 #[test]
1267 fn test_claude_settings_serialization() {
1268 let mut settings = ClaudeSettings {
1270 permissions: Some(json!({"allow": ["test"], "deny": []})),
1271 ..Default::default()
1272 };
1273
1274 let mut servers = HashMap::new();
1275 servers.insert(
1276 "test".to_string(),
1277 McpServerConfig {
1278 command: Some("test-cmd".to_string()),
1279 args: vec!["arg1".to_string()],
1280 env: Some({
1281 let mut env = HashMap::new();
1282 env.insert("VAR".to_string(), json!("value"));
1283 env
1284 }),
1285 r#type: None,
1286 url: None,
1287 headers: None,
1288 agpm_metadata: Some(AgpmMetadata {
1289 managed: true,
1290 source: Some("test-source".to_string()),
1291 version: Some("v1.0.0".to_string()),
1292 installed_at: "2024-01-01T00:00:00Z".to_string(),
1293 dependency_name: Some("test".to_string()),
1294 }),
1295 },
1296 );
1297 settings.mcp_servers = Some(servers);
1298
1299 settings.other.insert("custom".to_string(), json!("value"));
1300
1301 let json = serde_json::to_string(&settings).unwrap();
1303 let deserialized: ClaudeSettings = serde_json::from_str(&json).unwrap();
1304
1305 assert_eq!(deserialized.permissions, settings.permissions);
1306 assert_eq!(deserialized.mcp_servers.as_ref().unwrap().len(), 1);
1307 assert_eq!(deserialized.other.get("custom"), settings.other.get("custom"));
1308 }
1309
1310 #[test]
1311 fn test_mcp_config_serialization() {
1312 let mut config = McpConfig::default();
1313
1314 config.mcp_servers.insert(
1315 "test".to_string(),
1316 McpServerConfig {
1317 command: Some("test-cmd".to_string()),
1318 args: vec!["arg1".to_string(), "arg2".to_string()],
1319 env: Some({
1320 let mut env = HashMap::new();
1321 env.insert("TEST_VAR".to_string(), json!("test_value"));
1322 env
1323 }),
1324 r#type: None,
1325 url: None,
1326 headers: None,
1327 agpm_metadata: Some(AgpmMetadata {
1328 managed: true,
1329 source: Some("github.com/test/repo".to_string()),
1330 version: Some("v2.0.0".to_string()),
1331 installed_at: "2024-01-01T12:00:00Z".to_string(),
1332 dependency_name: Some("test-dep".to_string()),
1333 }),
1334 },
1335 );
1336
1337 let json = serde_json::to_string_pretty(&config).unwrap();
1339 let deserialized: McpConfig = serde_json::from_str(&json).unwrap();
1340
1341 assert_eq!(deserialized.mcp_servers.len(), 1);
1342 let server = &deserialized.mcp_servers["test"];
1343 assert_eq!(server.command, Some("test-cmd".to_string()));
1344 assert_eq!(server.args.len(), 2);
1345 assert!(server.env.is_some());
1346 assert!(server.agpm_metadata.is_some());
1347
1348 let metadata = server.agpm_metadata.as_ref().unwrap();
1349 assert!(metadata.managed);
1350 assert_eq!(metadata.source, Some("github.com/test/repo".to_string()));
1351 assert_eq!(metadata.version, Some("v2.0.0".to_string()));
1352 }
1353
1354 #[test]
1355 fn test_mcp_server_config_minimal_serialization() {
1356 let config = McpServerConfig {
1357 command: Some("minimal".to_string()),
1358 args: vec![],
1359 env: None,
1360 r#type: None,
1361 url: None,
1362 headers: None,
1363 agpm_metadata: None,
1364 };
1365
1366 let json = serde_json::to_string(&config).unwrap();
1367 let deserialized: McpServerConfig = serde_json::from_str(&json).unwrap();
1368
1369 assert_eq!(deserialized.command, Some("minimal".to_string()));
1370 assert!(deserialized.args.is_empty());
1371 assert!(deserialized.env.is_none());
1372 assert!(deserialized.agpm_metadata.is_none());
1373
1374 assert!(!json.contains(r#""args":[]"#));
1376 }
1377
1378 #[test]
1379 fn test_agpm_metadata_serialization() {
1380 let metadata = AgpmMetadata {
1381 managed: true,
1382 source: Some("test-source".to_string()),
1383 version: None,
1384 installed_at: "2024-01-01T00:00:00Z".to_string(),
1385 dependency_name: Some("test-dep".to_string()),
1386 };
1387
1388 let json = serde_json::to_string(&metadata).unwrap();
1389 let deserialized: AgpmMetadata = serde_json::from_str(&json).unwrap();
1390
1391 assert!(deserialized.managed);
1392 assert_eq!(deserialized.source, Some("test-source".to_string()));
1393 assert_eq!(deserialized.version, None);
1394 assert_eq!(deserialized.installed_at, "2024-01-01T00:00:00Z");
1395 assert_eq!(deserialized.dependency_name, Some("test-dep".to_string()));
1396
1397 assert!(!json.contains(r#""version""#));
1399 }
1400
1401 #[test]
1402 fn test_clean_mcp_servers() {
1403 let temp = tempfile::TempDir::new().unwrap();
1404 let project_root = temp.path();
1405 let claude_dir = project_root.join(".claude");
1406 let agpm_dir = claude_dir.join("agpm");
1407 let mcp_servers_dir = agpm_dir.join("mcp-servers");
1408 let settings_path = claude_dir.join("settings.local.json");
1409 let mcp_config_path = project_root.join(".mcp.json");
1410
1411 std::fs::create_dir_all(&mcp_servers_dir).unwrap();
1413
1414 let server1_path = mcp_servers_dir.join("server1.json");
1416 let server2_path = mcp_servers_dir.join("server2.json");
1417 let server_config = McpServerConfig {
1418 command: Some("test".to_string()),
1419 args: vec![],
1420 env: None,
1421 r#type: None,
1422 url: None,
1423 headers: None,
1424 agpm_metadata: Some(AgpmMetadata {
1425 managed: true,
1426 source: Some("test-source".to_string()),
1427 version: Some("v1.0.0".to_string()),
1428 installed_at: "2024-01-01T00:00:00Z".to_string(),
1429 dependency_name: Some("test-server".to_string()),
1430 }),
1431 };
1432 crate::utils::write_json_file(&server1_path, &server_config, true).unwrap();
1433 crate::utils::write_json_file(&server2_path, &server_config, true).unwrap();
1434
1435 let mut settings = ClaudeSettings::default();
1437 let mut servers = HashMap::new();
1438
1439 servers.insert(
1441 "agpm-server".to_string(),
1442 McpServerConfig {
1443 command: Some("agpm-cmd".to_string()),
1444 args: vec![],
1445 env: None,
1446 r#type: None,
1447 url: None,
1448 headers: None,
1449 agpm_metadata: Some(AgpmMetadata {
1450 managed: true,
1451 source: Some("test".to_string()),
1452 version: Some("v1.0.0".to_string()),
1453 installed_at: "2024-01-01T00:00:00Z".to_string(),
1454 dependency_name: None,
1455 }),
1456 },
1457 );
1458
1459 servers.insert(
1461 "user-server".to_string(),
1462 McpServerConfig {
1463 command: Some("user-cmd".to_string()),
1464 args: vec![],
1465 env: None,
1466 r#type: None,
1467 url: None,
1468 headers: None,
1469 agpm_metadata: None,
1470 },
1471 );
1472
1473 settings.mcp_servers = Some(servers);
1474 settings.save(&settings_path).unwrap();
1475
1476 let mut mcp_config = McpConfig::default();
1478 mcp_config.mcp_servers.insert(
1479 "agpm-server".to_string(),
1480 McpServerConfig {
1481 command: Some("agpm-cmd".to_string()),
1482 args: vec![],
1483 env: None,
1484 r#type: None,
1485 url: None,
1486 headers: None,
1487 agpm_metadata: Some(AgpmMetadata {
1488 managed: true,
1489 source: Some("test".to_string()),
1490 version: Some("v1.0.0".to_string()),
1491 installed_at: "2024-01-01T00:00:00Z".to_string(),
1492 dependency_name: None,
1493 }),
1494 },
1495 );
1496 mcp_config.mcp_servers.insert(
1497 "user-server".to_string(),
1498 McpServerConfig {
1499 command: Some("user-cmd".to_string()),
1500 args: vec![],
1501 env: None,
1502 r#type: None,
1503 url: None,
1504 headers: None,
1505 agpm_metadata: None,
1506 },
1507 );
1508 mcp_config.save(&mcp_config_path).unwrap();
1509
1510 clean_mcp_servers(project_root).unwrap();
1512
1513 assert!(!server1_path.exists());
1515 assert!(!server2_path.exists());
1516
1517 let updated_mcp_config = McpConfig::load_or_default(&mcp_config_path).unwrap();
1519 assert_eq!(updated_mcp_config.mcp_servers.len(), 1);
1520 assert!(updated_mcp_config.mcp_servers.contains_key("user-server"));
1521 assert!(!updated_mcp_config.mcp_servers.contains_key("agpm-server"));
1522 }
1523
1524 #[test]
1525 fn test_clean_mcp_servers_no_servers() {
1526 let temp = tempfile::TempDir::new().unwrap();
1527 let project_root = temp.path();
1528
1529 let result = clean_mcp_servers(project_root);
1531 assert!(result.is_ok());
1532 }
1533
1534 #[test]
1535 fn test_list_mcp_servers() {
1536 let temp = tempfile::TempDir::new().unwrap();
1537 let project_root = temp.path();
1538 let claude_dir = project_root.join(".claude");
1539 let settings_path = claude_dir.join("settings.local.json");
1540
1541 std::fs::create_dir_all(&claude_dir).unwrap();
1542
1543 let mut settings = ClaudeSettings::default();
1545 let mut servers = HashMap::new();
1546
1547 servers.insert(
1548 "managed-server".to_string(),
1549 McpServerConfig {
1550 command: Some("managed".to_string()),
1551 args: vec![],
1552 env: None,
1553 r#type: None,
1554 url: None,
1555 headers: None,
1556 agpm_metadata: Some(AgpmMetadata {
1557 managed: true,
1558 source: Some("test".to_string()),
1559 version: Some("v2.0.0".to_string()),
1560 installed_at: "2024-01-01T00:00:00Z".to_string(),
1561 dependency_name: None,
1562 }),
1563 },
1564 );
1565
1566 servers.insert(
1567 "user-server".to_string(),
1568 McpServerConfig {
1569 command: Some("user".to_string()),
1570 args: vec![],
1571 env: None,
1572 r#type: None,
1573 url: None,
1574 headers: None,
1575 agpm_metadata: None,
1576 },
1577 );
1578
1579 settings.mcp_servers = Some(servers);
1580 settings.save(&settings_path).unwrap();
1581
1582 let result = list_mcp_servers(project_root);
1584 assert!(result.is_ok());
1585 }
1586
1587 #[test]
1588 fn test_list_mcp_servers_no_file() {
1589 let temp = tempfile::TempDir::new().unwrap();
1590 let project_root = temp.path();
1591
1592 let result = list_mcp_servers(project_root);
1594 assert!(result.is_ok());
1595 }
1596
1597 #[test]
1598 fn test_list_mcp_servers_empty() {
1599 let temp = tempfile::TempDir::new().unwrap();
1600 let project_root = temp.path();
1601 let claude_dir = project_root.join(".claude");
1602 let settings_path = claude_dir.join("settings.local.json");
1603
1604 std::fs::create_dir_all(&claude_dir).unwrap();
1605
1606 let settings = ClaudeSettings::default();
1608 settings.save(&settings_path).unwrap();
1609
1610 let result = list_mcp_servers(project_root);
1612 assert!(result.is_ok());
1613 }
1614
1615 #[test]
1616 fn test_claude_settings_save_backup() {
1617 let temp = tempfile::TempDir::new().unwrap();
1618 let settings_path = temp.path().join("settings.local.json");
1619 let backup_path = temp.path().join("agpm").join("settings.local.json.backup");
1620
1621 let settings1 = ClaudeSettings::default();
1623 settings1.save(&settings_path).unwrap();
1624 assert!(settings_path.exists());
1625 assert!(!backup_path.exists());
1626
1627 let settings2 = ClaudeSettings {
1629 hooks: Some(serde_json::json!({"test": "hook"})),
1630 ..Default::default()
1631 };
1632 settings2.save(&settings_path).unwrap();
1633
1634 assert!(backup_path.exists());
1636
1637 let backup_content: ClaudeSettings = crate::utils::read_json_file(&backup_path).unwrap();
1639 assert!(backup_content.hooks.is_none());
1640
1641 let main_content: ClaudeSettings = crate::utils::read_json_file(&settings_path).unwrap();
1643 assert!(main_content.hooks.is_some());
1644 }
1645
1646 #[test]
1647 fn test_mcp_config_save_backup() {
1648 let temp = tempfile::TempDir::new().unwrap();
1649 let config_path = temp.path().join(".mcp.json");
1650 let backup_path = temp.path().join(".claude").join("agpm").join(".mcp.json.backup");
1651
1652 let config1 = McpConfig::default();
1654 config1.save(&config_path).unwrap();
1655 assert!(config_path.exists());
1656 assert!(!backup_path.exists());
1657
1658 let mut config2 = McpConfig::default();
1660 config2.mcp_servers.insert(
1661 "test".to_string(),
1662 McpServerConfig {
1663 command: Some("test-cmd".to_string()),
1664 args: vec![],
1665 env: None,
1666 r#type: None,
1667 url: None,
1668 headers: None,
1669 agpm_metadata: None,
1670 },
1671 );
1672 config2.save(&config_path).unwrap();
1673
1674 assert!(backup_path.exists());
1676
1677 let backup_content: McpConfig = crate::utils::read_json_file(&backup_path).unwrap();
1679 assert!(backup_content.mcp_servers.is_empty());
1680
1681 let main_content: McpConfig = crate::utils::read_json_file(&config_path).unwrap();
1683 assert_eq!(main_content.mcp_servers.len(), 1);
1684 }
1685
1686 #[test]
1687 fn test_update_mcp_servers_preserves_user_servers() {
1688 let temp = tempfile::TempDir::new().unwrap();
1689 let agpm_dir = temp.path().join(".claude").join("agpm");
1690 let mcp_servers_dir = agpm_dir.join("mcp-servers");
1691 std::fs::create_dir_all(&mcp_servers_dir).unwrap();
1692
1693 let server1 = McpServerConfig {
1695 command: Some("server1".to_string()),
1696 args: vec!["arg1".to_string()],
1697 env: None,
1698 r#type: None,
1699 url: None,
1700 headers: None,
1701 agpm_metadata: Some(AgpmMetadata {
1702 managed: true,
1703 source: Some("source1".to_string()),
1704 version: Some("v1.0.0".to_string()),
1705 installed_at: "2024-01-01T00:00:00Z".to_string(),
1706 dependency_name: None,
1707 }),
1708 };
1709 crate::utils::write_json_file(&mcp_servers_dir.join("server1.json"), &server1, true)
1710 .unwrap();
1711
1712 let mut settings = ClaudeSettings::default();
1714 let mut servers = HashMap::new();
1715 servers.insert(
1716 "user-server".to_string(),
1717 McpServerConfig {
1718 command: Some("user".to_string()),
1719 args: vec![],
1720 env: None,
1721 r#type: None,
1722 url: None,
1723 headers: None,
1724 agpm_metadata: None,
1725 },
1726 );
1727 settings.mcp_servers = Some(servers);
1728
1729 settings.update_mcp_servers(&mcp_servers_dir).unwrap();
1731
1732 let servers = settings.mcp_servers.as_ref().unwrap();
1734 assert_eq!(servers.len(), 2);
1735 assert!(servers.contains_key("user-server"));
1736 assert!(servers.contains_key("server1"));
1737
1738 let server1_config = servers.get("server1").unwrap();
1740 assert_eq!(server1_config.command, Some("server1".to_string()));
1741 assert_eq!(server1_config.args, vec!["arg1"]);
1742 }
1743
1744 #[test]
1745 fn test_update_mcp_servers_nonexistent_dir() {
1746 let temp = tempfile::TempDir::new().unwrap();
1747 let nonexistent_dir = temp.path().join("nonexistent");
1748
1749 let mut settings = ClaudeSettings::default();
1750 let result = settings.update_mcp_servers(&nonexistent_dir);
1751 assert!(result.is_ok());
1752 }
1753
1754 #[test]
1755 fn test_mcp_config_handles_extra_fields() {
1756 let json_str = r#"{
1758 "mcpServers": {
1759 "test": {
1760 "command": "test",
1761 "args": []
1762 }
1763 },
1764 "customField": "value",
1765 "anotherField": {
1766 "nested": true
1767 }
1768 }"#;
1769
1770 let temp = tempdir().unwrap();
1771 let config_path = temp.path().join(".mcp.json");
1772 std::fs::write(&config_path, json_str).unwrap();
1773
1774 let config = McpConfig::load_or_default(&config_path).unwrap();
1776 assert!(config.mcp_servers.contains_key("test"));
1777 assert_eq!(config.mcp_servers.len(), 1);
1778 }
1779}