agpm_cli/mcp/
config.rs

1use crate::mcp::models::{McpConfig, McpServerConfig};
2use anyhow::{Context, Result};
3use std::collections::HashMap;
4use std::path::Path;
5
6impl McpConfig {
7    /// Load an existing `.mcp.json` file or create a new empty configuration.
8    ///
9    /// This method preserves all existing server configurations, including
10    /// user-managed ones.
11    pub fn load_or_default(path: &Path) -> Result<Self> {
12        if path.exists() {
13            // Parse with lenient error handling to preserve user configurations
14            crate::utils::read_json_file(path).with_context(|| {
15                format!(
16                    "Failed to parse MCP configuration file: {}\n\
17                     The file may be malformed or contain invalid JSON.",
18                    path.display()
19                )
20            })
21        } else {
22            Ok(Self::default())
23        }
24    }
25
26    /// Save the configuration to a `.mcp.json` file.
27    ///
28    /// The file is written atomically to prevent corruption.
29    pub fn save(&self, path: &Path) -> Result<()> {
30        // Create a backup if the file exists
31        if path.exists() {
32            // Generate backup path at project root: .agpm/backups/claude-code/.mcp.json
33            let backup_path = crate::utils::generate_backup_path(path, "claude-code")?;
34
35            // Ensure backup directory exists
36            if let Some(backup_dir) = backup_path.parent() {
37                if !backup_dir.exists() {
38                    std::fs::create_dir_all(backup_dir).with_context(|| {
39                        format!("Failed to create directory: {}", backup_dir.display())
40                    })?;
41                }
42            }
43
44            std::fs::copy(path, &backup_path).with_context(|| {
45                format!(
46                    "Failed to create backup of MCP configuration at: {}",
47                    backup_path.display()
48                )
49            })?;
50        }
51
52        // Write with pretty formatting for readability
53        crate::utils::write_json_file(path, self, true)
54            .with_context(|| format!("Failed to write MCP configuration to: {}", path.display()))?;
55
56        Ok(())
57    }
58
59    /// Update only AGPM-managed servers, preserving user configurations.
60    ///
61    /// This method:
62    /// 1. Removes old AGPM-managed servers not in the update set
63    /// 2. Adds or updates AGPM-managed servers from the update set
64    /// 3. Preserves all user-managed servers (those without AGPM metadata)
65    pub fn update_managed_servers(
66        &mut self,
67        updates: HashMap<String, McpServerConfig>,
68    ) -> Result<()> {
69        // Build set of server names being updated
70        let updating_names: std::collections::HashSet<_> = updates.keys().cloned().collect();
71
72        // Remove old AGPM-managed servers not being updated
73        self.mcp_servers.retain(|name, config| {
74            // Keep if:
75            // 1. It's not managed by AGPM (user server), OR
76            // 2. It's being updated in this operation
77            config
78                .agpm_metadata
79                .as_ref()
80                .is_none_or(|meta| !meta.managed || updating_names.contains(name))
81        });
82
83        // Add/update AGPM-managed servers
84        for (name, config) in updates {
85            self.mcp_servers.insert(name, config);
86        }
87
88        Ok(())
89    }
90
91    /// Check for conflicts with user-managed servers.
92    ///
93    /// Returns a list of server names that would conflict with existing
94    /// user-managed servers.
95    #[must_use]
96    pub fn check_conflicts(&self, new_servers: &HashMap<String, McpServerConfig>) -> Vec<String> {
97        let mut conflicts = Vec::new();
98
99        for name in new_servers.keys() {
100            if let Some(existing) = self.mcp_servers.get(name) {
101                // Conflict if the existing server is not managed by AGPM
102                if existing.agpm_metadata.is_none()
103                    || !existing.agpm_metadata.as_ref().unwrap().managed
104                {
105                    conflicts.push(name.clone());
106                }
107            }
108        }
109
110        conflicts
111    }
112
113    /// Remove all AGPM-managed servers.
114    ///
115    /// This is useful for cleanup operations.
116    pub fn remove_all_managed(&mut self) {
117        self.mcp_servers
118            .retain(|_, config| config.agpm_metadata.as_ref().is_none_or(|meta| !meta.managed));
119    }
120
121    /// Get all AGPM-managed servers.
122    #[must_use]
123    pub fn get_managed_servers(&self) -> HashMap<String, &McpServerConfig> {
124        self.mcp_servers
125            .iter()
126            .filter(|(_, config)| config.agpm_metadata.as_ref().is_some_and(|meta| meta.managed))
127            .map(|(name, config)| (name.clone(), config))
128            .collect()
129    }
130}