Skip to main content

mcp_execution_cli/commands/
server.rs

1//! Server command implementation.
2//!
3//! Manages MCP server listing, inspection, and validation using
4//! `~/.claude/mcp.json` as the single source of truth for server definitions.
5
6use crate::actions::ServerAction;
7use crate::commands::common::{McpServerEntry, get_mcp_server, list_mcp_servers};
8use anyhow::{Context, Result};
9use mcp_execution_core::cli::{ExitCode, OutputFormat};
10use mcp_execution_introspector::Introspector;
11use serde::Serialize;
12use tracing::{info, warn};
13
14/// Status of a configured server.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16enum ServerStatus {
17    /// Server command exists and is executable.
18    Available,
19    /// Server command not found in PATH.
20    Unavailable,
21}
22
23impl ServerStatus {
24    const fn as_str(self) -> &'static str {
25        match self {
26            Self::Available => "available",
27            Self::Unavailable => "unavailable",
28        }
29    }
30}
31
32/// Represents a configured server entry for output.
33#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
34pub struct ServerEntry {
35    /// Server identifier.
36    pub id: String,
37    /// Command used to start the server.
38    pub command: String,
39    /// Current server status.
40    pub status: String,
41}
42
43/// List of configured servers.
44#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
45pub struct ServerList {
46    /// All configured servers.
47    pub servers: Vec<ServerEntry>,
48}
49
50/// Detailed server information for output.
51#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
52pub struct ServerInfo {
53    /// Server identifier.
54    pub id: String,
55    /// Server name from introspection.
56    pub name: String,
57    /// Server version.
58    pub version: String,
59    /// Command used to start the server.
60    pub command: String,
61    /// Current server status.
62    pub status: String,
63    /// Available tools.
64    pub tools: Vec<ToolSummary>,
65    /// Server capabilities.
66    pub capabilities: Vec<String>,
67}
68
69/// Tool summary for output.
70#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
71pub struct ToolSummary {
72    /// Tool name.
73    pub name: String,
74    /// Tool description.
75    pub description: String,
76}
77
78/// Validation result for a server command.
79#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
80pub struct ValidationResult {
81    /// The validated command.
82    pub command: String,
83    /// Whether the command is valid.
84    pub valid: bool,
85    /// Validation message.
86    pub message: String,
87}
88
89/// Runs the server command.
90///
91/// Manages server listing, detailed info, and validation.
92/// All server definitions are loaded from `~/.claude/mcp.json`.
93///
94/// # Arguments
95///
96/// * `action` - Server management action
97/// * `output_format` - Output format (json, text, pretty)
98///
99/// # Errors
100///
101/// Returns an error if the server operation fails.
102///
103/// # Examples
104///
105/// ```no_run
106/// use mcp_execution_cli::commands::server;
107/// use mcp_execution_core::cli::{ExitCode, OutputFormat};
108///
109/// # #[tokio::main]
110/// # async fn main() {
111/// let result = server::run(
112///     mcp_execution_cli::ServerAction::List,
113///     OutputFormat::Json
114/// ).await;
115/// assert!(result.is_ok());
116/// # }
117/// ```
118pub async fn run(action: ServerAction, output_format: OutputFormat) -> Result<ExitCode> {
119    info!("Server action: {:?}", action);
120    info!("Output format: {}", output_format);
121
122    match action {
123        ServerAction::List => list_servers(output_format).await,
124        ServerAction::Info { server } => show_server_info(server, output_format).await,
125        ServerAction::Validate { command } => validate_command(command, output_format).await,
126    }
127}
128
129/// Lists all servers configured in `~/.claude/mcp.json`.
130///
131/// Returns an empty list (not an error) when the config file does not exist.
132async fn list_servers(output_format: OutputFormat) -> Result<ExitCode> {
133    let servers = list_mcp_servers()
134        .context("failed to read server configuration from ~/.claude/mcp.json")?;
135
136    if servers.is_empty() {
137        info!("No MCP servers configured in ~/.claude/mcp.json");
138        let server_list = ServerList {
139            servers: Vec::new(),
140        };
141        let formatted = crate::formatters::format_output(&server_list, output_format)?;
142        println!("{formatted}");
143        return Ok(ExitCode::SUCCESS);
144    }
145
146    let mut entries = Vec::new();
147    for (name, entry) in servers {
148        let command = build_command_string(&entry);
149        let status = if check_command_exists(&entry.command) {
150            ServerStatus::Available
151        } else {
152            ServerStatus::Unavailable
153        };
154
155        entries.push(ServerEntry {
156            id: name,
157            command,
158            status: status.as_str().to_string(),
159        });
160    }
161
162    let server_list = ServerList { servers: entries };
163    let formatted = crate::formatters::format_output(&server_list, output_format)?;
164    println!("{formatted}");
165
166    Ok(ExitCode::SUCCESS)
167}
168
169/// Shows detailed information about a specific server.
170///
171/// Connects to the server and introspects its capabilities, tools, and status.
172async fn show_server_info(server: String, output_format: OutputFormat) -> Result<ExitCode> {
173    let (server_id, server_config, entry) = get_mcp_server(&server)
174        .with_context(|| format!("server '{server}' not found in ~/.claude/mcp.json"))?;
175
176    let command = build_command_string(&entry);
177
178    info!("Introspecting server '{}'...", server);
179
180    let mut introspector = Introspector::new();
181    match introspector
182        .discover_server(server_id, &server_config)
183        .await
184    {
185        Ok(introspected) => {
186            let mut capabilities = Vec::new();
187            if introspected.capabilities.supports_tools {
188                capabilities.push("tools".to_string());
189            }
190            if introspected.capabilities.supports_resources {
191                capabilities.push("resources".to_string());
192            }
193            if introspected.capabilities.supports_prompts {
194                capabilities.push("prompts".to_string());
195            }
196
197            let tools = introspected
198                .tools
199                .iter()
200                .map(|t| ToolSummary {
201                    name: t.name.as_str().to_string(),
202                    description: t.description.clone(),
203                })
204                .collect();
205
206            let server_info = ServerInfo {
207                id: server,
208                name: introspected.name,
209                version: introspected.version,
210                command,
211                status: ServerStatus::Available.as_str().to_string(),
212                tools,
213                capabilities,
214            };
215
216            let formatted = crate::formatters::format_output(&server_info, output_format)?;
217            println!("{formatted}");
218
219            Ok(ExitCode::SUCCESS)
220        }
221        Err(e) => {
222            warn!("Failed to introspect server '{}': {}", server, e);
223
224            let server_info = ServerInfo {
225                id: server.clone(),
226                name: server,
227                version: "unknown".to_string(),
228                command,
229                status: ServerStatus::Unavailable.as_str().to_string(),
230                tools: Vec::new(),
231                capabilities: Vec::new(),
232            };
233
234            let formatted = crate::formatters::format_output(&server_info, output_format)?;
235            println!("{formatted}");
236
237            Ok(ExitCode::ERROR)
238        }
239    }
240}
241
242/// Validates a server by checking its command and attempting introspection.
243///
244/// The server must be configured in `~/.claude/mcp.json`.
245async fn validate_command(server_name: String, output_format: OutputFormat) -> Result<ExitCode> {
246    let (server_id, server_config, entry) = match get_mcp_server(&server_name) {
247        Ok(result) => result,
248        Err(e) => {
249            let result = ValidationResult {
250                command: server_name,
251                valid: false,
252                message: format!("Server not found in configuration: {e}"),
253            };
254            let formatted = crate::formatters::format_output(&result, output_format)?;
255            println!("{formatted}");
256            return Ok(ExitCode::ERROR);
257        }
258    };
259
260    let command = build_command_string(&entry);
261    info!("Validating server '{}'...", server_name);
262
263    if !check_command_exists(&entry.command) {
264        let result = ValidationResult {
265            command: command.clone(),
266            valid: false,
267            message: format!("Command '{}' not found in PATH", entry.command),
268        };
269        let formatted = crate::formatters::format_output(&result, output_format)?;
270        println!("{formatted}");
271        return Ok(ExitCode::ERROR);
272    }
273
274    let mut introspector = Introspector::new();
275    match introspector
276        .discover_server(server_id, &server_config)
277        .await
278    {
279        Ok(_) => {
280            let result = ValidationResult {
281                command,
282                valid: true,
283                message: format!(
284                    "Server '{server_name}' is available and responds to MCP protocol"
285                ),
286            };
287            let formatted = crate::formatters::format_output(&result, output_format)?;
288            println!("{formatted}");
289            Ok(ExitCode::SUCCESS)
290        }
291        Err(e) => {
292            warn!(
293                "Failed to introspect server '{}' during validation: {}",
294                server_name, e
295            );
296            let result = ValidationResult {
297                command,
298                valid: false,
299                message: format!(
300                    "Server '{server_name}' command exists but failed to respond to MCP protocol"
301                ),
302            };
303            let formatted = crate::formatters::format_output(&result, output_format)?;
304            println!("{formatted}");
305            Ok(ExitCode::ERROR)
306        }
307    }
308}
309
310/// Builds a displayable command string from a server entry.
311fn build_command_string(entry: &McpServerEntry) -> String {
312    if entry.args.is_empty() {
313        entry.command.clone()
314    } else {
315        format!("{} {}", entry.command, entry.args.join(" "))
316    }
317}
318
319/// Returns `true` if the given command binary is available in PATH.
320fn check_command_exists(command: &str) -> bool {
321    which::which(command).is_ok()
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use std::collections::HashMap;
328
329    #[test]
330    fn test_server_status_as_str() {
331        assert_eq!(ServerStatus::Available.as_str(), "available");
332        assert_eq!(ServerStatus::Unavailable.as_str(), "unavailable");
333    }
334
335    #[test]
336    fn test_build_command_string_no_args() {
337        let entry = McpServerEntry {
338            command: "node".to_string(),
339            args: Vec::new(),
340            env: HashMap::default(),
341        };
342        assert_eq!(build_command_string(&entry), "node");
343    }
344
345    #[test]
346    fn test_build_command_string_with_args() {
347        let entry = McpServerEntry {
348            command: "node".to_string(),
349            args: vec!["/path/to/server.js".to_string(), "--verbose".to_string()],
350            env: HashMap::default(),
351        };
352        assert_eq!(
353            build_command_string(&entry),
354            "node /path/to/server.js --verbose"
355        );
356    }
357
358    #[test]
359    fn test_check_command_exists() {
360        assert!(check_command_exists("ls"));
361        assert!(!check_command_exists(
362            "this_command_definitely_does_not_exist_12345"
363        ));
364    }
365
366    #[test]
367    fn test_server_entry_serialization() {
368        let entry = ServerEntry {
369            id: "test".to_string(),
370            command: "test-cmd".to_string(),
371            status: "available".to_string(),
372        };
373
374        let json = serde_json::to_string(&entry).unwrap();
375        assert!(json.contains("test"));
376        assert!(json.contains("test-cmd"));
377        assert!(json.contains("available"));
378    }
379
380    #[test]
381    fn test_server_list_serialization() {
382        let list = ServerList {
383            servers: vec![ServerEntry {
384                id: "test".to_string(),
385                command: "test-cmd".to_string(),
386                status: "available".to_string(),
387            }],
388        };
389
390        let json = serde_json::to_string(&list).unwrap();
391        assert!(json.contains("servers"));
392        assert!(json.contains("test"));
393    }
394
395    #[test]
396    fn test_server_info_serialization() {
397        let info = ServerInfo {
398            id: "test".to_string(),
399            name: "Test Server".to_string(),
400            version: "1.0.0".to_string(),
401            command: "test-cmd".to_string(),
402            status: "available".to_string(),
403            tools: vec![ToolSummary {
404                name: "test_tool".to_string(),
405                description: "A test tool".to_string(),
406            }],
407            capabilities: vec!["tools".to_string()],
408        };
409
410        let json = serde_json::to_string(&info).unwrap();
411        assert!(json.contains("test"));
412        assert!(json.contains("Test Server"));
413        assert!(json.contains("capabilities"));
414        assert!(json.contains("tools"));
415    }
416
417    #[test]
418    fn test_tool_summary_serialization() {
419        let tool = ToolSummary {
420            name: "send_message".to_string(),
421            description: "Sends a message".to_string(),
422        };
423
424        let json = serde_json::to_string(&tool).unwrap();
425        assert!(json.contains("send_message"));
426        assert!(json.contains("Sends a message"));
427    }
428
429    #[test]
430    fn test_validation_result_serialization() {
431        let result = ValidationResult {
432            command: "test".to_string(),
433            valid: true,
434            message: "ok".to_string(),
435        };
436
437        let json = serde_json::to_string(&result).unwrap();
438        assert!(json.contains("command"));
439        assert!(json.contains("valid"));
440        assert!(json.contains("message"));
441    }
442}