use super::validation::{ValidationConfig, ValidationEngine, ValidationStatus};
use crate::cli::{Severity, ValidateArgs};
use anyhow::Result;
use std::time::Duration;
pub async fn run(args: ValidateArgs) -> Result<()> {
tracing::info!("Starting MCP server validation");
let transport_config = args.transport.to_transport_config()?;
tracing::info!("Using transport: {}", transport_config.transport_type());
println!("🔍 MCP Server Validation");
println!("Transport: {}", transport_config.transport_type());
println!("Severity: {:?}", args.severity);
if !args.rules.is_empty() {
println!("📋 Validation rules: {:?}", args.rules);
} else {
println!("📋 Using default validation rules");
}
let mut config = ValidationConfig {
custom_rules: args.rules.clone(),
..Default::default()
};
match args.severity {
Severity::Info => {
config.strict_schema_validation = false;
config.test_error_conditions = false;
}
Severity::Warning => {
config.strict_schema_validation = true;
config.test_error_conditions = false;
}
Severity::Error => {
config.strict_schema_validation = true;
config.test_error_conditions = true;
}
Severity::Critical => {
config.strict_schema_validation = true;
config.test_error_conditions = true;
config.test_timeout = Duration::from_secs(60);
config.total_timeout = Duration::from_secs(600);
}
}
let mut validator = ValidationEngine::new(transport_config).with_config(config);
println!("🚀 Starting validation engine...");
match validator.validate().await {
Ok(report) => {
display_validation_results(&report.results, &args.severity);
if let Some(report_path) = &args.report {
generate_validation_report(&report, report_path)?;
println!("📄 Validation report saved to: {}", report_path.display());
} else {
use crate::paths::get_mcp_probe_paths;
let paths = get_mcp_probe_paths()?;
let auto_report_path = paths.report_file("validation-report", "json");
generate_validation_report(&report, &auto_report_path)?;
println!(
"📄 Validation report auto-saved to: {}",
auto_report_path.display()
);
}
let summary = &report.summary;
println!("\n📊 Validation Summary:");
println!("Total tests: {}", summary.total_tests);
println!(
"Passed: {} ({}%)",
summary.passed,
summary.compliance_percentage.round()
);
println!("Warnings: {}", summary.warnings);
println!("Errors: {}", summary.errors);
println!("Critical: {}", summary.critical);
if summary.compliance_percentage >= 90.0 {
println!("✅ Server validation completed successfully!");
} else if summary.compliance_percentage >= 70.0 {
println!("⚠️ Server validation completed with warnings");
} else {
println!("❌ Server validation failed - multiple issues found");
}
Ok(())
}
Err(e) => {
eprintln!("❌ Validation failed: {}", e);
Err(e)
}
}
}
fn display_validation_results(
results: &[super::validation::ValidationResult],
severity_filter: &Severity,
) {
println!("\n📊 Validation Results:");
println!("{:-<80}", "");
let filtered_results: Vec<_> = results
.iter()
.filter(|result| should_display_result(result, severity_filter))
.collect();
let mut categories: std::collections::HashMap<String, Vec<_>> =
std::collections::HashMap::new();
for result in &filtered_results {
let category = format!("{:?}", result.category);
categories.entry(category).or_default().push(result);
}
for (category, category_results) in categories {
println!("\n🏷️ {}", category);
println!("{:-<40}", "");
for result in category_results {
let duration_str = if result.duration.as_millis() > 0 {
format!(" ({}ms)", result.duration.as_millis())
} else {
String::new()
};
println!(
"{} {} - {}{}",
result.status.icon(),
result.test_name,
result.message,
duration_str
);
if matches!(
result.status,
ValidationStatus::Error | ValidationStatus::Critical
) && result.details.is_some()
{
if let Ok(details_str) =
serde_json::to_string_pretty(result.details.as_ref().unwrap())
{
let truncated = if details_str.len() > 200 {
format!("{}...", &details_str[..200])
} else {
details_str
};
println!(" Details: {}", truncated);
}
}
}
}
println!("{:-<80}", "");
}
fn should_display_result(
result: &super::validation::ValidationResult,
severity_filter: &Severity,
) -> bool {
match severity_filter {
Severity::Info => true, Severity::Warning => !matches!(result.status, ValidationStatus::Pass),
Severity::Error => matches!(
result.status,
ValidationStatus::Error | ValidationStatus::Critical
),
Severity::Critical => matches!(result.status, ValidationStatus::Critical),
}
}
fn generate_validation_report(
report: &super::validation::ValidationReport,
path: &std::path::Path,
) -> Result<()> {
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("json");
match extension {
"json" => {
let json_content = serde_json::to_string_pretty(report)?;
std::fs::write(path, json_content)?;
}
"yaml" | "yml" => {
let yaml_content = serde_yaml::to_string(report)?;
std::fs::write(path, yaml_content)?;
}
"md" | "markdown" => {
let markdown_content = generate_markdown_report(report)?;
std::fs::write(path, markdown_content)?;
}
_ => {
let json_content = serde_json::to_string_pretty(report)?;
std::fs::write(path, json_content)?;
}
}
Ok(())
}
fn generate_markdown_report(report: &super::validation::ValidationReport) -> Result<String> {
let mut content = String::new();
content.push_str("# MCP Server Validation Report\n\n");
content.push_str(&format!(
"**Generated:** {}\n",
report.metadata.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
));
content.push_str(&format!(
"**Validator Version:** {}\n",
report.metadata.validator_version
));
content.push_str(&format!(
"**Transport:** {}\n",
report.metadata.transport_type
));
content.push_str(&format!(
"**Duration:** {:.2}s\n\n",
report.metadata.total_duration.as_secs_f64()
));
content.push_str("## Summary\n\n");
content.push_str(&format!(
"- **Total Tests:** {}\n",
report.summary.total_tests
));
content.push_str(&format!(
"- **Passed:** {} ({:.1}%)\n",
report.summary.passed, report.summary.compliance_percentage
));
content.push_str(&format!("- **Warnings:** {}\n", report.summary.warnings));
content.push_str(&format!("- **Errors:** {}\n", report.summary.errors));
content.push_str(&format!("- **Critical:** {}\n", report.summary.critical));
content.push_str(&format!("- **Skipped:** {}\n\n", report.summary.skipped));
content.push_str("## Detailed Results\n\n");
let mut categories: std::collections::HashMap<String, Vec<_>> =
std::collections::HashMap::new();
for result in &report.results {
let category = format!("{:?}", result.category);
categories.entry(category).or_default().push(result);
}
for (category, results) in categories {
content.push_str(&format!("### {}\n\n", category));
for result in results {
let status_emoji = result.status.icon();
content.push_str(&format!(
"- {} **{}**: {}\n",
status_emoji, result.test_name, result.message
));
if result.duration.as_millis() > 0 {
content.push_str(&format!(
" - Duration: {}ms\n",
result.duration.as_millis()
));
}
}
content.push('\n');
}
content.push_str("## Performance\n\n");
content.push_str(&format!(
"- **Initialization Time:** {}ms\n",
report.performance.initialization_time.as_millis()
));
content.push_str(&format!(
"- **Average Request Time:** {}ms\n",
report.performance.average_request_time.as_millis()
));
content.push_str(&format!(
"- **Total Requests:** {}\n",
report.performance.total_requests
));
content.push_str(&format!(
"- **Failed Requests:** {}\n",
report.performance.failed_requests
));
content.push_str(&format!(
"- **Timeouts:** {}\n",
report.performance.timeouts
));
Ok(content)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::validation::*;
#[test]
fn test_severity_filtering() {
let result_pass = ValidationResult {
test_id: "test".to_string(),
test_name: "Test".to_string(),
category: ValidationCategory::Protocol,
status: ValidationStatus::Pass,
message: "Test message".to_string(),
details: None,
duration: std::time::Duration::from_millis(1),
timestamp: chrono::Utc::now(),
};
let result_error = ValidationResult {
test_id: "test".to_string(),
test_name: "Test".to_string(),
category: ValidationCategory::Protocol,
status: ValidationStatus::Error,
message: "Test message".to_string(),
details: None,
duration: std::time::Duration::from_millis(1),
timestamp: chrono::Utc::now(),
};
assert!(should_display_result(&result_pass, &Severity::Info));
assert!(should_display_result(&result_error, &Severity::Info));
assert!(!should_display_result(&result_pass, &Severity::Error));
assert!(should_display_result(&result_error, &Severity::Error));
}
#[tokio::test]
async fn test_markdown_report_generation() -> Result<()> {
let report = ValidationReport {
metadata: ReportMetadata {
generated_at: chrono::Utc::now(),
validator_version: "1.0.0".to_string(),
transport_type: "stdio".to_string(),
total_duration: Duration::from_secs(60),
config: ValidationConfig::default(),
},
summary: ValidationSummary {
total_tests: 5,
passed: 4,
info: 0,
warnings: 1,
errors: 0,
critical: 0,
skipped: 0,
compliance_percentage: 80.0,
},
results: vec![],
server_info: None,
performance: PerformanceMetrics {
initialization_time: Duration::from_millis(100),
average_request_time: Duration::from_millis(50),
total_requests: 5,
failed_requests: 0,
timeouts: 0,
},
};
let markdown = generate_markdown_report(&report)?;
assert!(markdown.contains("# MCP Server Validation Report"));
assert!(markdown.contains("**Total Tests:** 5"));
assert!(markdown.contains("80.0%"));
Ok(())
}
}