1use crate::actions::ServerAction;
6use anyhow::{Context, Result};
7use mcp_execution_core::cli::{ExitCode, OutputFormat};
8use mcp_execution_core::{ServerConfig as CoreServerConfig, ServerId};
9use mcp_execution_introspector::Introspector;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::PathBuf;
13use tracing::{debug, info, warn};
14
15#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
19struct ClaudeDesktopConfig {
20 #[serde(rename = "mcpServers")]
21 mcp_execution_servers: HashMap<String, ServerConfig>,
22}
23
24#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
28struct ServerConfig {
29 command: String,
31 #[serde(default)]
33 args: Vec<String>,
34 #[serde(default)]
36 env: HashMap<String, String>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41enum ServerStatus {
42 Available,
44 Unavailable,
46}
47
48impl ServerStatus {
49 const fn as_str(self) -> &'static str {
50 match self {
51 Self::Available => "available",
52 Self::Unavailable => "unavailable",
53 }
54 }
55}
56
57#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
59pub struct ServerEntry {
60 pub id: String,
62 pub command: String,
64 pub status: String,
66}
67
68#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
70pub struct ServerList {
71 pub servers: Vec<ServerEntry>,
73}
74
75#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
77pub struct ServerInfo {
78 pub id: String,
80 pub name: String,
82 pub version: String,
84 pub command: String,
86 pub status: String,
88 pub tools: Vec<ToolSummary>,
90 pub capabilities: Vec<String>,
92}
93
94#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
96pub struct ToolSummary {
97 pub name: String,
99 pub description: String,
101}
102
103#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
105pub struct ValidationResult {
106 pub command: String,
108 pub valid: bool,
110 pub message: String,
112}
113
114#[derive(Debug)]
118struct ServerManager {
119 config_path: PathBuf,
120}
121
122impl ServerManager {
123 fn new() -> Result<Self> {
127 let config_path = Self::find_config_path()?;
128 Ok(Self { config_path })
129 }
130
131 fn find_config_path() -> Result<PathBuf> {
138 let home = dirs::home_dir().context("Failed to determine home directory")?;
139
140 let paths = if cfg!(target_os = "macos") {
141 vec![
142 home.join("Library")
143 .join("Application Support")
144 .join("Claude")
145 .join("claude_desktop_config.json"),
146 ]
147 } else if cfg!(target_os = "windows") {
148 let appdata = std::env::var("APPDATA")
149 .map_or_else(|_| home.join("AppData").join("Roaming"), PathBuf::from);
150 vec![appdata.join("Claude").join("claude_desktop_config.json")]
151 } else {
152 vec![
154 home.join(".config")
155 .join("Claude")
156 .join("claude_desktop_config.json"),
157 ]
158 };
159
160 if let Ok(custom_path) = std::env::var("CLAUDE_CONFIG_PATH") {
162 let custom = PathBuf::from(custom_path);
163 if custom.exists() {
164 debug!("Using config from CLAUDE_CONFIG_PATH: {}", custom.display());
165 return Ok(custom);
166 }
167 }
168
169 for path in paths {
171 if path.exists() {
172 debug!("Found Claude Desktop config at: {}", path.display());
173 return Ok(path);
174 }
175 }
176
177 anyhow::bail!(
178 "Claude Desktop configuration not found. \
179 Please ensure Claude Desktop is installed or set CLAUDE_CONFIG_PATH environment variable."
180 )
181 }
182
183 fn read_config(&self) -> Result<ClaudeDesktopConfig> {
185 let contents = std::fs::read_to_string(&self.config_path).context(format!(
186 "Failed to read config file: {}",
187 self.config_path.display()
188 ))?;
189
190 let config: ClaudeDesktopConfig = serde_json::from_str(&contents).context(format!(
191 "Failed to parse config file: {}",
192 self.config_path.display()
193 ))?;
194
195 Ok(config)
196 }
197
198 fn list_servers(&self) -> Result<Vec<(String, ServerConfig)>> {
200 let config = self.read_config()?;
201 Ok(config.mcp_execution_servers.into_iter().collect())
202 }
203
204 fn get_server_config(&self, server_name: &str) -> Result<ServerConfig> {
206 let config = self.read_config()?;
207 config
208 .mcp_execution_servers
209 .get(server_name)
210 .cloned()
211 .context(format!("Server '{server_name}' not found in configuration"))
212 }
213
214 fn build_command_string(config: &ServerConfig) -> String {
216 if config.args.is_empty() {
217 config.command.clone()
218 } else {
219 format!("{} {}", config.command, config.args.join(" "))
220 }
221 }
222
223 fn check_command_exists(command: &str) -> bool {
225 which::which(command).is_ok()
226 }
227
228 async fn validate_server(&self, server_name: &str) -> Result<ServerStatus> {
230 let config = self.get_server_config(server_name)?;
231
232 if !Self::check_command_exists(&config.command) {
234 warn!(
235 "Command '{}' not found in PATH for server '{}'",
236 config.command, server_name
237 );
238 return Ok(ServerStatus::Unavailable);
239 }
240
241 match self.introspect_server(server_name).await {
243 Ok(_) => Ok(ServerStatus::Available),
244 Err(e) => {
245 warn!("Failed to introspect server '{}': {}", server_name, e);
246 Ok(ServerStatus::Unavailable)
247 }
248 }
249 }
250
251 async fn introspect_server(
253 &self,
254 server_name: &str,
255 ) -> Result<mcp_execution_introspector::ServerInfo> {
256 let config = self.get_server_config(server_name)?;
257
258 let mut introspector = Introspector::new();
259 let server_id = ServerId::new(server_name);
260
261 let mut builder = CoreServerConfig::builder().command(config.command.clone());
263
264 if !config.args.is_empty() {
265 builder = builder.args(config.args.clone());
266 }
267
268 for (key, value) in &config.env {
269 builder = builder.env(key.clone(), value.clone());
270 }
271
272 let server_config = builder.build();
273
274 introspector
275 .discover_server(server_id, &server_config)
276 .await
277 .context(format!("Failed to introspect server '{server_name}'"))
278 }
279}
280
281pub async fn run(action: ServerAction, output_format: OutputFormat) -> Result<ExitCode> {
310 info!("Server action: {:?}", action);
311 info!("Output format: {}", output_format);
312
313 match action {
314 ServerAction::List => list_servers(output_format).await,
315 ServerAction::Info { server } => show_server_info(server, output_format).await,
316 ServerAction::Validate { command } => validate_command(command, output_format).await,
317 }
318}
319
320async fn list_servers(output_format: OutputFormat) -> Result<ExitCode> {
324 let manager = ServerManager::new().context("Failed to initialize server manager")?;
325
326 let servers = manager
327 .list_servers()
328 .context("Failed to read server configuration")?;
329
330 if servers.is_empty() {
331 info!("No MCP servers configured in Claude Desktop");
332 let server_list = ServerList {
333 servers: Vec::new(),
334 };
335 let formatted = crate::formatters::format_output(&server_list, output_format)?;
336 println!("{formatted}");
337 return Ok(ExitCode::SUCCESS);
338 }
339
340 let mut entries = Vec::new();
342 for (name, config) in servers {
343 let command = ServerManager::build_command_string(&config);
344 let status = if ServerManager::check_command_exists(&config.command) {
345 ServerStatus::Available
346 } else {
347 ServerStatus::Unavailable
348 };
349
350 entries.push(ServerEntry {
351 id: name,
352 command,
353 status: status.as_str().to_string(),
354 });
355 }
356
357 let server_list = ServerList { servers: entries };
358
359 let formatted = crate::formatters::format_output(&server_list, output_format)?;
360 println!("{formatted}");
361
362 Ok(ExitCode::SUCCESS)
363}
364
365async fn show_server_info(server: String, output_format: OutputFormat) -> Result<ExitCode> {
369 let manager = ServerManager::new().context("Failed to initialize server manager")?;
370
371 let config = manager
372 .get_server_config(&server)
373 .context(format!("Server '{server}' not found in configuration"))?;
374
375 let command = ServerManager::build_command_string(&config);
376
377 info!("Introspecting server '{}'...", server);
379 match manager.introspect_server(&server).await {
380 Ok(introspected) => {
381 let mut capabilities = Vec::new();
383 if introspected.capabilities.supports_tools {
384 capabilities.push("tools".to_string());
385 }
386 if introspected.capabilities.supports_resources {
387 capabilities.push("resources".to_string());
388 }
389 if introspected.capabilities.supports_prompts {
390 capabilities.push("prompts".to_string());
391 }
392
393 let tools = introspected
394 .tools
395 .iter()
396 .map(|t| ToolSummary {
397 name: t.name.as_str().to_string(),
398 description: t.description.clone(),
399 })
400 .collect();
401
402 let server_info = ServerInfo {
403 id: server,
404 name: introspected.name,
405 version: introspected.version,
406 command,
407 status: ServerStatus::Available.as_str().to_string(),
408 tools,
409 capabilities,
410 };
411
412 let formatted = crate::formatters::format_output(&server_info, output_format)?;
413 println!("{formatted}");
414
415 Ok(ExitCode::SUCCESS)
416 }
417 Err(e) => {
418 warn!("Failed to introspect server '{}': {}", server, e);
420
421 let server_info = ServerInfo {
422 id: server.clone(),
423 name: server,
424 version: "unknown".to_string(),
425 command,
426 status: ServerStatus::Unavailable.as_str().to_string(),
427 tools: Vec::new(),
428 capabilities: Vec::new(),
429 };
430
431 let formatted = crate::formatters::format_output(&server_info, output_format)?;
432 println!("{formatted}");
433
434 Ok(ExitCode::ERROR)
436 }
437 }
438}
439
440async fn validate_command(server_name: String, output_format: OutputFormat) -> Result<ExitCode> {
445 let manager = ServerManager::new().context("Failed to initialize server manager")?;
446
447 let config = match manager.get_server_config(&server_name) {
449 Ok(cfg) => cfg,
450 Err(e) => {
451 let result = ValidationResult {
452 command: server_name,
453 valid: false,
454 message: format!("Server not found in configuration: {e}"),
455 };
456 let formatted = crate::formatters::format_output(&result, output_format)?;
457 println!("{formatted}");
458 return Ok(ExitCode::ERROR);
459 }
460 };
461
462 let command = ServerManager::build_command_string(&config);
463 info!("Validating server '{}'...", server_name);
464
465 if !ServerManager::check_command_exists(&config.command) {
467 let result = ValidationResult {
468 command: command.clone(),
469 valid: false,
470 message: format!("Command '{}' not found in PATH", config.command),
471 };
472 let formatted = crate::formatters::format_output(&result, output_format)?;
473 println!("{formatted}");
474 return Ok(ExitCode::ERROR);
475 }
476
477 match manager.validate_server(&server_name).await? {
479 ServerStatus::Available => {
480 let result = ValidationResult {
481 command,
482 valid: true,
483 message: format!(
484 "Server '{server_name}' is available and responds to MCP protocol"
485 ),
486 };
487 let formatted = crate::formatters::format_output(&result, output_format)?;
488 println!("{formatted}");
489 Ok(ExitCode::SUCCESS)
490 }
491 ServerStatus::Unavailable => {
492 let result = ValidationResult {
493 command,
494 valid: false,
495 message: format!(
496 "Server '{server_name}' command exists but failed to respond to MCP protocol"
497 ),
498 };
499 let formatted = crate::formatters::format_output(&result, output_format)?;
500 println!("{formatted}");
501 Ok(ExitCode::ERROR)
502 }
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use std::io::Write;
510
511 fn create_test_config(content: &str) -> tempfile::NamedTempFile {
513 let mut file = tempfile::NamedTempFile::new().unwrap();
514 file.write_all(content.as_bytes()).unwrap();
515 file.flush().unwrap();
516 file
517 }
518
519 #[test]
520 fn test_server_status_as_str() {
521 assert_eq!(ServerStatus::Available.as_str(), "available");
522 assert_eq!(ServerStatus::Unavailable.as_str(), "unavailable");
523 }
524
525 #[test]
526 fn test_server_config_deserialization() {
527 let json = r#"{
528 "command": "node",
529 "args": ["/path/to/server.js"],
530 "env": {"KEY": "value"}
531 }"#;
532
533 let config: ServerConfig = serde_json::from_str(json).unwrap();
534 assert_eq!(config.command, "node");
535 assert_eq!(config.args, vec!["/path/to/server.js"]);
536 assert_eq!(config.env.get("KEY"), Some(&"value".to_string()));
537 }
538
539 #[test]
540 fn test_server_config_deserialization_minimal() {
541 let json = r#"{
542 "command": "python"
543 }"#;
544
545 let config: ServerConfig = serde_json::from_str(json).unwrap();
546 assert_eq!(config.command, "python");
547 assert!(config.args.is_empty());
548 assert!(config.env.is_empty());
549 }
550
551 #[test]
552 fn test_claude_desktop_config_deserialization() {
553 let json = r#"{
554 "mcpServers": {
555 "test-server": {
556 "command": "node",
557 "args": ["server.js"]
558 }
559 }
560 }"#;
561
562 let config: ClaudeDesktopConfig = serde_json::from_str(json).unwrap();
563 assert!(config.mcp_execution_servers.contains_key("test-server"));
564 }
565
566 #[test]
567 fn test_build_command_string_no_args() {
568 let config = ServerConfig {
569 command: "node".to_string(),
570 args: Vec::new(),
571 env: HashMap::new(),
572 };
573
574 assert_eq!(ServerManager::build_command_string(&config), "node");
575 }
576
577 #[test]
578 fn test_build_command_string_with_args() {
579 let config = ServerConfig {
580 command: "node".to_string(),
581 args: vec!["/path/to/server.js".to_string(), "--verbose".to_string()],
582 env: HashMap::new(),
583 };
584
585 assert_eq!(
586 ServerManager::build_command_string(&config),
587 "node /path/to/server.js --verbose"
588 );
589 }
590
591 #[test]
592 fn test_check_command_exists() {
593 assert!(ServerManager::check_command_exists("ls"));
595
596 assert!(!ServerManager::check_command_exists(
598 "this_command_definitely_does_not_exist_12345"
599 ));
600 }
601
602 #[test]
603 fn test_server_manager_read_config() {
604 let config_content = r#"{
605 "mcpServers": {
606 "test-server": {
607 "command": "node",
608 "args": ["server.js"]
609 }
610 }
611 }"#;
612
613 let temp_file = create_test_config(config_content);
614
615 let manager = ServerManager {
616 config_path: temp_file.path().to_path_buf(),
617 };
618
619 let config = manager.read_config().unwrap();
620 assert_eq!(config.mcp_execution_servers.len(), 1);
621 assert!(config.mcp_execution_servers.contains_key("test-server"));
622 }
623
624 #[test]
625 fn test_server_manager_list_servers() {
626 let config_content = r#"{
627 "mcpServers": {
628 "server1": {
629 "command": "node",
630 "args": ["s1.js"]
631 },
632 "server2": {
633 "command": "python",
634 "args": ["s2.py"]
635 }
636 }
637 }"#;
638
639 let temp_file = create_test_config(config_content);
640
641 let manager = ServerManager {
642 config_path: temp_file.path().to_path_buf(),
643 };
644
645 let servers = manager.list_servers().unwrap();
646 assert_eq!(servers.len(), 2);
647
648 let names: Vec<String> = servers.iter().map(|(name, _)| name.clone()).collect();
649 assert!(names.contains(&"server1".to_string()));
650 assert!(names.contains(&"server2".to_string()));
651 }
652
653 #[test]
654 fn test_server_manager_get_server_config() {
655 let config_content = r#"{
656 "mcpServers": {
657 "test-server": {
658 "command": "node",
659 "args": ["server.js"]
660 }
661 }
662 }"#;
663
664 let temp_file = create_test_config(config_content);
665
666 let manager = ServerManager {
667 config_path: temp_file.path().to_path_buf(),
668 };
669
670 let config = manager.get_server_config("test-server").unwrap();
671 assert_eq!(config.command, "node");
672 assert_eq!(config.args, vec!["server.js"]);
673 }
674
675 #[test]
676 fn test_server_manager_get_server_config_not_found() {
677 let config_content = r#"{
678 "mcpServers": {}
679 }"#;
680
681 let temp_file = create_test_config(config_content);
682
683 let manager = ServerManager {
684 config_path: temp_file.path().to_path_buf(),
685 };
686
687 let result = manager.get_server_config("nonexistent");
688 assert!(result.is_err());
689 assert!(
690 result
691 .unwrap_err()
692 .to_string()
693 .contains("not found in configuration")
694 );
695 }
696
697 #[test]
698 fn test_server_entry_serialization() {
699 let entry = ServerEntry {
700 id: "test".to_string(),
701 command: "test-cmd".to_string(),
702 status: "available".to_string(),
703 };
704
705 let json = serde_json::to_string(&entry).unwrap();
706 assert!(json.contains("test"));
707 assert!(json.contains("test-cmd"));
708 assert!(json.contains("available"));
709 }
710
711 #[test]
712 fn test_server_list_serialization() {
713 let list = ServerList {
714 servers: vec![ServerEntry {
715 id: "test".to_string(),
716 command: "test-cmd".to_string(),
717 status: "available".to_string(),
718 }],
719 };
720
721 let json = serde_json::to_string(&list).unwrap();
722 assert!(json.contains("servers"));
723 assert!(json.contains("test"));
724 }
725
726 #[test]
727 fn test_server_info_serialization() {
728 let info = ServerInfo {
729 id: "test".to_string(),
730 name: "Test Server".to_string(),
731 version: "1.0.0".to_string(),
732 command: "test-cmd".to_string(),
733 status: "available".to_string(),
734 tools: vec![ToolSummary {
735 name: "test_tool".to_string(),
736 description: "A test tool".to_string(),
737 }],
738 capabilities: vec!["tools".to_string()],
739 };
740
741 let json = serde_json::to_string(&info).unwrap();
742 assert!(json.contains("test"));
743 assert!(json.contains("Test Server"));
744 assert!(json.contains("capabilities"));
745 assert!(json.contains("tools"));
746 }
747
748 #[test]
749 fn test_tool_summary_serialization() {
750 let tool = ToolSummary {
751 name: "send_message".to_string(),
752 description: "Sends a message".to_string(),
753 };
754
755 let json = serde_json::to_string(&tool).unwrap();
756 assert!(json.contains("send_message"));
757 assert!(json.contains("Sends a message"));
758 }
759
760 #[test]
761 fn test_validation_result_serialization() {
762 let result = ValidationResult {
763 command: "test".to_string(),
764 valid: true,
765 message: "ok".to_string(),
766 };
767
768 let json = serde_json::to_string(&result).unwrap();
769 assert!(json.contains("command"));
770 assert!(json.contains("valid"));
771 assert!(json.contains("message"));
772 }
773
774 #[tokio::test]
776 #[ignore = "requires CLAUDE_CONFIG_PATH environment variable"]
777 async fn test_list_servers_integration() {
778 if std::env::var("CLAUDE_CONFIG_PATH").is_err() {
780 return;
781 }
782
783 let result = run(ServerAction::List, OutputFormat::Json).await;
784 assert!(result.is_ok());
785 }
786
787 #[tokio::test]
788 #[ignore = "requires CLAUDE_CONFIG_PATH and configured server"]
789 async fn test_server_info_integration() {
790 if std::env::var("CLAUDE_CONFIG_PATH").is_err() {
792 return;
793 }
794
795 let result = run(
797 ServerAction::Info {
798 server: "test-server".to_string(),
799 },
800 OutputFormat::Json,
801 )
802 .await;
803
804 assert!(result.is_ok());
806 }
807}