1use 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#[derive(Debug, Clone, Serialize)]
37pub struct IntrospectionResult {
38 pub server: ServerMetadata,
40 pub tools: Vec<ToolMetadata>,
42}
43
44#[derive(Debug, Clone, Serialize)]
49pub struct ServerMetadata {
50 pub id: String,
52 pub name: String,
54 pub version: String,
56 pub supports_tools: bool,
58 pub supports_resources: bool,
60 pub supports_prompts: bool,
62}
63
64#[derive(Debug, Clone, Serialize)]
69pub struct ToolMetadata {
70 pub name: String,
72 pub description: String,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub input_schema: Option<serde_json::Value>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub output_schema: Option<serde_json::Value>,
80}
81
82#[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 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 let mut introspector = Introspector::new();
184
185 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 let result = build_result(&server_info, detailed);
202
203 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#[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
264fn 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 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 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 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 #[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 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 assert!(result.is_err());
639 }
640
641 #[tokio::test]
642 async fn test_run_with_pretty_format() {
643 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 assert!(result.is_err());
660 }
661
662 #[tokio::test]
663 async fn test_run_with_detailed_mode() {
664 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, OutputFormat::Json,
676 )
677 .await;
678
679 assert!(result.is_err());
680 }
681
682 #[tokio::test]
683 async fn test_run_http_transport() {
684 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 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 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 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, 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 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 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 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 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 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, },
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 let result = run(
994 Some("test-server".to_string()),
995 None, vec![],
997 vec![],
998 None,
999 None,
1000 None,
1001 vec![],
1002 false,
1003 OutputFormat::Json,
1004 )
1005 .await;
1006
1007 assert!(result.is_err());
1009 let err_msg = result.unwrap_err().to_string();
1010 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 let result = run(
1021 None, 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 assert!(
1038 err_msg.contains("failed to connect") || err_msg.contains("test-server-direct"),
1039 "Should try direct connection: {err_msg}"
1040 );
1041 }
1042}