1use clap::builder::{PossibleValuesParser, TypedValueParser as _};
8use clap::{ArgGroup, Args, Parser, Subcommand};
9use clap_complete::Shell;
10use std::fmt;
11use std::path::{Path, PathBuf};
12use std::str::FromStr;
13
14use crate::actions::ServerAction;
15use crate::commands::common::{ServerSource, TransportArgs};
16use mcp_execution_core::cli::OutputFormat;
17use mcp_execution_core::{Error as CoreError, RedactedItems, RedactedUrl, sanitize_path_for_error};
18
19#[derive(Parser)]
36#[command(version, about, long_about = None)]
37#[command(author = "MCP Execution Team")]
38pub struct Cli {
39 #[command(subcommand)]
41 pub command: Commands,
42
43 #[arg(short, long, global = true)]
45 pub verbose: bool,
46
47 #[arg(
49 long = "format",
50 global = true,
51 default_value = "pretty",
52 ignore_case = true,
53 value_parser = PossibleValuesParser::new(["json", "text", "pretty"])
54 .map(|s| OutputFormat::from_str(&s).expect("possible values are OutputFormat variants"))
55 )]
56 pub format: OutputFormat,
57}
58
59impl fmt::Debug for Cli {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 let Self {
73 command,
74 verbose,
75 format,
76 } = self;
77 f.debug_struct("Cli")
78 .field("command", command)
79 .field("verbose", verbose)
80 .field("format", format)
81 .finish()
82 }
83}
84
85#[derive(Args)]
117#[command(group(
118 ArgGroup::new("server_source")
119 .required(true)
120 .args(["from_config", "server", "http", "sse"])
121))]
122pub struct ServerFlags {
123 #[arg(long = "from-config", conflicts_with_all = ["server", "args", "env", "cwd", "http", "sse", "connect_timeout_secs", "discover_timeout_secs"])]
146 from_config: Option<String>,
147
148 server: Option<String>,
153
154 #[arg(short, long = "arg", num_args = 1)]
156 args: Vec<String>,
157
158 #[arg(short, long = "env", num_args = 1)]
160 env: Vec<String>,
161
162 #[arg(long)]
164 cwd: Option<String>,
165
166 #[arg(long, conflicts_with = "sse")]
168 http: Option<String>,
169
170 #[arg(long, conflicts_with = "http")]
172 sse: Option<String>,
173
174 #[arg(long = "header", num_args = 1)]
176 headers: Vec<String>,
177
178 #[arg(long = "connect-timeout-secs")]
190 connect_timeout_secs: Option<u64>,
191
192 #[arg(long = "discover-timeout-secs")]
197 discover_timeout_secs: Option<u64>,
198}
199
200impl fmt::Debug for ServerFlags {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 let Self {
220 from_config,
221 server,
222 args,
223 env,
224 cwd,
225 http,
226 sse,
227 headers,
228 connect_timeout_secs,
229 discover_timeout_secs,
230 } = self;
231 f.debug_struct("ServerFlags")
232 .field("from_config", from_config)
233 .field(
234 "server",
235 &server
236 .as_deref()
237 .map(|s| sanitize_path_for_error(Path::new(s))),
238 )
239 .field("args", args)
240 .field("env", &RedactedItems(env))
241 .field(
242 "cwd",
243 &cwd.as_deref()
244 .map(|cwd| sanitize_path_for_error(Path::new(cwd))),
245 )
246 .field("http", &http.as_deref().map(RedactedUrl))
247 .field("sse", &sse.as_deref().map(RedactedUrl))
248 .field("headers", &RedactedItems(headers))
249 .field("connect_timeout_secs", connect_timeout_secs)
250 .field("discover_timeout_secs", discover_timeout_secs)
251 .finish()
252 }
253}
254
255impl TryFrom<ServerFlags> for ServerSource {
267 type Error = CoreError;
268
269 fn try_from(flags: ServerFlags) -> Result<Self, Self::Error> {
270 let ServerFlags {
271 from_config,
272 server,
273 args,
274 env,
275 cwd,
276 http,
277 sse,
278 headers,
279 connect_timeout_secs,
280 discover_timeout_secs,
281 } = flags;
282
283 match (from_config, server, http, sse) {
284 (Some(name), None, None, None) => Ok(Self::Config { name }),
285 (None, Some(command), None, None) => Ok(Self::Flags {
286 transport: TransportArgs::Stdio {
287 command,
288 args,
289 env,
290 cwd,
291 },
292 connect_timeout_secs,
293 discover_timeout_secs,
294 }),
295 (None, None, Some(url), None) => Ok(Self::Flags {
296 transport: TransportArgs::Http { url, headers },
297 connect_timeout_secs,
298 discover_timeout_secs,
299 }),
300 (None, None, None, Some(url)) => Ok(Self::Flags {
301 transport: TransportArgs::Sse { url, headers },
302 connect_timeout_secs,
303 discover_timeout_secs,
304 }),
305 _ => Err(CoreError::InvalidArgument(
306 "exactly one of --from-config, a server command, --http, or --sse must be set"
307 .to_string(),
308 )),
309 }
310 }
311}
312
313#[derive(Subcommand)]
332pub enum Commands {
333 Introspect {
375 #[command(flatten)]
377 flags: ServerFlags,
378
379 #[arg(short, long)]
381 detailed: bool,
382 },
383
384 Skill {
413 #[arg(short, long)]
417 server: String,
418
419 #[arg(long)]
423 servers_dir: Option<PathBuf>,
424
425 #[arg(short, long)]
429 output: Option<PathBuf>,
430
431 #[arg(long)]
435 skill_name: Option<String>,
436
437 #[arg(long = "hint", num_args = 1)]
442 hints: Vec<String>,
443
444 #[arg(long)]
446 overwrite: bool,
447 },
448
449 Generate {
482 #[command(flatten)]
484 flags: ServerFlags,
485
486 #[arg(long)]
489 name: Option<String>,
490
491 #[arg(long)]
494 progressive_output: Option<PathBuf>,
495
496 #[arg(long)]
498 dry_run: bool,
499 },
500
501 Server {
505 #[command(subcommand)]
507 action: ServerAction,
508 },
509
510 Setup,
529
530 Completions {
535 #[arg(value_enum)]
537 shell: Shell,
538 },
539}
540
541impl fmt::Debug for Commands {
542 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543 match self {
544 Self::Introspect { flags, detailed } => f
545 .debug_struct("Introspect")
546 .field("flags", flags)
547 .field("detailed", detailed)
548 .finish(),
549 Self::Skill {
550 server,
551 servers_dir,
552 output,
553 skill_name,
554 hints,
555 overwrite,
556 } => f
557 .debug_struct("Skill")
558 .field("server", server)
559 .field("servers_dir", servers_dir)
560 .field("output", output)
561 .field("skill_name", skill_name)
562 .field("hints", hints)
563 .field("overwrite", overwrite)
564 .finish(),
565 Self::Generate {
566 flags,
567 name,
568 progressive_output,
569 dry_run,
570 } => f
571 .debug_struct("Generate")
572 .field("flags", flags)
573 .field("name", name)
574 .field("progressive_output", progressive_output)
575 .field("dry_run", dry_run)
576 .finish(),
577 Self::Server { action } => f.debug_struct("Server").field("action", action).finish(),
578 Self::Setup => write!(f, "Setup"),
579 Self::Completions { shell } => {
580 f.debug_struct("Completions").field("shell", shell).finish()
581 }
582 }
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589 use clap::CommandFactory;
590
591 #[test]
592 fn test_cli_help_examples_use_published_binary_name() {
593 let mut command = Cli::command();
594
595 for subcommand in ["introspect", "generate", "skill"] {
596 let help = command
597 .find_subcommand_mut(subcommand)
598 .expect("subcommand should exist")
599 .render_long_help()
600 .to_string();
601
602 assert!(
603 help.contains(&format!("mcp-execution-cli {subcommand}")),
604 "{subcommand} help should include examples with the published binary name"
605 );
606 assert!(
607 !help.contains(&format!("mcp-cli {subcommand}")),
608 "{subcommand} help should not reference the old binary name"
609 );
610 }
611 }
612
613 #[test]
614 fn test_cli_parsing_introspect() {
615 let cli = Cli::parse_from(["mcp-cli", "introspect", "github"]);
616 assert!(matches!(cli.command, Commands::Introspect { .. }));
617 }
618
619 #[test]
620 fn test_cli_parsing_introspect_with_args() {
621 let cli = Cli::parse_from([
622 "mcp-cli",
623 "introspect",
624 "docker",
625 "--arg=run",
626 "--arg=-i",
627 "--arg=--rm",
628 "--arg=ghcr.io/github/github-mcp-server",
629 "--env=GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx",
630 ]);
631 if let Commands::Introspect { flags, .. } = cli.command {
632 assert_eq!(flags.server, Some("docker".to_string()));
633 assert_eq!(
634 flags.args,
635 vec!["run", "-i", "--rm", "ghcr.io/github/github-mcp-server"]
636 );
637 assert_eq!(flags.env, vec!["GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx"]);
638 } else {
639 panic!("Expected Introspect command");
640 }
641 }
642
643 #[test]
644 fn test_cli_parsing_introspect_http() {
645 let cli = Cli::parse_from([
646 "mcp-cli",
647 "introspect",
648 "--http",
649 "https://api.githubcopilot.com/mcp/",
650 "--header",
651 "Authorization=Bearer token",
652 ]);
653 if let Commands::Introspect { flags, .. } = cli.command {
654 assert_eq!(flags.server, None);
655 assert_eq!(
656 flags.http,
657 Some("https://api.githubcopilot.com/mcp/".to_string())
658 );
659 assert_eq!(flags.headers, vec!["Authorization=Bearer token"]);
660 } else {
661 panic!("Expected Introspect command");
662 }
663 }
664
665 #[test]
666 fn test_cli_parsing_introspect_timeout_overrides() {
667 let cli = Cli::parse_from([
668 "mcp-cli",
669 "introspect",
670 "docker",
671 "--connect-timeout-secs",
672 "5",
673 "--discover-timeout-secs",
674 "90",
675 ]);
676 if let Commands::Introspect { flags, .. } = cli.command {
677 assert_eq!(flags.connect_timeout_secs, Some(5));
678 assert_eq!(flags.discover_timeout_secs, Some(90));
679 } else {
680 panic!("Expected Introspect command");
681 }
682 }
683
684 #[test]
685 fn test_cli_parsing_introspect_timeout_conflicts_with_from_config() {
686 let result = Cli::try_parse_from([
687 "mcp-cli",
688 "introspect",
689 "--from-config",
690 "github",
691 "--connect-timeout-secs",
692 "5",
693 ]);
694 assert!(result.is_err());
695 }
696
697 #[test]
698 fn test_cli_parsing_generate_timeout_conflicts_with_from_config() {
699 let result = Cli::try_parse_from([
700 "mcp-cli",
701 "generate",
702 "--from-config",
703 "github",
704 "--discover-timeout-secs",
705 "90",
706 ]);
707 assert!(result.is_err());
708 }
709
710 #[test]
711 fn test_cli_parsing_generate() {
712 let cli = Cli::parse_from(["mcp-cli", "generate", "server"]);
713 assert!(matches!(cli.command, Commands::Generate { .. }));
714
715 let cli = Cli::parse_from([
716 "mcp-cli",
717 "generate",
718 "server",
719 "--progressive-output",
720 "/tmp/output",
721 ]);
722 if let Commands::Generate {
723 progressive_output, ..
724 } = cli.command
725 {
726 assert_eq!(progressive_output, Some(PathBuf::from("/tmp/output")));
727 } else {
728 panic!("Expected Generate command");
729 }
730 }
731
732 #[test]
733 fn test_cli_parsing_generate_timeout_overrides() {
734 let cli = Cli::parse_from([
735 "mcp-cli",
736 "generate",
737 "docker",
738 "--connect-timeout-secs",
739 "5",
740 "--discover-timeout-secs",
741 "90",
742 ]);
743 if let Commands::Generate { flags, .. } = cli.command {
744 assert_eq!(flags.connect_timeout_secs, Some(5));
745 assert_eq!(flags.discover_timeout_secs, Some(90));
746 } else {
747 panic!("Expected Generate command");
748 }
749 }
750
751 #[test]
752 fn test_cli_parsing_generate_dry_run() {
753 let cli = Cli::parse_from(["mcp-cli", "generate", "server", "--dry-run"]);
754 if let Commands::Generate { dry_run, .. } = cli.command {
755 assert!(dry_run);
756 } else {
757 panic!("Expected Generate command");
758 }
759 }
760
761 #[test]
762 fn test_cli_parsing_generate_dry_run_default_false() {
763 let cli = Cli::parse_from(["mcp-cli", "generate", "server"]);
764 if let Commands::Generate { dry_run, .. } = cli.command {
765 assert!(!dry_run);
766 } else {
767 panic!("Expected Generate command");
768 }
769 }
770
771 #[test]
772 fn test_cli_parsing_server_list() {
773 let cli = Cli::parse_from(["mcp-cli", "server", "list"]);
774 assert!(matches!(cli.command, Commands::Server { .. }));
775 }
776
777 #[test]
778 fn test_cli_verbose_flag() {
779 let cli = Cli::parse_from(["mcp-cli", "--verbose", "introspect", "github"]);
780 assert!(cli.verbose);
781 }
782
783 #[test]
784 fn test_cli_output_format_default() {
785 let cli = Cli::parse_from(["mcp-cli", "introspect", "github"]);
786 assert_eq!(cli.format, OutputFormat::Pretty);
787 }
788
789 #[test]
790 fn test_cli_output_format_custom() {
791 let cli = Cli::parse_from(["mcp-cli", "--format", "json", "introspect", "github"]);
792 assert_eq!(cli.format, OutputFormat::Json);
793 }
794
795 #[test]
796 fn test_cli_output_format_invalid_rejected_by_clap() {
797 let result = Cli::try_parse_from(["mcp-cli", "--format", "xml", "introspect", "github"]);
798 assert!(result.is_err());
799 }
800
801 #[test]
802 fn test_cli_output_format_possible_values_parse_via_from_str() {
803 let cmd = Cli::command();
810 let arg = cmd
811 .get_arguments()
812 .find(|a| a.get_id() == "format")
813 .expect("--format argument must exist");
814 let values = arg.get_possible_values();
815 assert!(!values.is_empty(), "--format must declare possible values");
816 for possible_value in values {
817 let name = possible_value.get_name();
818 assert!(
819 OutputFormat::from_str(name).is_ok(),
820 "{name} must parse via OutputFormat::from_str to match the --format value parser"
821 );
822 }
823 }
824
825 #[test]
826 fn test_cli_output_format_case_insensitive() {
827 let cli = Cli::parse_from(["mcp-cli", "--format", "JSON", "introspect", "github"]);
828 assert_eq!(cli.format, OutputFormat::Json);
829
830 let cli = Cli::parse_from(["mcp-cli", "--format", "PRETTY", "introspect", "github"]);
831 assert_eq!(cli.format, OutputFormat::Pretty);
832 }
833
834 #[test]
835 fn test_output_format_parsing_valid() {
836 use mcp_execution_core::cli::OutputFormat;
837
838 let format: OutputFormat = "json".parse().unwrap();
839 assert_eq!(format, OutputFormat::Json);
840
841 let format: OutputFormat = "text".parse().unwrap();
842 assert_eq!(format, OutputFormat::Text);
843
844 let format: OutputFormat = "pretty".parse().unwrap();
845 assert_eq!(format, OutputFormat::Pretty);
846 }
847
848 #[test]
849 fn test_output_format_parsing_invalid() {
850 use mcp_execution_core::cli::OutputFormat;
851 assert!("invalid".parse::<OutputFormat>().is_err());
852 }
853
854 #[test]
855 fn test_cli_parsing_completions_bash() {
856 let cli = Cli::parse_from(["mcp-cli", "completions", "bash"]);
857 assert!(matches!(cli.command, Commands::Completions { .. }));
858 }
859
860 #[test]
861 fn test_cli_parsing_completions_zsh() {
862 let cli = Cli::parse_from(["mcp-cli", "completions", "zsh"]);
863 if let Commands::Completions { shell } = cli.command {
864 assert_eq!(shell, Shell::Zsh);
865 } else {
866 panic!("Expected Completions command");
867 }
868 }
869
870 #[test]
871 fn test_cli_parsing_skill_basic() {
872 let cli = Cli::parse_from(["mcp-cli", "skill", "--server", "github"]);
873 if let Commands::Skill {
874 server,
875 servers_dir,
876 output,
877 skill_name,
878 hints,
879 overwrite,
880 } = cli.command
881 {
882 assert_eq!(server, "github");
883 assert!(servers_dir.is_none());
884 assert!(output.is_none());
885 assert!(skill_name.is_none());
886 assert!(hints.is_empty());
887 assert!(!overwrite);
888 } else {
889 panic!("Expected Skill command");
890 }
891 }
892
893 #[test]
894 fn test_cli_parsing_skill_all_options() {
895 let cli = Cli::parse_from([
896 "mcp-cli",
897 "skill",
898 "--server",
899 "github",
900 "--servers-dir",
901 "/custom/servers",
902 "--output",
903 "/custom/skills/github.md",
904 "--skill-name",
905 "github-advanced",
906 "--hint",
907 "pull requests",
908 "--hint",
909 "code review",
910 "--overwrite",
911 ]);
912 if let Commands::Skill {
913 server,
914 servers_dir,
915 output,
916 skill_name,
917 hints,
918 overwrite,
919 } = cli.command
920 {
921 assert_eq!(server, "github");
922 assert_eq!(servers_dir, Some(PathBuf::from("/custom/servers")));
923 assert_eq!(output, Some(PathBuf::from("/custom/skills/github.md")));
924 assert_eq!(skill_name, Some("github-advanced".to_string()));
925 assert_eq!(
926 hints,
927 vec!["pull requests".to_string(), "code review".to_string()]
928 );
929 assert!(overwrite);
930 } else {
931 panic!("Expected Skill command");
932 }
933 }
934
935 #[test]
936 fn test_cli_parsing_skill_short_flags() {
937 let cli = Cli::parse_from(["mcp-cli", "skill", "-s", "github", "-o", "/tmp/skill.md"]);
938 if let Commands::Skill { server, output, .. } = cli.command {
939 assert_eq!(server, "github");
940 assert_eq!(output, Some(PathBuf::from("/tmp/skill.md")));
941 } else {
942 panic!("Expected Skill command");
943 }
944 }
945
946 #[test]
947 fn test_cli_parsing_skill_multiple_hints() {
948 let cli = Cli::parse_from([
949 "mcp-cli",
950 "skill",
951 "--server",
952 "github",
953 "--hint",
954 "managing pull requests",
955 "--hint",
956 "code review",
957 "--hint",
958 "CI/CD automation",
959 ]);
960 if let Commands::Skill { hints, .. } = cli.command {
961 assert_eq!(hints.len(), 3);
962 assert_eq!(hints[0], "managing pull requests");
963 assert_eq!(hints[1], "code review");
964 assert_eq!(hints[2], "CI/CD automation");
965 } else {
966 panic!("Expected Skill command");
967 }
968 }
969
970 #[test]
971 fn test_cli_parsing_skill_overwrite() {
972 let cli = Cli::parse_from(["mcp-cli", "skill", "--server", "test", "--overwrite"]);
973 if let Commands::Skill { overwrite, .. } = cli.command {
974 assert!(overwrite);
975 } else {
976 panic!("Expected Skill command");
977 }
978 }
979
980 #[test]
981 fn test_commands_debug_redacts_introspect_env_and_headers() {
982 let secret_body = "sk-verySECRETtoken1234567890";
983 let cli = Cli::parse_from([
984 "mcp-cli",
985 "introspect",
986 "docker",
987 "--env",
988 &format!("GITHUB_TOKEN={secret_body}"),
989 "--header",
990 &format!("Authorization=Bearer {secret_body}"),
991 ]);
992
993 let debug_output = format!("{:?}", cli.command);
994 assert!(debug_output.contains("<redacted>"));
995 assert!(!debug_output.contains(secret_body));
996 }
997
998 #[test]
999 fn test_commands_debug_redacts_generate_env_and_headers() {
1000 let secret_body = "sk-verySECRETtoken1234567890";
1001 let cli = Cli::parse_from([
1002 "mcp-cli",
1003 "generate",
1004 "docker",
1005 "--env",
1006 &format!("GITHUB_TOKEN={secret_body}"),
1007 "--header",
1008 &format!("Authorization=Bearer {secret_body}"),
1009 ]);
1010
1011 let debug_output = format!("{:?}", cli.command);
1012 assert!(debug_output.contains("<redacted>"));
1013 assert!(!debug_output.contains(secret_body));
1014 }
1015
1016 #[test]
1017 fn test_commands_debug_does_not_redact_args() {
1018 let cli = Cli::parse_from(["mcp-cli", "introspect", "docker", "--arg=stdio"]);
1021 let debug_output = format!("{:?}", cli.command);
1022 assert!(debug_output.contains("stdio"));
1023 }
1024
1025 #[test]
1026 fn test_commands_debug_redacts_introspect_http_url() {
1027 let secret = "sk-verySECRETtoken1234567890";
1028 let cli = Cli::parse_from([
1029 "mcp-cli",
1030 "introspect",
1031 "--http",
1032 &format!("https://user:{secret}@host.example.com/mcp?token={secret}"),
1033 ]);
1034
1035 let debug_output = format!("{:?}", cli.command);
1036 assert!(!debug_output.contains(secret));
1037 assert!(debug_output.contains("host.example.com/mcp"));
1038 }
1039
1040 #[test]
1041 fn test_commands_debug_redacts_introspect_sse_url() {
1042 let secret = "sk-verySECRETtoken1234567890";
1043 let cli = Cli::parse_from([
1044 "mcp-cli",
1045 "introspect",
1046 "--sse",
1047 &format!("https://user:{secret}@host.example.com/mcp?token={secret}"),
1048 ]);
1049
1050 let debug_output = format!("{:?}", cli.command);
1051 assert!(!debug_output.contains(secret));
1052 assert!(debug_output.contains("host.example.com/mcp"));
1053 }
1054
1055 #[test]
1056 fn test_commands_debug_redacts_generate_http_url() {
1057 let secret = "sk-verySECRETtoken1234567890";
1058 let cli = Cli::parse_from([
1059 "mcp-cli",
1060 "generate",
1061 "--http",
1062 &format!("https://user:{secret}@host.example.com/mcp?token={secret}"),
1063 ]);
1064
1065 let debug_output = format!("{:?}", cli.command);
1066 assert!(!debug_output.contains(secret));
1067 assert!(debug_output.contains("host.example.com/mcp"));
1068 }
1069
1070 #[test]
1071 fn test_commands_debug_redacts_generate_sse_url() {
1072 let secret = "sk-verySECRETtoken1234567890";
1073 let cli = Cli::parse_from([
1074 "mcp-cli",
1075 "generate",
1076 "--sse",
1077 &format!("https://user:{secret}@host.example.com/mcp?token={secret}"),
1078 ]);
1079
1080 let debug_output = format!("{:?}", cli.command);
1081 assert!(!debug_output.contains(secret));
1082 assert!(debug_output.contains("host.example.com/mcp"));
1083 }
1084
1085 #[test]
1086 fn test_server_flags_debug_redacts_secret_shaped_fields() {
1087 let secret = "sk-live-secret";
1091 let home = dirs::home_dir().expect("home directory must be resolvable in test environment");
1092 let server_path = home.join("tools").join("mcp-server");
1093
1094 let cli = Cli::parse_from([
1095 "mcp-cli",
1096 "introspect",
1097 &server_path.display().to_string(),
1098 "--env",
1099 &format!("GITHUB_TOKEN={secret}"),
1100 "--connect-timeout-secs",
1101 "30",
1102 ]);
1103
1104 let debug_output = format!("{:?}", cli.command);
1105 assert!(!debug_output.contains(secret));
1106 assert!(!debug_output.contains(&home.display().to_string()));
1107 assert!(debug_output.contains('~'));
1108 assert!(debug_output.contains("connect_timeout_secs: Some(30)"));
1109 }
1110
1111 #[test]
1115 fn test_cli_parsing_introspect_positional_with_http_errors() {
1116 let result = Cli::try_parse_from([
1120 "mcp-cli",
1121 "introspect",
1122 "docker",
1123 "--http",
1124 "https://api.example.com",
1125 ]);
1126 assert!(result.is_err());
1127 }
1128
1129 #[test]
1130 fn test_cli_parsing_generate_positional_with_http_errors() {
1131 let result = Cli::try_parse_from([
1132 "mcp-cli",
1133 "generate",
1134 "docker",
1135 "--http",
1136 "https://api.example.com",
1137 ]);
1138 assert!(result.is_err());
1139 }
1140
1141 #[test]
1142 fn test_cli_parsing_introspect_no_selector_errors() {
1143 let result = Cli::try_parse_from(["mcp-cli", "introspect"]);
1144 assert!(result.is_err());
1145 }
1146
1147 #[test]
1148 fn test_cli_parsing_generate_no_selector_errors() {
1149 let result = Cli::try_parse_from(["mcp-cli", "generate"]);
1150 assert!(result.is_err());
1151 }
1152
1153 #[test]
1154 fn test_cli_parsing_introspect_http_and_sse_together_errors() {
1155 let result = Cli::try_parse_from([
1156 "mcp-cli",
1157 "introspect",
1158 "--http",
1159 "https://api.example.com",
1160 "--sse",
1161 "https://api.example.com/sse",
1162 ]);
1163 assert!(result.is_err());
1164 }
1165
1166 #[test]
1167 fn test_cli_parsing_generate_http_and_sse_together_errors() {
1168 let result = Cli::try_parse_from([
1169 "mcp-cli",
1170 "generate",
1171 "--http",
1172 "https://api.example.com",
1173 "--sse",
1174 "https://api.example.com/sse",
1175 ]);
1176 assert!(result.is_err());
1177 }
1178
1179 #[test]
1180 fn test_cli_parsing_introspect_from_config_and_http_together_errors() {
1181 let result = Cli::try_parse_from([
1182 "mcp-cli",
1183 "introspect",
1184 "--from-config",
1185 "github",
1186 "--http",
1187 "https://api.example.com",
1188 ]);
1189 assert!(result.is_err());
1190 }
1191
1192 #[test]
1193 fn test_cli_parsing_generate_from_config_and_http_together_errors() {
1194 let result = Cli::try_parse_from([
1195 "mcp-cli",
1196 "generate",
1197 "--from-config",
1198 "github",
1199 "--http",
1200 "https://api.example.com",
1201 ]);
1202 assert!(result.is_err());
1203 }
1204
1205 #[test]
1206 fn test_server_source_try_from_server_flags_catch_all_errors() {
1207 let flags = ServerFlags {
1212 from_config: None,
1213 server: None,
1214 args: vec![],
1215 env: vec![],
1216 cwd: None,
1217 http: None,
1218 sse: None,
1219 headers: vec![],
1220 connect_timeout_secs: None,
1221 discover_timeout_secs: None,
1222 };
1223
1224 let result = ServerSource::try_from(flags);
1225 assert!(result.is_err());
1226 }
1227
1228 fn introspect_flags(cli: Cli) -> ServerFlags {
1236 match cli.command {
1237 Commands::Introspect { flags, .. } => flags,
1238 other => panic!("expected Introspect command, got {other:?}"),
1239 }
1240 }
1241
1242 #[test]
1243 fn test_server_source_try_from_config_arm() {
1244 let cli = Cli::parse_from(["mcp-cli", "introspect", "--from-config", "github"]);
1245 let source = ServerSource::try_from(introspect_flags(cli)).unwrap();
1246
1247 assert!(matches!(source, ServerSource::Config { name } if name == "github"));
1248 }
1249
1250 #[test]
1251 fn test_server_source_try_from_stdio_arm_does_not_transpose_args_and_env() {
1252 let cli = Cli::parse_from([
1253 "mcp-cli",
1254 "introspect",
1255 "docker",
1256 "--arg=run",
1257 "--env=TOKEN=abc",
1258 "--cwd=/tmp/work",
1259 ]);
1260 let source = ServerSource::try_from(introspect_flags(cli)).unwrap();
1261
1262 match source {
1263 ServerSource::Flags {
1264 transport:
1265 TransportArgs::Stdio {
1266 command,
1267 args,
1268 env,
1269 cwd,
1270 },
1271 ..
1272 } => {
1273 assert_eq!(command, "docker");
1274 assert_eq!(args, vec!["run".to_string()]);
1275 assert_eq!(env, vec!["TOKEN=abc".to_string()]);
1276 assert_eq!(cwd, Some("/tmp/work".to_string()));
1277 }
1278 other => panic!("expected Flags{{Stdio}}, got {other:?}"),
1279 }
1280 }
1281
1282 #[test]
1283 fn test_server_source_try_from_http_arm_does_not_swap_with_sse() {
1284 let cli = Cli::parse_from([
1285 "mcp-cli",
1286 "introspect",
1287 "--http",
1288 "https://api.example.com/mcp",
1289 "--header=Authorization=Bearer x",
1290 ]);
1291 let source = ServerSource::try_from(introspect_flags(cli)).unwrap();
1292
1293 match source {
1294 ServerSource::Flags {
1295 transport: TransportArgs::Http { url, headers },
1296 ..
1297 } => {
1298 assert_eq!(url, "https://api.example.com/mcp");
1299 assert_eq!(headers, vec!["Authorization=Bearer x".to_string()]);
1300 }
1301 other => panic!("expected Flags{{Http}}, got {other:?}"),
1302 }
1303 }
1304
1305 #[test]
1306 fn test_server_source_try_from_sse_arm_does_not_swap_with_http() {
1307 let cli = Cli::parse_from([
1308 "mcp-cli",
1309 "introspect",
1310 "--sse",
1311 "https://api.example.com/sse",
1312 "--header=X-API-Key=secret",
1313 ]);
1314 let source = ServerSource::try_from(introspect_flags(cli)).unwrap();
1315
1316 match source {
1317 ServerSource::Flags {
1318 transport: TransportArgs::Sse { url, headers },
1319 ..
1320 } => {
1321 assert_eq!(url, "https://api.example.com/sse");
1322 assert_eq!(headers, vec!["X-API-Key=secret".to_string()]);
1323 }
1324 other => panic!("expected Flags{{Sse}}, got {other:?}"),
1325 }
1326 }
1327}