use clap::{Parser, Subcommand, ValueEnum};
use mcp_probe_core::transport::TransportConfig;
use std::path::PathBuf;
use url::Url;
#[derive(Parser)]
#[command(
name = "mcp-probe",
version,
about = "A production-grade MCP client and debugger built in Rust",
long_about = "MCP Probe provides both a powerful SDK for building MCP clients and an intuitive debugging tool for validating MCP servers before deploying them to LLM hosts."
)]
pub struct Cli {
#[arg(short, long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long)]
pub no_color: bool,
#[arg(long, value_enum, default_value = "pretty")]
pub output: OutputFormat,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Debug(crate::commands::debug::DebugCommand),
Test(TestArgs),
Config(ConfigArgs),
Validate(ValidateArgs),
Export(ExportArgs),
Paths(PathsArgs),
}
#[derive(Parser)]
pub struct DebugArgs {
#[command(flatten)]
pub transport: TransportArgs,
#[arg(short, long)]
pub config: Option<PathBuf>,
#[arg(long)]
pub non_interactive: bool,
#[arg(long)]
pub show_raw: bool,
#[arg(long)]
pub save_session: Option<PathBuf>,
#[arg(long)]
pub replay_session: Option<PathBuf>,
#[arg(long, default_value = "30")]
pub timeout: u64,
#[arg(long, default_value = "3")]
pub max_retries: u32,
}
#[derive(Parser, Debug)]
pub struct TestArgs {
#[arg(short, long)]
pub suite: Option<String>,
#[command(flatten)]
pub transport: TransportArgs,
#[arg(short, long)]
pub config: Option<PathBuf>,
#[arg(long)]
pub report: bool,
#[arg(long)]
pub output_dir: Option<PathBuf>,
#[arg(long)]
pub fail_fast: bool,
#[arg(long, default_value = "60")]
pub timeout: u64,
#[arg(long, value_name = "BASE_URL")]
pub discover: Option<String>,
}
#[derive(Parser, Debug)]
pub struct ConfigArgs {
#[command(subcommand)]
pub action: ConfigAction,
}
#[derive(Subcommand, Debug)]
pub enum ConfigAction {
Init {
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long, value_enum, default_value = "full")]
template: ConfigTemplate,
},
Validate {
config: PathBuf,
},
Show {
config: Option<PathBuf>,
},
}
#[derive(Parser, Debug)]
pub struct ValidateArgs {
#[command(flatten)]
pub transport: TransportArgs,
#[arg(short, long)]
pub config: Option<PathBuf>,
#[arg(long, value_delimiter = ',')]
pub rules: Vec<String>,
#[arg(long)]
pub report: Option<PathBuf>,
#[arg(long, value_enum, default_value = "error")]
pub severity: Severity,
}
#[derive(Parser, Debug)]
pub struct ExportArgs {
pub session: PathBuf,
#[arg(short, long, value_enum, default_value = "json")]
pub format: ExportFormat,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long)]
pub include_raw: bool,
#[arg(long)]
pub include_timing: bool,
}
#[derive(Parser, Debug)]
pub struct PathsArgs {
#[command(subcommand)]
pub action: PathsAction,
}
#[derive(Subcommand, Debug)]
pub enum PathsAction {
Show,
Cleanup {
#[arg(long, default_value = "30")]
days: u64,
#[arg(long)]
force: bool,
},
Open,
}
#[derive(Parser, Clone, Debug)]
pub struct TransportArgs {
#[arg(long, value_name = "COMMAND")]
pub stdio: Option<String>,
#[arg(long, requires = "stdio")]
pub args: Vec<String>,
#[arg(long, requires = "stdio")]
pub working_dir: Option<PathBuf>,
#[arg(long, value_name = "URL")]
pub http_sse: Option<Url>,
#[arg(long, value_name = "URL")]
pub http_stream: Option<Url>,
#[arg(long, requires = "http_sse")]
pub auth_header: Option<String>,
#[arg(long, requires = "http_sse")]
pub headers: Vec<String>,
}
#[derive(ValueEnum, Clone, Debug)]
pub enum OutputFormat {
Pretty,
Json,
Yaml,
Text,
}
#[derive(Clone, Debug, ValueEnum)]
pub enum ConfigTemplate {
Minimal,
Full,
Dev,
Prod,
}
#[derive(ValueEnum, Clone, Debug)]
pub enum Severity {
Info,
Warning,
Error,
Critical,
}
#[derive(ValueEnum, Clone, Debug)]
pub enum ExportFormat {
Json,
Yaml,
Markdown,
Html,
Csv,
}
impl TransportArgs {
pub fn to_transport_config(&self) -> anyhow::Result<TransportConfig> {
match (&self.stdio, &self.http_sse, &self.http_stream) {
(Some(command), None, None) => {
let args: Vec<String> = self.args.to_vec();
Ok(TransportConfig::stdio(command, &args))
}
(None, Some(url), None) => Ok(TransportConfig::http_sse(url.as_str())?),
(None, None, Some(url)) => Ok(TransportConfig::http_stream(url.clone())?),
(None, None, None) => {
anyhow::bail!("No transport specified. Use --stdio, --http-sse, or --http-stream")
}
_ => {
anyhow::bail!("Only one transport type can be specified at a time")
}
}
}
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputFormat::Pretty => write!(f, "pretty"),
OutputFormat::Json => write!(f, "json"),
OutputFormat::Yaml => write!(f, "yaml"),
OutputFormat::Text => write!(f, "text"),
}
}
}