Skip to main content

cc_switch_lib/cli/commands/
mcp.rs

1use clap::Subcommand;
2
3use crate::app_config::{AppType, McpApps, McpServer};
4use crate::cli::ui::{create_table, error, highlight, info, success};
5use crate::error::AppError;
6use crate::services::McpService;
7use crate::store::AppState;
8
9#[derive(Subcommand)]
10pub enum McpCommand {
11    /// List all MCP servers
12    List,
13    /// Add a new MCP server (interactive)
14    Add,
15    /// Edit an MCP server
16    Edit {
17        /// Server ID to edit
18        id: String,
19    },
20    /// Delete an MCP server
21    Delete {
22        /// Server ID to delete
23        id: String,
24    },
25    /// Enable an MCP server for specific app(s)
26    Enable {
27        /// Server ID to enable
28        id: String,
29    },
30    /// Disable an MCP server for specific app(s)
31    Disable {
32        /// Server ID to disable
33        id: String,
34    },
35    /// Validate a command is in PATH
36    Validate {
37        /// Command to validate
38        command: String,
39    },
40    /// Sync MCP configuration to live files
41    Sync,
42    /// Import MCP servers from live configuration
43    Import,
44}
45
46pub fn execute(cmd: McpCommand, app: Option<AppType>) -> Result<(), AppError> {
47    let app_type = app.unwrap_or(AppType::Claude);
48
49    match cmd {
50        McpCommand::List => list_servers(app_type),
51        McpCommand::Add => add_server(app_type),
52        McpCommand::Edit { id } => edit_server(app_type, &id),
53        McpCommand::Delete { id } => delete_server(&id),
54        McpCommand::Enable { id } => enable_server(app_type, &id),
55        McpCommand::Disable { id } => disable_server(app_type, &id),
56        McpCommand::Validate { command } => validate_command(&command),
57        McpCommand::Sync => sync_servers(),
58        McpCommand::Import => import_servers(app_type),
59    }
60}
61
62fn get_state() -> Result<AppState, AppError> {
63    AppState::try_new()
64}
65
66fn list_servers(app_type: AppType) -> Result<(), AppError> {
67    let state = get_state()?;
68    let servers = McpService::get_all_servers(&state)?;
69
70    if servers.is_empty() {
71        println!("{}", info("No MCP servers found."));
72        println!("Use 'cc-switch mcp add' or 'cc-switch mcp import' to add servers.");
73        return Ok(());
74    }
75
76    // 创建表格
77    let mut table = create_table();
78    table.set_header(vec!["ID", "Name", "Claude", "Codex", "Gemini", "Tags"]);
79
80    // 按 ID 排序
81    let mut server_list: Vec<_> = servers.into_iter().collect();
82    server_list.sort_by(|(a, _), (b, _)| a.cmp(b));
83
84    for (id, server) in server_list {
85        let claude_marker = if server.apps.claude { "✓" } else { " " };
86        let codex_marker = if server.apps.codex { "✓" } else { " " };
87        let gemini_marker = if server.apps.gemini { "✓" } else { " " };
88        let tags = server.tags.join(", ");
89
90        let row = vec![
91            id.clone(),
92            server.name.clone(),
93            claude_marker.to_string(),
94            codex_marker.to_string(),
95            gemini_marker.to_string(),
96            tags,
97        ];
98
99        table.add_row(row);
100    }
101
102    println!("{}", table);
103    println!(
104        "\n{} Viewing from: {} perspective",
105        info("ℹ"),
106        app_type.as_str()
107    );
108    println!("{} ✓ = Enabled for this app", info("→"));
109
110    Ok(())
111}
112
113fn delete_server(id: &str) -> Result<(), AppError> {
114    let state = get_state()?;
115
116    // 检查服务器是否存在
117    let servers = McpService::get_all_servers(&state)?;
118    let server = servers
119        .get(id)
120        .ok_or_else(|| AppError::Message(format!("MCP server '{}' not found", id)))?;
121
122    // 显示将要删除的服务器信息
123    println!("{}", highlight("Server to be deleted:"));
124    println!("ID:   {}", id);
125    println!("Name: {}", server.name);
126
127    let enabled_apps: Vec<&str> = vec![
128        if server.apps.claude {
129            Some("Claude")
130        } else {
131            None
132        },
133        if server.apps.codex {
134            Some("Codex")
135        } else {
136            None
137        },
138        if server.apps.gemini {
139            Some("Gemini")
140        } else {
141            None
142        },
143    ]
144    .into_iter()
145    .flatten()
146    .collect();
147
148    if !enabled_apps.is_empty() {
149        println!("Enabled for: {}", enabled_apps.join(", "));
150    }
151    println!();
152
153    // 确认删除
154    let confirm = inquire::Confirm::new(&format!(
155        "Are you sure you want to delete MCP server '{}'?",
156        id
157    ))
158    .with_default(false)
159    .prompt()
160    .map_err(|e| AppError::Message(format!("Prompt failed: {}", e)))?;
161
162    if !confirm {
163        println!("{}", info("Cancelled."));
164        return Ok(());
165    }
166
167    // 执行删除
168    let deleted = McpService::delete_server(&state, id)?;
169
170    if deleted {
171        println!("{}", success(&format!("✓ Deleted MCP server '{}'", id)));
172        if !enabled_apps.is_empty() {
173            println!(
174                "{}",
175                info(&format!("  Removed from: {}", enabled_apps.join(", ")))
176            );
177        }
178    } else {
179        println!("{}", error(&format!("Failed to delete server '{}'", id)));
180    }
181
182    Ok(())
183}
184
185fn enable_server(app_type: AppType, id: &str) -> Result<(), AppError> {
186    let state = get_state()?;
187    let app_str = app_type.as_str().to_string();
188
189    // 检查服务器是否存在
190    let servers = McpService::get_all_servers(&state)?;
191    if !servers.contains_key(id) {
192        return Err(AppError::Message(format!("MCP server '{}' not found", id)));
193    }
194
195    // 执行启用
196    McpService::toggle_app(&state, id, app_type, true)?;
197
198    println!(
199        "{}",
200        success(&format!("✓ Enabled MCP server '{}' for {}", id, app_str))
201    );
202    println!(
203        "{}",
204        info("Note: Configuration has been synced to live file.")
205    );
206
207    Ok(())
208}
209
210fn disable_server(app_type: AppType, id: &str) -> Result<(), AppError> {
211    let state = get_state()?;
212    let app_str = app_type.as_str().to_string();
213
214    // 检查服务器是否存在
215    let servers = McpService::get_all_servers(&state)?;
216    if !servers.contains_key(id) {
217        return Err(AppError::Message(format!("MCP server '{}' not found", id)));
218    }
219
220    // 执行禁用
221    McpService::toggle_app(&state, id, app_type, false)?;
222
223    println!(
224        "{}",
225        success(&format!("✓ Disabled MCP server '{}' for {}", id, app_str))
226    );
227    println!(
228        "{}",
229        info("Note: Configuration has been removed from live file.")
230    );
231
232    Ok(())
233}
234
235fn sync_servers() -> Result<(), AppError> {
236    let state = get_state()?;
237
238    println!("{}", info("Syncing all enabled MCP servers..."));
239
240    McpService::sync_all_enabled(&state)?;
241
242    println!("{}", success("✓ All MCP servers synced successfully"));
243    println!(
244        "{}",
245        info("Note: Live configuration files have been updated.")
246    );
247
248    Ok(())
249}
250
251fn import_servers(app_type: AppType) -> Result<(), AppError> {
252    let state = get_state()?;
253    let app_str = app_type.as_str().to_string();
254
255    println!(
256        "{}",
257        info(&format!(
258            "Importing MCP servers from {} live config...",
259            app_str
260        ))
261    );
262
263    let count = match app_type {
264        AppType::Claude => McpService::import_from_claude(&state)?,
265        AppType::Codex => McpService::import_from_codex(&state)?,
266        AppType::Gemini => McpService::import_from_gemini(&state)?,
267        AppType::OpenCode => 0,
268        AppType::OpenClaw => McpService::import_from_openclaw(&state)?,
269        AppType::Hermes => McpService::import_from_hermes(&state)?,
270    };
271
272    if count > 0 {
273        println!(
274            "{}",
275            success(&format!(
276                "✓ Imported {} MCP server(s) from {}",
277                count, app_str
278            ))
279        );
280        println!(
281            "{}",
282            info("Note: Servers have been added to unified configuration.")
283        );
284    } else {
285        println!(
286            "{}",
287            info(&format!("No new MCP servers found in {} config.", app_str))
288        );
289    }
290
291    Ok(())
292}
293
294fn add_server(_app_type: AppType) -> Result<(), AppError> {
295    let state = get_state()?;
296
297    let mut apps = McpApps::default();
298    apps.set_enabled_for(&_app_type, true);
299
300    let template = McpServer {
301        id: String::new(),
302        name: String::new(),
303        server: serde_json::json!({
304            "command": "",
305            "args": [],
306        }),
307        apps,
308        description: None,
309        homepage: None,
310        docs: None,
311        tags: vec![],
312    };
313    let initial = serde_json::to_string_pretty(&template)
314        .map_err(|e| AppError::Message(format!("failed to serialize template: {e}")))?;
315
316    println!("{}", highlight("Add New MCP Server"));
317    println!("{}", info("Opening external editor..."));
318    let edited = crate::cli::editor::open_external_editor(&initial)?;
319
320    let server: McpServer = serde_json::from_str(&edited)
321        .map_err(|e| AppError::Message(format!("invalid JSON: {e}")))?;
322    if server.id.trim().is_empty() || server.name.trim().is_empty() {
323        return Err(AppError::InvalidInput(
324            "missing required fields: id, name".to_string(),
325        ));
326    }
327
328    McpService::upsert_server(&state, server)?;
329
330    println!("{}", success("✓ MCP server saved"));
331    println!(
332        "{}",
333        info("Tip: Use 'cc-switch mcp list' to view all servers.")
334    );
335    Ok(())
336}
337
338fn edit_server(_app_type: AppType, id: &str) -> Result<(), AppError> {
339    let state = get_state()?;
340    let servers = McpService::get_all_servers(&state)?;
341    let Some(existing) = servers.get(id).cloned() else {
342        return Err(AppError::InvalidInput(format!(
343            "MCP server '{id}' not found"
344        )));
345    };
346
347    let initial = serde_json::to_string_pretty(&existing)
348        .map_err(|e| AppError::Message(format!("failed to serialize server: {e}")))?;
349
350    println!("{}", info(&format!("Editing MCP server '{}'...", id)));
351    println!("{}", info("Opening external editor..."));
352    let edited = crate::cli::editor::open_external_editor(&initial)?;
353
354    if edited.trim_end() == initial.trim_end() {
355        println!("{}", info("No changes detected."));
356        return Ok(());
357    }
358
359    let mut server: McpServer = serde_json::from_str(&edited)
360        .map_err(|e| AppError::Message(format!("invalid JSON: {e}")))?;
361    server.id = id.to_string();
362    if server.name.trim().is_empty() {
363        return Err(AppError::InvalidInput(
364            "missing required field: name".to_string(),
365        ));
366    }
367
368    McpService::upsert_server(&state, server)?;
369
370    println!("{}", success("✓ MCP server updated"));
371    Ok(())
372}
373
374fn validate_command(command: &str) -> Result<(), AppError> {
375    println!("{}", info(&format!("Validating command '{}'...", command)));
376
377    // 检查命令是否在 PATH 中
378    if which::which(command).is_ok() {
379        println!(
380            "{}",
381            success(&format!("✓ Command '{}' is available in PATH", command))
382        );
383    } else {
384        println!(
385            "{}",
386            error(&format!("✗ Command '{}' not found in PATH", command))
387        );
388        println!(
389            "{}",
390            info("Make sure the command is installed and accessible.")
391        );
392    }
393
394    Ok(())
395}