use std::fs;
use std::io::Read as _;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand, ValueEnum};
use mcp_conformance_core::requirement::{Registry, Verification};
use mcp_trace_validator::report::Verdict;
use mcp_trace_validator::{engine, reader};
const EXIT_OK: u8 = 0;
const EXIT_FINDINGS: u8 = 1;
const EXIT_USAGE: u8 = 2;
const EXIT_MALFORMED_TRACE: u8 = 3;
#[derive(Debug, Parser)]
#[command(name = "mcp-trace-validator", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Validate {
trace: String,
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
#[arg(long)]
strict: bool,
#[arg(long)]
registry: Option<PathBuf>,
},
Requirements {
#[arg(long, value_enum, default_value_t = Format::Human)]
format: Format,
#[arg(long)]
registry: Option<PathBuf>,
},
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Format {
Human,
Json,
Junit,
}
fn main() -> ExitCode {
let cli = Cli::parse();
let code = match cli.command {
Command::Validate {
trace,
format,
strict,
registry,
} => run_validate(&trace, format, strict, registry.as_deref()),
Command::Requirements { format, registry } => run_requirements(format, registry.as_deref()),
};
ExitCode::from(code)
}
fn load_registry(path: Option<&std::path::Path>) -> Result<Registry, String> {
match path {
None => Registry::builtin_2025_11_25().map_err(|error| error.to_string()),
Some(path) => {
let text = fs::read_to_string(path)
.map_err(|error| format!("cannot read {}: {error}", path.display()))?;
Registry::from_json(&text).map_err(|error| format!("{}: {error}", path.display()))
}
}
}
fn read_trace_document(source: &str) -> Result<String, String> {
if source == "-" {
let mut text = String::new();
std::io::stdin()
.read_to_string(&mut text)
.map_err(|error| format!("cannot read stdin: {error}"))?;
Ok(text)
} else {
fs::read_to_string(source).map_err(|error| format!("cannot read {source}: {error}"))
}
}
fn run_validate(
trace_source: &str,
format: Format,
strict: bool,
registry_path: Option<&std::path::Path>,
) -> u8 {
let registry = match load_registry(registry_path) {
Ok(registry) => registry,
Err(message) => {
eprintln!("error: {message}");
return EXIT_USAGE;
}
};
let document = match read_trace_document(trace_source) {
Ok(document) => document,
Err(message) => {
eprintln!("error: {message}");
return EXIT_USAGE;
}
};
let events = match reader::parse_trace(&document, &reader::Limits::default()) {
Ok(events) => events,
Err(error) => {
eprintln!("error: malformed trace: {error}");
return EXIT_MALFORMED_TRACE;
}
};
let report = engine::validate(®istry, &events);
match format {
Format::Human => print!("{}", report.render_human()),
Format::Json => match serde_json::to_string_pretty(&report) {
Ok(json) => println!("{json}"),
Err(error) => {
eprintln!("error: cannot serialize report: {error}");
return EXIT_USAGE;
}
},
Format::Junit => print!("{}", mcp_trace_validator::junit::render(&report)),
}
match report.verdict() {
Verdict::Fail => EXIT_FINDINGS,
Verdict::PassWithWarnings => {
if strict {
EXIT_FINDINGS
} else {
EXIT_OK
}
}
Verdict::Pass => EXIT_OK,
_ => EXIT_USAGE,
}
}
fn run_requirements(format: Format, registry_path: Option<&std::path::Path>) -> u8 {
let registry = match load_registry(registry_path) {
Ok(registry) => registry,
Err(message) => {
eprintln!("error: {message}");
return EXIT_USAGE;
}
};
match format {
Format::Junit => {
eprintln!("error: --format junit applies to validate, not requirements");
return EXIT_USAGE;
}
Format::Json => match serde_json::to_string_pretty(®istry) {
Ok(json) => println!("{json}"),
Err(error) => {
eprintln!("error: cannot serialize registry: {error}");
return EXIT_USAGE;
}
},
Format::Human => {
println!("requirement registry — revision {}", registry.revision());
for requirement in registry.requirements() {
let verification = match &requirement.verification {
Verification::Checks { checks } => format!("checks: {}", checks.join(", ")),
Verification::Excluded { .. } => "excluded".to_owned(),
_ => "unrecognized verification".to_owned(),
};
println!(
" {} {:<9} ({}) — {}",
requirement.id,
requirement.level.keyword(),
verification,
requirement.source.quote
);
}
}
}
EXIT_OK
}