mcp_execution_cli/commands/
server.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16enum ServerStatus {
17 Available,
19 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#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
34pub struct ServerEntry {
35 pub id: String,
37 pub command: String,
39 pub status: String,
41}
42
43#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
45pub struct ServerList {
46 pub servers: Vec<ServerEntry>,
48}
49
50#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
52pub struct ServerInfo {
53 pub id: String,
55 pub name: String,
57 pub version: String,
59 pub command: String,
61 pub status: String,
63 pub tools: Vec<ToolSummary>,
65 pub capabilities: Vec<String>,
67}
68
69#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
71pub struct ToolSummary {
72 pub name: String,
74 pub description: String,
76}
77
78#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
80pub struct ValidationResult {
81 pub command: String,
83 pub valid: bool,
85 pub message: String,
87}
88
89pub 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
129async 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
169async 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
242async 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
310fn 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
319fn 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}