cc_switch_lib/cli/commands/
mcp.rs1use 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,
13 Add,
15 Edit {
17 id: String,
19 },
20 Delete {
22 id: String,
24 },
25 Enable {
27 id: String,
29 },
30 Disable {
32 id: String,
34 },
35 Validate {
37 command: String,
39 },
40 Sync,
42 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 let mut table = create_table();
78 table.set_header(vec!["ID", "Name", "Claude", "Codex", "Gemini", "Tags"]);
79
80 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 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 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 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 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 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 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 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 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 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}