mcp_execution_cli/commands/
introspect.rs

1//! Introspect command implementation.
2//!
3//! Connects to an MCP server and displays its capabilities, tools, and metadata.
4
5use super::common::{build_server_config, load_server_from_config};
6use anyhow::{Context, Result};
7use mcp_execution_core::cli::{ExitCode, OutputFormat};
8use mcp_execution_introspector::{Introspector, ServerInfo, ToolInfo};
9use serde::Serialize;
10use tracing::{debug, info};
11
12/// Result of server introspection.
13///
14/// Contains server information and list of available tools,
15/// formatted for display to the user.
16///
17/// # Examples
18///
19/// ```
20/// use mcp_execution_cli::commands::introspect::{IntrospectionResult, ServerMetadata};
21///
22/// let result = IntrospectionResult {
23///     server: ServerMetadata {
24///         id: "github".to_string(),
25///         name: "github".to_string(),
26///         version: "1.0.0".to_string(),
27///         supports_tools: true,
28///         supports_resources: false,
29///         supports_prompts: false,
30///     },
31///     tools: vec![],
32/// };
33///
34/// assert_eq!(result.server.name, "github");
35/// ```
36#[derive(Debug, Clone, Serialize)]
37pub struct IntrospectionResult {
38    /// Server metadata
39    pub server: ServerMetadata,
40    /// List of available tools
41    pub tools: Vec<ToolMetadata>,
42}
43
44/// Server metadata for display.
45///
46/// Simplified representation of server information optimized
47/// for CLI output formatting.
48#[derive(Debug, Clone, Serialize)]
49pub struct ServerMetadata {
50    /// Server identifier
51    pub id: String,
52    /// Server name
53    pub name: String,
54    /// Server version
55    pub version: String,
56    /// Whether server supports tools
57    pub supports_tools: bool,
58    /// Whether server supports resources
59    pub supports_resources: bool,
60    /// Whether server supports prompts
61    pub supports_prompts: bool,
62}
63
64/// Tool metadata for display.
65///
66/// Contains tool information with optional schema details
67/// when detailed output is requested.
68#[derive(Debug, Clone, Serialize)]
69pub struct ToolMetadata {
70    /// Tool name
71    pub name: String,
72    /// Tool description
73    pub description: String,
74    /// Input schema (only included when detailed mode is enabled)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub input_schema: Option<serde_json::Value>,
77    /// Output schema (only included when detailed mode is enabled and available)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub output_schema: Option<serde_json::Value>,
80}
81
82/// Runs the introspect command.
83///
84/// Connects to the specified server, discovers its tools, and displays
85/// information according to the output format.
86///
87/// # Process
88///
89/// 1. Builds `ServerConfig` from CLI arguments or loads from ~/.claude/mcp.json
90/// 2. Creates an introspector and connects to the server
91/// 3. Discovers server capabilities and tools
92/// 4. Formats the output according to the specified format
93/// 5. Displays the results to stdout
94///
95/// # Arguments
96///
97/// * `from_config` - Load server config from ~/.claude/mcp.json by name
98/// * `server` - Server command (binary name or path), None for HTTP/SSE
99/// * `args` - Arguments to pass to the server command
100/// * `env` - Environment variables in KEY=VALUE format
101/// * `cwd` - Working directory for the server process
102/// * `http` - HTTP transport URL
103/// * `sse` - SSE transport URL
104/// * `headers` - HTTP headers in KEY=VALUE format
105/// * `detailed` - Whether to show detailed tool schemas
106/// * `output_format` - Output format (json, text, pretty)
107///
108/// # Errors
109///
110/// Returns an error if:
111/// - Server configuration is invalid
112/// - Server connection fails
113/// - Server introspection fails
114/// - Output formatting fails
115///
116/// # Examples
117///
118/// ```no_run
119/// use mcp_execution_cli::commands::introspect;
120/// use mcp_execution_core::cli::OutputFormat;
121///
122/// # async fn example() -> anyhow::Result<()> {
123/// // Simple server
124/// let exit_code = introspect::run(
125///     None,
126///     Some("github-mcp-server".to_string()),
127///     vec!["stdio".to_string()],
128///     vec![],
129///     None,
130///     None,
131///     None,
132///     vec![],
133///     false,
134///     OutputFormat::Json
135/// ).await?;
136///
137/// // HTTP transport
138/// let exit_code = introspect::run(
139///     None,
140///     None,
141///     vec![],
142///     vec![],
143///     None,
144///     Some("https://api.githubcopilot.com/mcp/".to_string()),
145///     None,
146///     vec!["Authorization=Bearer token".to_string()],
147///     false,
148///     OutputFormat::Json
149/// ).await?;
150/// # Ok(())
151/// # }
152/// ```
153#[allow(clippy::too_many_arguments)]
154pub async fn run(
155    from_config: Option<String>,
156    server: Option<String>,
157    args: Vec<String>,
158    env: Vec<String>,
159    cwd: Option<String>,
160    http: Option<String>,
161    sse: Option<String>,
162    headers: Vec<String>,
163    detailed: bool,
164    output_format: OutputFormat,
165) -> Result<ExitCode> {
166    // Build server config: either from mcp.json or from CLI arguments
167    let (server_id, config) = if let Some(config_name) = from_config {
168        debug!(
169            "Loading server configuration from ~/.claude/mcp.json: {}",
170            config_name
171        );
172        load_server_from_config(&config_name)?
173    } else {
174        build_server_config(server, args, env, cwd, http, sse, headers)?
175    };
176
177    info!("Introspecting server: {}", server_id);
178    info!("Transport: {:?}", config.transport());
179    info!("Detailed: {}", detailed);
180    info!("Output format: {}", output_format);
181
182    // Create introspector
183    let mut introspector = Introspector::new();
184
185    // Discover server
186    let server_info = introspector
187        .discover_server(server_id.clone(), &config)
188        .await
189        .with_context(|| {
190            format!(
191                "failed to connect to server '{server_id}' - ensure the server is installed and accessible"
192            )
193        })?;
194
195    info!(
196        "Successfully discovered {} tools from server",
197        server_info.tools.len()
198    );
199
200    // Build result
201    let result = build_result(&server_info, detailed);
202
203    // Format and display output
204    let formatted = crate::formatters::format_output(&result, output_format)
205        .context("failed to format introspection results")?;
206
207    println!("{formatted}");
208
209    Ok(ExitCode::SUCCESS)
210}
211
212/// Builds the introspection result from server info.
213///
214/// Transforms `ServerInfo` into `IntrospectionResult` suitable for CLI display.
215///
216/// # Arguments
217///
218/// * `server_info` - Server information from introspector
219/// * `detailed` - Whether to include detailed tool schemas
220///
221/// # Examples
222///
223/// ```
224/// use mcp_execution_cli::commands::introspect::build_result;
225/// use mcp_execution_introspector::{ServerInfo, ServerCapabilities};
226/// use mcp_execution_core::ServerId;
227///
228/// let server_info = ServerInfo {
229///     id: ServerId::new("test"),
230///     name: "Test Server".to_string(),
231///     version: "1.0.0".to_string(),
232///     tools: vec![],
233///     capabilities: ServerCapabilities {
234///         supports_tools: true,
235///         supports_resources: false,
236///         supports_prompts: false,
237///     },
238/// };
239///
240/// let result = build_result(&server_info, false);
241/// assert_eq!(result.server.name, "Test Server");
242/// assert_eq!(result.tools.len(), 0);
243/// ```
244#[must_use]
245pub fn build_result(server_info: &ServerInfo, detailed: bool) -> IntrospectionResult {
246    let server = ServerMetadata {
247        id: server_info.id.as_str().to_string(),
248        name: server_info.name.clone(),
249        version: server_info.version.clone(),
250        supports_tools: server_info.capabilities.supports_tools,
251        supports_resources: server_info.capabilities.supports_resources,
252        supports_prompts: server_info.capabilities.supports_prompts,
253    };
254
255    let tools = server_info
256        .tools
257        .iter()
258        .map(|tool| build_tool_metadata(tool, detailed))
259        .collect();
260
261    IntrospectionResult { server, tools }
262}
263
264/// Builds tool metadata from tool info.
265///
266/// Transforms `ToolInfo` into `ToolMetadata` with optional schema details.
267///
268/// # Arguments
269///
270/// * `tool_info` - Tool information from introspector
271/// * `detailed` - Whether to include input/output schemas
272fn build_tool_metadata(tool_info: &ToolInfo, detailed: bool) -> ToolMetadata {
273    ToolMetadata {
274        name: tool_info.name.as_str().to_string(),
275        description: tool_info.description.clone(),
276        input_schema: if detailed {
277            Some(tool_info.input_schema.clone())
278        } else {
279            None
280        },
281        output_schema: if detailed {
282            tool_info.output_schema.clone()
283        } else {
284            None
285        },
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use mcp_execution_core::{ServerId, ToolName};
293    use mcp_execution_introspector::ServerCapabilities;
294    use serde_json::json;
295
296    #[test]
297    fn test_build_result_basic() {
298        let server_info = ServerInfo {
299            id: ServerId::new("test-server"),
300            name: "Test Server".to_string(),
301            version: "1.0.0".to_string(),
302            tools: vec![],
303            capabilities: ServerCapabilities {
304                supports_tools: true,
305                supports_resources: false,
306                supports_prompts: false,
307            },
308        };
309
310        let result = build_result(&server_info, false);
311
312        assert_eq!(result.server.id, "test-server");
313        assert_eq!(result.server.name, "Test Server");
314        assert_eq!(result.server.version, "1.0.0");
315        assert!(result.server.supports_tools);
316        assert!(!result.server.supports_resources);
317        assert!(!result.server.supports_prompts);
318        assert_eq!(result.tools.len(), 0);
319    }
320
321    #[test]
322    fn test_build_result_with_tools_not_detailed() {
323        let server_info = ServerInfo {
324            id: ServerId::new("test"),
325            name: "Test".to_string(),
326            version: "1.0.0".to_string(),
327            tools: vec![
328                ToolInfo {
329                    name: ToolName::new("tool1"),
330                    description: "First tool".to_string(),
331                    input_schema: json!({"type": "object"}),
332                    output_schema: None,
333                },
334                ToolInfo {
335                    name: ToolName::new("tool2"),
336                    description: "Second tool".to_string(),
337                    input_schema: json!({"type": "string"}),
338                    output_schema: Some(json!({"type": "boolean"})),
339                },
340            ],
341            capabilities: ServerCapabilities {
342                supports_tools: true,
343                supports_resources: true,
344                supports_prompts: true,
345            },
346        };
347
348        let result = build_result(&server_info, false);
349
350        assert_eq!(result.tools.len(), 2);
351        assert_eq!(result.tools[0].name, "tool1");
352        assert_eq!(result.tools[0].description, "First tool");
353        assert!(result.tools[0].input_schema.is_none());
354        assert!(result.tools[0].output_schema.is_none());
355
356        assert_eq!(result.tools[1].name, "tool2");
357        assert_eq!(result.tools[1].description, "Second tool");
358        assert!(result.tools[1].input_schema.is_none());
359        assert!(result.tools[1].output_schema.is_none());
360    }
361
362    #[test]
363    fn test_build_result_with_tools_detailed() {
364        let server_info = ServerInfo {
365            id: ServerId::new("test"),
366            name: "Test".to_string(),
367            version: "1.0.0".to_string(),
368            tools: vec![
369                ToolInfo {
370                    name: ToolName::new("tool1"),
371                    description: "First tool".to_string(),
372                    input_schema: json!({"type": "object", "properties": {"name": {"type": "string"}}}),
373                    output_schema: None,
374                },
375                ToolInfo {
376                    name: ToolName::new("tool2"),
377                    description: "Second tool".to_string(),
378                    input_schema: json!({"type": "string"}),
379                    output_schema: Some(json!({"type": "boolean"})),
380                },
381            ],
382            capabilities: ServerCapabilities {
383                supports_tools: true,
384                supports_resources: false,
385                supports_prompts: false,
386            },
387        };
388
389        let result = build_result(&server_info, true);
390
391        assert_eq!(result.tools.len(), 2);
392
393        // First tool - has input schema but no output schema
394        assert_eq!(result.tools[0].name, "tool1");
395        assert!(result.tools[0].input_schema.is_some());
396        assert_eq!(
397            result.tools[0].input_schema.as_ref().unwrap()["type"],
398            "object"
399        );
400        assert!(result.tools[0].output_schema.is_none());
401
402        // Second tool - has both input and output schemas
403        assert_eq!(result.tools[1].name, "tool2");
404        assert!(result.tools[1].input_schema.is_some());
405        assert_eq!(
406            result.tools[1].input_schema.as_ref().unwrap()["type"],
407            "string"
408        );
409        assert!(result.tools[1].output_schema.is_some());
410        assert_eq!(
411            result.tools[1].output_schema.as_ref().unwrap()["type"],
412            "boolean"
413        );
414    }
415
416    #[test]
417    fn test_build_tool_metadata_not_detailed() {
418        let tool_info = ToolInfo {
419            name: ToolName::new("send_message"),
420            description: "Sends a message".to_string(),
421            input_schema: json!({"type": "object"}),
422            output_schema: Some(json!({"type": "string"})),
423        };
424
425        let metadata = build_tool_metadata(&tool_info, false);
426
427        assert_eq!(metadata.name, "send_message");
428        assert_eq!(metadata.description, "Sends a message");
429        assert!(metadata.input_schema.is_none());
430        assert!(metadata.output_schema.is_none());
431    }
432
433    #[test]
434    fn test_build_tool_metadata_detailed() {
435        let tool_info = ToolInfo {
436            name: ToolName::new("send_message"),
437            description: "Sends a message".to_string(),
438            input_schema: json!({
439                "type": "object",
440                "properties": {
441                    "chat_id": {"type": "string"},
442                    "text": {"type": "string"}
443                }
444            }),
445            output_schema: Some(json!({"type": "string"})),
446        };
447
448        let metadata = build_tool_metadata(&tool_info, true);
449
450        assert_eq!(metadata.name, "send_message");
451        assert_eq!(metadata.description, "Sends a message");
452        assert!(metadata.input_schema.is_some());
453        assert_eq!(metadata.input_schema.as_ref().unwrap()["type"], "object");
454        assert!(metadata.output_schema.is_some());
455        assert_eq!(metadata.output_schema.as_ref().unwrap()["type"], "string");
456    }
457
458    #[test]
459    fn test_introspection_result_serialization() {
460        let result = IntrospectionResult {
461            server: ServerMetadata {
462                id: "test".to_string(),
463                name: "Test Server".to_string(),
464                version: "1.0.0".to_string(),
465                supports_tools: true,
466                supports_resources: false,
467                supports_prompts: false,
468            },
469            tools: vec![ToolMetadata {
470                name: "test_tool".to_string(),
471                description: "A test tool".to_string(),
472                input_schema: None,
473                output_schema: None,
474            }],
475        };
476
477        let json = serde_json::to_string(&result).unwrap();
478        assert!(json.contains("Test Server"));
479        assert!(json.contains("test_tool"));
480
481        // Schemas should not be in JSON when None
482        assert!(!json.contains("input_schema"));
483        assert!(!json.contains("output_schema"));
484    }
485
486    #[test]
487    fn test_introspection_result_serialization_with_schemas() {
488        let result = IntrospectionResult {
489            server: ServerMetadata {
490                id: "test".to_string(),
491                name: "Test Server".to_string(),
492                version: "1.0.0".to_string(),
493                supports_tools: true,
494                supports_resources: false,
495                supports_prompts: false,
496            },
497            tools: vec![ToolMetadata {
498                name: "test_tool".to_string(),
499                description: "A test tool".to_string(),
500                input_schema: Some(json!({"type": "object"})),
501                output_schema: Some(json!({"type": "string"})),
502            }],
503        };
504
505        let json = serde_json::to_string(&result).unwrap();
506        assert!(json.contains("input_schema"));
507        assert!(json.contains("output_schema"));
508        assert!(json.contains("\"type\":\"object\""));
509        assert!(json.contains("\"type\":\"string\""));
510    }
511
512    #[tokio::test]
513    async fn test_run_server_connection_failure() {
514        let result = run(
515            None,
516            Some("nonexistent-server-xyz".to_string()),
517            vec![],
518            vec![],
519            None,
520            None,
521            None,
522            vec![],
523            false,
524            OutputFormat::Json,
525        )
526        .await;
527
528        assert!(result.is_err());
529        let err_msg = result.unwrap_err().to_string();
530        assert!(err_msg.contains("failed to connect to server"));
531    }
532
533    // Note: build_server_config tests are in common.rs
534
535    #[test]
536    fn test_server_metadata_all_capabilities() {
537        let metadata = ServerMetadata {
538            id: "test".to_string(),
539            name: "Test".to_string(),
540            version: "2.0.0".to_string(),
541            supports_tools: true,
542            supports_resources: true,
543            supports_prompts: true,
544        };
545
546        assert!(metadata.supports_tools);
547        assert!(metadata.supports_resources);
548        assert!(metadata.supports_prompts);
549    }
550
551    #[test]
552    fn test_server_metadata_no_capabilities() {
553        let metadata = ServerMetadata {
554            id: "test".to_string(),
555            name: "Test".to_string(),
556            version: "1.0.0".to_string(),
557            supports_tools: false,
558            supports_resources: false,
559            supports_prompts: false,
560        };
561
562        assert!(!metadata.supports_tools);
563        assert!(!metadata.supports_resources);
564        assert!(!metadata.supports_prompts);
565    }
566
567    #[test]
568    fn test_tool_metadata_empty_description() {
569        let metadata = ToolMetadata {
570            name: "tool".to_string(),
571            description: String::new(),
572            input_schema: None,
573            output_schema: None,
574        };
575
576        assert_eq!(metadata.description, "");
577    }
578
579    #[test]
580    fn test_build_result_preserves_tool_order() {
581        let server_info = ServerInfo {
582            id: ServerId::new("test"),
583            name: "Test".to_string(),
584            version: "1.0.0".to_string(),
585            tools: vec![
586                ToolInfo {
587                    name: ToolName::new("alpha"),
588                    description: "A".to_string(),
589                    input_schema: json!({}),
590                    output_schema: None,
591                },
592                ToolInfo {
593                    name: ToolName::new("beta"),
594                    description: "B".to_string(),
595                    input_schema: json!({}),
596                    output_schema: None,
597                },
598                ToolInfo {
599                    name: ToolName::new("gamma"),
600                    description: "C".to_string(),
601                    input_schema: json!({}),
602                    output_schema: None,
603                },
604            ],
605            capabilities: ServerCapabilities {
606                supports_tools: true,
607                supports_resources: false,
608                supports_prompts: false,
609            },
610        };
611
612        let result = build_result(&server_info, false);
613
614        assert_eq!(result.tools.len(), 3);
615        assert_eq!(result.tools[0].name, "alpha");
616        assert_eq!(result.tools[1].name, "beta");
617        assert_eq!(result.tools[2].name, "gamma");
618    }
619
620    #[tokio::test]
621    async fn test_run_with_text_format() {
622        // Test that Text format output works correctly (compact JSON)
623        let result = run(
624            None,
625            Some("nonexistent-server".to_string()),
626            vec![],
627            vec![],
628            None,
629            None,
630            None,
631            vec![],
632            false,
633            OutputFormat::Text,
634        )
635        .await;
636
637        // Connection should fail but format handling should not panic
638        assert!(result.is_err());
639    }
640
641    #[tokio::test]
642    async fn test_run_with_pretty_format() {
643        // Test that Pretty format output works correctly (colorized)
644        let result = run(
645            None,
646            Some("nonexistent-server".to_string()),
647            vec![],
648            vec![],
649            None,
650            None,
651            None,
652            vec![],
653            false,
654            OutputFormat::Pretty,
655        )
656        .await;
657
658        // Connection should fail but format handling should not panic
659        assert!(result.is_err());
660    }
661
662    #[tokio::test]
663    async fn test_run_with_detailed_mode() {
664        // Test that detailed mode doesn't cause crashes even with connection failure
665        let result = run(
666            None,
667            Some("nonexistent-server".to_string()),
668            vec![],
669            vec![],
670            None,
671            None,
672            None,
673            vec![],
674            true, // detailed mode
675            OutputFormat::Json,
676        )
677        .await;
678
679        assert!(result.is_err());
680    }
681
682    #[tokio::test]
683    async fn test_run_http_transport() {
684        // Test HTTP transport with invalid URL
685        let result = run(
686            None,
687            None,
688            vec![],
689            vec![],
690            None,
691            Some("https://localhost:99999/invalid".to_string()),
692            None,
693            vec!["Authorization=Bearer test".to_string()],
694            false,
695            OutputFormat::Json,
696        )
697        .await;
698
699        assert!(result.is_err());
700        let err_msg = result.unwrap_err().to_string();
701        assert!(err_msg.contains("failed to connect to server"));
702    }
703
704    #[tokio::test]
705    async fn test_run_sse_transport() {
706        // Test SSE transport with invalid URL
707        let result = run(
708            None,
709            None,
710            vec![],
711            vec![],
712            None,
713            None,
714            Some("https://localhost:99999/sse".to_string()),
715            vec!["X-API-Key=test-key".to_string()],
716            false,
717            OutputFormat::Json,
718        )
719        .await;
720
721        assert!(result.is_err());
722        let err_msg = result.unwrap_err().to_string();
723        assert!(err_msg.contains("failed to connect to server"));
724    }
725
726    #[tokio::test]
727    async fn test_run_all_output_formats() {
728        // Test all output formats don't cause panics
729        for format in [OutputFormat::Json, OutputFormat::Text, OutputFormat::Pretty] {
730            let result = run(
731                None,
732                Some("nonexistent".to_string()),
733                vec![],
734                vec![],
735                None,
736                None,
737                None,
738                vec![],
739                false,
740                format,
741            )
742            .await;
743
744            assert!(result.is_err());
745        }
746    }
747
748    #[tokio::test]
749    async fn test_run_detailed_with_all_formats() {
750        // Test detailed mode with all output formats
751        for format in [OutputFormat::Json, OutputFormat::Text, OutputFormat::Pretty] {
752            let result = run(
753                None,
754                Some("nonexistent".to_string()),
755                vec![],
756                vec![],
757                None,
758                None,
759                None,
760                vec![],
761                true, // detailed
762                format,
763            )
764            .await;
765
766            assert!(result.is_err());
767        }
768    }
769
770    #[test]
771    fn test_build_result_empty_tools() {
772        let server_info = ServerInfo {
773            id: ServerId::new("empty"),
774            name: "Empty Server".to_string(),
775            version: "0.1.0".to_string(),
776            tools: vec![],
777            capabilities: ServerCapabilities {
778                supports_tools: false,
779                supports_resources: false,
780                supports_prompts: false,
781            },
782        };
783
784        let result = build_result(&server_info, false);
785
786        assert_eq!(result.server.name, "Empty Server");
787        assert_eq!(result.tools.len(), 0);
788        assert!(!result.server.supports_tools);
789    }
790
791    #[test]
792    fn test_build_result_many_tools() {
793        // Test with many tools to ensure no performance issues
794        let tools: Vec<ToolInfo> = (0..100)
795            .map(|i| ToolInfo {
796                name: ToolName::new(&format!("tool_{i}")),
797                description: format!("Tool number {i}"),
798                input_schema: json!({"type": "object"}),
799                output_schema: Some(json!({"type": "string"})),
800            })
801            .collect();
802
803        let server_info = ServerInfo {
804            id: ServerId::new("many-tools"),
805            name: "Server with many tools".to_string(),
806            version: "1.0.0".to_string(),
807            tools,
808            capabilities: ServerCapabilities {
809                supports_tools: true,
810                supports_resources: true,
811                supports_prompts: true,
812            },
813        };
814
815        let result = build_result(&server_info, true);
816
817        assert_eq!(result.tools.len(), 100);
818        assert_eq!(result.tools[0].name, "tool_0");
819        assert_eq!(result.tools[99].name, "tool_99");
820        // In detailed mode, schemas should be present
821        assert!(result.tools[0].input_schema.is_some());
822        assert!(result.tools[0].output_schema.is_some());
823    }
824
825    #[test]
826    fn test_build_tool_metadata_complex_schema() {
827        let tool_info = ToolInfo {
828            name: ToolName::new("complex_tool"),
829            description: "Tool with complex schema".to_string(),
830            input_schema: json!({
831                "type": "object",
832                "properties": {
833                    "name": {"type": "string", "minLength": 1},
834                    "age": {"type": "integer", "minimum": 0},
835                    "tags": {
836                        "type": "array",
837                        "items": {"type": "string"}
838                    }
839                },
840                "required": ["name"]
841            }),
842            output_schema: Some(json!({
843                "type": "object",
844                "properties": {
845                    "success": {"type": "boolean"},
846                    "message": {"type": "string"}
847                }
848            })),
849        };
850
851        let metadata = build_tool_metadata(&tool_info, true);
852
853        assert_eq!(metadata.name, "complex_tool");
854        assert!(metadata.input_schema.is_some());
855        assert!(metadata.output_schema.is_some());
856
857        let input = metadata.input_schema.as_ref().unwrap();
858        assert_eq!(input["type"], "object");
859        assert!(input["properties"]["name"].is_object());
860        assert!(input["properties"]["tags"]["items"].is_object());
861    }
862
863    #[test]
864    fn test_introspection_result_clone() {
865        let result = IntrospectionResult {
866            server: ServerMetadata {
867                id: "test".to_string(),
868                name: "Test".to_string(),
869                version: "1.0.0".to_string(),
870                supports_tools: true,
871                supports_resources: false,
872                supports_prompts: false,
873            },
874            tools: vec![],
875        };
876
877        // Test Clone implementation
878        let cloned = result.clone();
879        assert_eq!(cloned.server.id, result.server.id);
880        assert_eq!(cloned.server.name, result.server.name);
881    }
882
883    #[test]
884    fn test_server_metadata_serialization_all_fields() {
885        let metadata = ServerMetadata {
886            id: "test-id".to_string(),
887            name: "Test Server".to_string(),
888            version: "2.1.0".to_string(),
889            supports_tools: true,
890            supports_resources: true,
891            supports_prompts: true,
892        };
893
894        let json = serde_json::to_value(&metadata).unwrap();
895
896        assert_eq!(json["id"], "test-id");
897        assert_eq!(json["name"], "Test Server");
898        assert_eq!(json["version"], "2.1.0");
899        assert_eq!(json["supports_tools"], true);
900        assert_eq!(json["supports_resources"], true);
901        assert_eq!(json["supports_prompts"], true);
902    }
903
904    #[test]
905    fn test_tool_metadata_serialization_without_schemas() {
906        let metadata = ToolMetadata {
907            name: "simple_tool".to_string(),
908            description: "A simple tool".to_string(),
909            input_schema: None,
910            output_schema: None,
911        };
912
913        let json = serde_json::to_string(&metadata).unwrap();
914
915        // Fields with None should not be serialized (skip_serializing_if)
916        assert!(!json.contains("input_schema"));
917        assert!(!json.contains("output_schema"));
918        assert!(json.contains("simple_tool"));
919        assert!(json.contains("A simple tool"));
920    }
921
922    #[test]
923    fn test_tool_metadata_long_description() {
924        let long_description = "A".repeat(1000);
925        let metadata = ToolMetadata {
926            name: "tool".to_string(),
927            description: long_description.clone(),
928            input_schema: None,
929            output_schema: None,
930        };
931
932        // Should handle long descriptions without issues
933        assert_eq!(metadata.description.len(), 1000);
934        let json = serde_json::to_string(&metadata).unwrap();
935        assert!(json.contains(&long_description));
936    }
937
938    #[test]
939    fn test_build_result_mixed_capabilities() {
940        let server_info = ServerInfo {
941            id: ServerId::new("mixed"),
942            name: "Mixed Server".to_string(),
943            version: "1.0.0".to_string(),
944            tools: vec![ToolInfo {
945                name: ToolName::new("tool1"),
946                description: "First".to_string(),
947                input_schema: json!({}),
948                output_schema: None,
949            }],
950            capabilities: ServerCapabilities {
951                supports_tools: true,
952                supports_resources: true,
953                supports_prompts: false, // Mixed capabilities
954            },
955        };
956
957        let result = build_result(&server_info, false);
958
959        assert!(result.server.supports_tools);
960        assert!(result.server.supports_resources);
961        assert!(!result.server.supports_prompts);
962    }
963
964    #[tokio::test]
965    async fn test_run_from_config_not_found() {
966        let result = run(
967            Some("nonexistent-server-xyz".to_string()),
968            None,
969            vec![],
970            vec![],
971            None,
972            None,
973            None,
974            vec![],
975            false,
976            OutputFormat::Json,
977        )
978        .await;
979
980        assert!(result.is_err());
981        let err_msg = result.unwrap_err().to_string();
982        assert!(
983            err_msg.contains("not found in MCP config")
984                || err_msg.contains("failed to read MCP config"),
985            "Expected config-related error, got: {err_msg}"
986        );
987    }
988
989    #[tokio::test]
990    async fn test_run_from_config_takes_priority() {
991        // When from_config is Some, it should be used for config loading
992        // (server arg should be None due to clap conflicts, but we test the logic)
993        let result = run(
994            Some("test-server".to_string()),
995            None, // server is None when from_config is used
996            vec![],
997            vec![],
998            None,
999            None,
1000            None,
1001            vec![],
1002            false,
1003            OutputFormat::Json,
1004        )
1005        .await;
1006
1007        // Should fail because config doesn't exist, not because of server
1008        assert!(result.is_err());
1009        let err_msg = result.unwrap_err().to_string();
1010        // Should try to load from config, not use manual server
1011        assert!(
1012            err_msg.contains("MCP config") || err_msg.contains("test-server"),
1013            "Should attempt config loading: {err_msg}"
1014        );
1015    }
1016
1017    #[tokio::test]
1018    async fn test_run_manual_mode_backward_compatible() {
1019        // Existing behavior: from_config = None, use server arg
1020        let result = run(
1021            None, // from_config
1022            Some("test-server-direct".to_string()),
1023            vec![],
1024            vec![],
1025            None,
1026            None,
1027            None,
1028            vec![],
1029            false,
1030            OutputFormat::Json,
1031        )
1032        .await;
1033
1034        assert!(result.is_err());
1035        let err_msg = result.unwrap_err().to_string();
1036        // Should fail with connection error, not config error
1037        assert!(
1038            err_msg.contains("failed to connect") || err_msg.contains("test-server-direct"),
1039            "Should try direct connection: {err_msg}"
1040        );
1041    }
1042}