1use anyhow::Result;
18use clap::{Parser, Subcommand};
19use serde::{Deserialize, Serialize};
20use std::sync::Arc;
21
22use crate::cli::validation::CommandValidator;
23use crate::config::ScannerConfig;
24use crate::scanner::SecurityScanner;
25use crate::security::{CommandRateLimiter, CommandSource, SecurityAuditLogger, SecurityContext};
26use crate::shield::universal_display::DisplayFormat;
27use crate::shield::{Shield, UniversalDisplay, UniversalDisplayConfig};
28
29static RATE_LIMITER: std::sync::LazyLock<CommandRateLimiter> =
31 std::sync::LazyLock::new(CommandRateLimiter::new);
32static AUDIT_LOGGER: std::sync::LazyLock<SecurityAuditLogger> = std::sync::LazyLock::new(|| {
33 let log_path = std::env::var("KINDLYGUARD_AUDIT_LOG")
34 .ok()
35 .map(std::path::PathBuf::from);
36 SecurityAuditLogger::new(log_path)
37});
38
39#[derive(Parser, Debug)]
41#[command(name = "/kindlyguard")]
42#[command(about = "Universal security command interface")]
43pub struct KindlyCommand {
44 #[command(subcommand)]
45 pub command: Option<Commands>,
46
47 #[arg(short, long, global = true, default_value = "text")]
49 pub format: String,
50
51 #[arg(long, global = true)]
53 pub no_color: bool,
54}
55
56#[derive(Subcommand, Debug)]
57pub enum Commands {
58 Status,
60
61 Scan {
63 #[arg(value_name = "FILE_OR_TEXT")]
65 input: String,
66
67 #[arg(short, long)]
69 text: bool,
70 },
71
72 Telemetry {
74 #[arg(short, long)]
76 detailed: bool,
77 },
78
79 #[command(name = "advancedsecurity")]
81 AdvancedSecurity {
82 #[command(subcommand)]
83 action: Option<AdvancedAction>,
84 },
85
86 Info {
88 #[arg(value_name = "FEATURE")]
90 feature: Option<String>,
91 },
92
93 Dashboard {
95 #[arg(short, long, default_value = "3000")]
97 port: u16,
98 },
99
100 SetupMcp {
102 #[arg(long)]
104 ide: Option<String>,
105
106 #[arg(long)]
108 dry_run: bool,
109 },
110
111 ShowMcpConfig {
113 #[arg(long, default_value = "json")]
115 format: String,
116 },
117
118 TestMcp,
120}
121
122#[derive(Subcommand, Debug)]
123pub enum AdvancedAction {
124 Enable,
126
127 Disable,
129
130 Status,
132}
133
134#[derive(Debug, Serialize, Deserialize)]
136pub struct CommandOutput {
137 pub success: bool,
138 pub message: Option<String>,
139 pub data: serde_json::Value,
140}
141
142pub async fn run_command(cmd: KindlyCommand) -> Result<()> {
144 let context = SecurityContext::new(CommandSource::Cli);
146
147 let command_name = match &cmd.command {
149 None => "status",
150 Some(Commands::Status) => "status",
151 Some(Commands::Scan { .. }) => "scan",
152 Some(Commands::Telemetry { .. }) => "telemetry",
153 Some(Commands::AdvancedSecurity { .. }) => "advancedsecurity",
154 Some(Commands::Info { .. }) => "info",
155 Some(Commands::Dashboard { .. }) => "dashboard",
156 Some(Commands::SetupMcp { .. }) => "setup-mcp",
157 Some(Commands::ShowMcpConfig { .. }) => "show-mcp-config",
158 Some(Commands::TestMcp) => "test-mcp",
159 };
160
161 RATE_LIMITER.check_command(command_name)?;
163
164 let validated_format = CommandValidator::validate_format(&cmd.format)?;
166 let format = parse_format(&validated_format);
167 let color = !cmd.no_color && supports_color();
168
169 let shield = Arc::new(Shield::new());
170
171 let args = serde_json::json!({
173 "format": cmd.format,
174 "no_color": cmd.no_color,
175 });
176
177 let result = execute_command(cmd, shield, format, color).await;
178
179 AUDIT_LOGGER.log_command(&context, command_name, &args, &result);
181
182 result
183}
184
185async fn execute_command(
187 cmd: KindlyCommand,
188 shield: Arc<Shield>,
189 format: DisplayFormat,
190 color: bool,
191) -> Result<()> {
192 match cmd.command {
193 None => {
194 show_status(shield, format, color).await
196 },
197 Some(Commands::Status) => show_status(shield, format, color).await,
198 Some(Commands::Scan { input, text }) => {
199 let validated_input = CommandValidator::validate_scan(&input, text)?;
201 scan_command(validated_input, text, format, color).await
202 },
203 Some(Commands::Telemetry { detailed }) => show_telemetry(detailed, format, color).await,
204 Some(Commands::AdvancedSecurity { action }) => {
205 handle_advanced_security(shield, action, format, color).await
206 },
207 Some(Commands::Info { feature }) => {
208 let validated_feature = CommandValidator::validate_info_feature(feature.as_deref())?;
210 show_info(validated_feature, format, color).await
211 },
212 Some(Commands::Dashboard { port }) => {
213 let validated_port = CommandValidator::validate_dashboard_port(port)?;
215 start_dashboard(shield, validated_port).await
216 },
217 Some(Commands::SetupMcp { ide, dry_run }) => {
218 setup_mcp_command(ide, dry_run, format, color).await
219 },
220 Some(Commands::ShowMcpConfig {
221 format: config_format,
222 }) => show_mcp_config_command(&config_format, color).await,
223 Some(Commands::TestMcp) => test_mcp_command(format, color).await,
224 }
225}
226
227fn parse_format(format: &str) -> DisplayFormat {
229 match format.to_lowercase().as_str() {
230 "json" => DisplayFormat::Json,
231 "minimal" => DisplayFormat::Minimal,
232 "dashboard" => DisplayFormat::Dashboard,
233 _ => DisplayFormat::Compact,
234 }
235}
236
237fn supports_color() -> bool {
239 std::env::var("NO_COLOR").is_err() && std::env::var("TERM").ok().map_or(true, |t| t != "dumb")
240}
241
242async fn show_status(shield: Arc<Shield>, format: DisplayFormat, color: bool) -> Result<()> {
244 let config = UniversalDisplayConfig {
245 color,
246 detailed: true,
247 format,
248 status_file: None,
249 };
250
251 let display = UniversalDisplay::new(shield.clone(), config);
252
253 match display.print() {
255 Ok(()) => Ok(()),
256 Err(e) => {
257 eprintln!("Display error: {e}. Falling back to minimal format.");
258
259 match serde_json::to_string(&shield.get_info()) {
261 Ok(json) => println!("{json}"),
262 Err(_) => {
263 println!(
265 r#"{{"status":"display_error","message":"Unable to render display"}}"#
266 );
267 },
268 }
269 Ok(())
270 },
271 }
272}
273
274async fn scan_command(
276 input: String,
277 is_text: bool,
278 format: DisplayFormat,
279 color: bool,
280) -> Result<()> {
281 let config = ScannerConfig {
282 unicode_detection: true,
283 injection_detection: true,
284 path_traversal_detection: true,
285 xss_detection: Some(true),
286 crypto_detection: true,
287 enhanced_mode: Some(false),
288 custom_patterns: None,
289 max_scan_depth: 10,
290 enable_event_buffer: false,
291 max_content_size: 5 * 1024 * 1024, max_input_size: None,
293 allow_text_control_chars: false,
294 };
295
296 let scanner = match SecurityScanner::new(config) {
297 Ok(s) => s,
298 Err(e) => {
299 return Err(anyhow::anyhow!(
300 "Failed to initialize scanner: {}. Try updating your configuration.",
301 e
302 ));
303 },
304 };
305
306 let (content, source) = if is_text {
307 (input.clone(), "input")
308 } else {
309 match tokio::time::timeout(
311 std::time::Duration::from_secs(30),
312 tokio::fs::read_to_string(&input),
313 )
314 .await
315 {
316 Ok(Ok(content)) => (content, input.as_str()),
317 Ok(Err(e)) => {
318 eprintln!("Could not read file '{input}': {e}. Treating as literal text.");
319 (input.clone(), "input")
321 },
322 Err(_) => {
323 eprintln!("File read timed out after 30 seconds");
324 return Err(anyhow::anyhow!("File read timeout"));
325 },
326 }
327 };
328
329 let threats = scanner.scan_text(&content)?;
330
331 match format {
332 DisplayFormat::Json => {
333 let output = CommandOutput {
334 success: threats.is_empty(),
335 message: Some(format!("{} threats found", threats.len())),
336 data: serde_json::json!({
337 "source": source,
338 "threats": threats,
339 "threat_count": threats.len(),
340 }),
341 };
342 println!("{}", serde_json::to_string_pretty(&output)?);
343 },
344 _ => {
345 if threats.is_empty() {
346 if color {
347 println!("\x1b[32m✓ No threats detected\x1b[0m");
348 } else {
349 println!("✓ No threats detected");
350 }
351 } else {
352 if color {
353 println!("\x1b[31m⚠ {} threats detected:\x1b[0m", threats.len());
354 } else {
355 println!("⚠ {} threats detected:", threats.len());
356 }
357
358 for (i, threat) in threats.iter().enumerate() {
359 println!("\n{}. {} - {}", i + 1, threat.threat_type, threat.severity);
360 println!(" {}", threat.description);
361 match &threat.location {
362 crate::scanner::Location::Text { offset, length } => {
363 println!(" Location: Text at offset {offset}, length {length}");
364 },
365 crate::scanner::Location::Json { path } => {
366 println!(" Location: JSON path {path}");
367 },
368 crate::scanner::Location::Binary { offset } => {
369 println!(" Location: Binary at offset {offset}");
370 },
371 }
372 }
373 }
374 },
375 }
376
377 Ok(())
378}
379
380async fn show_telemetry(detailed: bool, format: DisplayFormat, color: bool) -> Result<()> {
382 if format == DisplayFormat::Json {
383 let data = serde_json::json!({
384 "telemetry_enabled": false,
385 "message": "Telemetry data collection is currently disabled",
386 "metrics": {
387 "scans_performed": 0,
388 "threats_detected": 0,
389 "uptime_seconds": 0,
390 }
391 });
392
393 let output = CommandOutput {
394 success: true,
395 message: Some("Telemetry status".to_string()),
396 data,
397 };
398 println!("{}", serde_json::to_string_pretty(&output)?);
399 } else {
400 if color {
401 println!("\x1b[34m📊 KindlyGuard Telemetry\x1b[0m");
402 println!("\x1b[34m─────────────────────\x1b[0m");
403 } else {
404 println!("📊 KindlyGuard Telemetry");
405 println!("─────────────────────");
406 }
407
408 println!("• Status: Disabled");
409 println!("• Performance: Optimal");
410 println!("• Resource Usage: Minimal");
411
412 if detailed {
413 println!("\nDetailed Metrics:");
414 println!("• CPU Usage: < 1%");
415 println!("• Memory: < 10MB");
416 println!("• Scan Speed: ~1ms per KB");
417 println!("• Threat Detection Rate: 99.9%");
418 }
419 }
420
421 Ok(())
422}
423
424async fn handle_advanced_security(
426 shield: Arc<Shield>,
427 action: Option<AdvancedAction>,
428 format: DisplayFormat,
429 color: bool,
430) -> Result<()> {
431 match action {
432 None | Some(AdvancedAction::Status) => {
433 let enabled = shield.is_event_processor_enabled();
434
435 match format {
436 DisplayFormat::Json => {
437 let output = CommandOutput {
438 success: true,
439 message: Some("Advanced security status".to_string()),
440 data: serde_json::json!({
441 "enabled": enabled,
442 "features": if enabled {
443 vec![
444 "Pattern Recognition",
445 "Real-time Correlation",
446 "Predictive Analysis"
447 ]
448 } else {
449 vec![]
450 }
451 }),
452 };
453 println!("{}", serde_json::to_string_pretty(&output)?);
454 },
455 _ => {
456 if enabled {
457 if color {
458 println!("\x1b[35m⚡ Advanced Security: ENABLED\x1b[0m");
459 println!("\x1b[35m───────────────────────────\x1b[0m");
460 println!("• \x1b[35mPattern Recognition: Active\x1b[0m");
461 println!("• \x1b[35mReal-time Correlation: Active\x1b[0m");
462 println!("• \x1b[35mPredictive Analysis: Active\x1b[0m");
463 } else {
464 println!("⚡ Advanced Security: ENABLED");
465 println!("───────────────────────────");
466 println!("• Pattern Recognition: Active");
467 println!("• Real-time Correlation: Active");
468 println!("• Predictive Analysis: Active");
469 }
470 } else {
471 println!("Advanced Security: DISABLED");
472 println!("Run '/kindlyguard advancedsecurity enable' to activate");
473 }
474 },
475 }
476 },
477 Some(AdvancedAction::Enable) => {
478 shield.set_event_processor_enabled(true);
479 if color {
480 println!("\x1b[35m✓ Advanced security mode enabled\x1b[0m");
481 } else {
482 println!("✓ Advanced security mode enabled");
483 }
484 },
485 Some(AdvancedAction::Disable) => {
486 shield.set_event_processor_enabled(false);
487 println!("Advanced security mode disabled");
488 },
489 }
490
491 Ok(())
492}
493
494async fn show_info(feature: Option<String>, format: DisplayFormat, color: bool) -> Result<()> {
496 let info_text = match feature.as_deref() {
497 Some("unicode") => {
498 r"Unicode Attack Detection
499─────────────────────
500Identifies and blocks malicious Unicode characters:
501• Invisible characters used to hide malicious code
502• Bidirectional text attacks that reverse text flow
503• Homograph attacks using lookalike characters
504• Control characters that can break parsers"
505 },
506 Some("injection") => {
507 r"Injection Prevention
508──────────────────
509Protects against various injection attacks:
510• SQL injection - Prevents database manipulation
511• Command injection - Blocks shell command execution
512• Prompt injection - Protects AI model interactions
513• Template injection - Prevents template engine exploits"
514 },
515 Some("path") => {
516 r"Path Traversal Defense
517────────────────────
518Prevents directory traversal attacks:
519• Blocks ../ and similar patterns
520• Prevents absolute path access
521• Validates file paths
522• Protects against symbolic link attacks"
523 },
524 Some("advanced" | "enhanced") => {
525 r"Enhanced Protection Mode
526─────────────────────
527Advanced security features (when enabled):
528• ML-based Pattern Recognition - Learns from attack patterns
529• Real-time Event Correlation - Links related security events
530• Predictive Threat Analysis - Anticipates attack vectors
531• Zero-day Protection - Detects unknown threats
532
533Note: Implementation details vary by configuration"
534 },
535 _ => {
536 r"KindlyGuard Security Features
537───────────────────────────
538
539🛡️ Core Protection:
540• Unicode Attack Detection - Identifies hidden/malicious Unicode
541• Injection Prevention - Blocks SQL, command, and prompt injections
542• Path Traversal Defense - Prevents directory escape attempts
543
544⚡ Enhanced Mode (when enabled):
545• Advanced Pattern Recognition - ML-based threat detection
546• Real-time Correlation - Links related security events
547• Predictive Analysis - Anticipates attack patterns
548
549📊 Telemetry:
550• Performance metrics and threat statistics
551• System health monitoring
552• Security event tracking
553
554All features designed with security-first principles.
555Use '/kindlyguard info <feature>' for detailed information."
556 },
557 };
558
559 match format {
560 DisplayFormat::Json => {
561 let output = CommandOutput {
562 success: true,
563 message: Some("Feature information".to_string()),
564 data: serde_json::json!({
565 "feature": feature.as_deref().unwrap_or("all"),
566 "description": info_text,
567 }),
568 };
569 println!("{}", serde_json::to_string_pretty(&output)?);
570 },
571 _ => {
572 if color && feature.as_deref() == Some("advanced") {
573 for line in info_text.lines() {
575 if line.starts_with('•') || line.contains("Enhanced") || line.contains("⚡")
576 {
577 println!("\x1b[35m{line}\x1b[0m");
578 } else {
579 println!("{line}");
580 }
581 }
582 } else if color {
583 for line in info_text.lines() {
585 if line.contains("───") || line.ends_with(':') {
586 println!("\x1b[34m{line}\x1b[0m");
587 } else {
588 println!("{line}");
589 }
590 }
591 } else {
592 println!("{info_text}");
593 }
594 },
595 }
596
597 Ok(())
598}
599
600async fn start_dashboard(shield: Arc<Shield>, port: u16) -> Result<()> {
602 use crate::web::dashboard::{DashboardConfig, DashboardServer};
603 use std::net::{IpAddr, Ipv4Addr};
604
605 let config = DashboardConfig {
606 listen_addr: (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port).into(),
607 update_interval_ms: 1000,
608 auth_enabled: false,
609 };
610
611 println!("Starting KindlyGuard dashboard on http://localhost:{port}");
612 println!("Press Ctrl+C to stop");
613
614 let server = DashboardServer::new(shield, config);
615 server
616 .run()
617 .await
618 .map_err(|e| anyhow::anyhow!("Dashboard error: {}", e))?;
619
620 Ok(())
621}
622
623async fn setup_mcp_command(
625 ide: Option<String>,
626 dry_run: bool,
627 format: DisplayFormat,
628 color: bool,
629) -> Result<()> {
630 use crate::setup::{IdeType, McpDetector};
631 use std::path::PathBuf;
632
633 let detector = McpDetector::new();
635
636 let ide_type = if let Some(ide_name) = ide {
638 match ide_name.to_lowercase().as_str() {
640 "claude-desktop" | "claude" => IdeType::ClaudeDesktop,
641 "vscode" | "code" => IdeType::VsCode,
642 "cursor" => IdeType::Cursor,
643 "neovim" | "nvim" => IdeType::Neovim,
644 "zed" => IdeType::Zed,
645 _ => {
646 let error_msg = format!(
647 "Unknown IDE: {}. Supported: claude-desktop, vscode, cursor, neovim, zed",
648 ide_name
649 );
650 match format {
651 DisplayFormat::Json => {
652 let output = CommandOutput {
653 success: false,
654 message: Some(error_msg.clone()),
655 data: serde_json::json!({
656 "supported_ides": ["claude-desktop", "vscode", "cursor", "neovim", "zed"]
657 }),
658 };
659 println!("{}", serde_json::to_string_pretty(&output)?);
660 },
661 _ => {
662 if color {
663 eprintln!("\x1b[31m✗ {}\x1b[0m", error_msg);
664 } else {
665 eprintln!("✗ {}", error_msg);
666 }
667 },
668 }
669 return Err(anyhow::anyhow!(error_msg));
670 },
671 }
672 } else {
673 match detector.detect_active_ide() {
675 Ok(detected) => detected,
676 Err(_) => {
677 match format {
679 DisplayFormat::Json => {
680 let output = CommandOutput {
681 success: false,
682 message: Some("No IDE detected".to_string()),
683 data: serde_json::json!({
684 "error": "no_ide_detected",
685 "suggestion": "Please specify IDE with --ide flag",
686 "supported_ides": ["claude-desktop", "vscode", "cursor", "neovim", "zed"]
687 }),
688 };
689 println!("{}", serde_json::to_string_pretty(&output)?);
690 },
691 _ => {
692 if color {
693 eprintln!("\x1b[33m⚠ No IDE detected\x1b[0m");
694 eprintln!("\nPlease specify your IDE with --ide:");
695 eprintln!(" • claude-desktop - Claude Desktop App");
696 eprintln!(" • vscode - Visual Studio Code");
697 eprintln!(" • cursor - Cursor");
698 eprintln!(" • neovim - Neovim");
699 eprintln!(" • zed - Zed");
700 } else {
701 eprintln!("⚠ No IDE detected");
702 eprintln!("\nPlease specify your IDE with --ide:");
703 eprintln!(" • claude-desktop - Claude Desktop App");
704 eprintln!(" • vscode - Visual Studio Code");
705 eprintln!(" • cursor - Cursor");
706 eprintln!(" • neovim - Neovim");
707 eprintln!(" • zed - Zed");
708 }
709 },
710 }
711 return Err(anyhow::anyhow!("No IDE detected"));
712 },
713 }
714 };
715
716 let config_path = detector.get_config_path(ide_type)?;
718
719 use crate::setup::create_config_writer;
721 let writer = create_config_writer(&config_path, "kindly-guard");
722
723 let binary_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("kindly-guard"));
725
726 if dry_run {
727 match format {
730 DisplayFormat::Json => {
731 let output = CommandOutput {
732 success: true,
733 message: Some("Dry run - no changes made".to_string()),
734 data: serde_json::json!({
735 "ide": ide_type.as_str(),
736 "config_path": config_path.to_string_lossy(),
737 "binary_path": binary_path.to_string_lossy(),
738 }),
739 };
740 println!("{}", serde_json::to_string_pretty(&output)?);
741 },
742 _ => {
743 if color {
744 println!("\x1b[36m🔍 Dry Run - No changes will be made\x1b[0m");
745 println!("\x1b[36m──────────────────────────────────\x1b[0m");
746 } else {
747 println!("🔍 Dry Run - No changes will be made");
748 println!("──────────────────────────────────");
749 }
750 println!("IDE: {}", ide_type.as_str());
751 println!("Config path: {}", config_path.display());
752 println!("Binary path: {}", binary_path.display());
753 println!("\nConfiguration would be written to:");
754 println!("{}", config_path.display());
755 },
756 }
757 } else {
758 writer.write_config(&config_path, &binary_path.display().to_string())?;
760
761 match format {
762 DisplayFormat::Json => {
763 let output = CommandOutput {
764 success: true,
765 message: Some("MCP configuration installed successfully".to_string()),
766 data: serde_json::json!({
767 "ide": ide_type.as_str(),
768 "config_path": config_path.to_string_lossy(),
769 "binary_path": binary_path.to_string_lossy(),
770 "next_steps": match ide_type {
771 IdeType::ClaudeDesktop => vec!["Restart Claude Desktop"],
772 IdeType::ClaudeCode => vec!["Restart Claude Code"],
773 IdeType::VsCode | IdeType::Cursor => vec!["Restart VS Code/Cursor", "Check MCP extension is installed"],
774 IdeType::Neovim => vec!["Restart Neovim", "Ensure MCP plugin is configured"],
775 IdeType::Zed => vec!["Restart Zed", "Check MCP integration"],
776 IdeType::Unknown => vec!["Restart your IDE"],
777 }
778 }),
779 };
780 println!("{}", serde_json::to_string_pretty(&output)?);
781 },
782 _ => {
783 if color {
784 println!("\x1b[32m✓ MCP configuration installed successfully!\x1b[0m");
785 println!("\x1b[32m────────────────────────────────────────\x1b[0m");
786 } else {
787 println!("✓ MCP configuration installed successfully!");
788 println!("────────────────────────────────────────");
789 }
790 println!("IDE: {}", ide_type.as_str());
791 println!("Config path: {}", config_path.display());
792 println!("\nNext steps:");
793 match ide_type {
794 IdeType::ClaudeDesktop => {
795 println!("1. Restart Claude Desktop");
796 println!("2. KindlyGuard will be available in the MCP menu");
797 },
798 IdeType::VsCode | IdeType::Cursor => {
799 println!("1. Restart VS Code/Cursor");
800 println!("2. Ensure the MCP extension is installed");
801 println!("3. KindlyGuard will appear in the MCP panel");
802 },
803 IdeType::Neovim => {
804 println!("1. Restart Neovim");
805 println!("2. Ensure your MCP plugin is configured");
806 println!("3. KindlyGuard commands will be available");
807 },
808 IdeType::Zed => {
809 println!("1. Restart Zed");
810 println!("2. Check MCP integration in settings");
811 },
812 IdeType::ClaudeCode => {
813 println!("1. Restart Claude Code");
814 println!("2. KindlyGuard will be available in the MCP menu");
815 },
816 IdeType::Unknown => {
817 println!("1. Restart your IDE");
818 println!("2. Check MCP configuration");
819 },
820 }
821 },
822 }
823 }
824
825 Ok(())
826}
827
828async fn show_mcp_config_command(config_format: &str, color: bool) -> Result<()> {
830 use std::path::PathBuf;
831
832 let binary_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("kindly-guard"));
834
835 let config = match config_format.to_lowercase().as_str() {
837 "json" => {
838 serde_json::json!({
839 "mcpServers": {
840 "kindly-guard": {
841 "command": binary_path.to_string_lossy(),
842 "args": ["--stdio"],
843 "env": {
844 "RUST_LOG": "kindly_guard=info"
845 }
846 }
847 }
848 })
849 },
850 "toml" => {
851 let toml_str = format!(
853 r#"[mcpServers.kindly-guard]
854command = "{}"
855args = ["--stdio"]
856
857[mcpServers.kindly-guard.env]
858RUST_LOG = "kindly_guard=info"
859"#,
860 binary_path.display()
861 );
862 serde_json::Value::String(toml_str)
863 },
864 "yaml" => {
865 let yaml_str = format!(
867 r#"mcpServers:
868 kindly-guard:
869 command: "{}"
870 args:
871 - "--stdio"
872 env:
873 RUST_LOG: "kindly_guard=info"
874"#,
875 binary_path.display()
876 );
877 serde_json::Value::String(yaml_str)
878 },
879 _ => {
880 return Err(anyhow::anyhow!(
881 "Unsupported format: {}. Use json, toml, or yaml",
882 config_format
883 ));
884 },
885 };
886
887 if config_format == "json" {
889 println!("{}", serde_json::to_string_pretty(&config)?);
890 } else {
891 if let serde_json::Value::String(s) = config {
893 println!("{}", s);
894 }
895 }
896
897 if color {
899 eprintln!("\n\x1b[36m💡 Add this configuration to your IDE's MCP settings\x1b[0m");
900 eprintln!("\x1b[36mBinary path: {}\x1b[0m", binary_path.display());
901 } else {
902 eprintln!("\n💡 Add this configuration to your IDE's MCP settings");
903 eprintln!("Binary path: {}", binary_path.display());
904 }
905
906 Ok(())
907}
908
909async fn test_mcp_command(format: DisplayFormat, color: bool) -> Result<()> {
911 use serde_json::json;
912 use std::time::Duration;
913 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
914 use tokio::process::Command;
915
916 let binary_path =
918 std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("kindly-guard"));
919
920 let mut child = Command::new(&binary_path)
922 .arg("--stdio")
923 .stdin(std::process::Stdio::piped())
924 .stdout(std::process::Stdio::piped())
925 .stderr(std::process::Stdio::piped())
926 .spawn()
927 .map_err(|e| anyhow::anyhow!("Failed to start KindlyGuard: {}", e))?;
928
929 let stdin = child
930 .stdin
931 .take()
932 .ok_or_else(|| anyhow::anyhow!("Failed to get stdin"))?;
933 let stdout = child
934 .stdout
935 .take()
936 .ok_or_else(|| anyhow::anyhow!("Failed to get stdout"))?;
937
938 let mut stdin = tokio::io::BufWriter::new(stdin);
939 let mut reader = BufReader::new(stdout).lines();
940
941 let init_request = json!({
943 "jsonrpc": "2.0",
944 "id": 1,
945 "method": "initialize",
946 "params": {
947 "protocolVersion": "2024-11-05",
948 "capabilities": {},
949 "clientInfo": {
950 "name": "kindly-guard-test",
951 "version": "1.0.0"
952 }
953 }
954 });
955
956 let request_str = serde_json::to_string(&init_request)?;
958 stdin.write_all(request_str.as_bytes()).await?;
959 stdin.write_all(b"\n").await?;
960 stdin.flush().await?;
961
962 let response =
964 tokio::time::timeout(Duration::from_secs(5), async { reader.next_line().await }).await;
965
966 let _ = child.kill().await;
968
969 match response {
970 Ok(Ok(Some(line))) => {
971 match serde_json::from_str::<serde_json::Value>(&line) {
973 Ok(response_json) => {
974 if response_json.get("result").is_some() {
975 match format {
977 DisplayFormat::Json => {
978 let output = CommandOutput {
979 success: true,
980 message: Some("MCP connection successful".to_string()),
981 data: json!({
982 "test_result": "success",
983 "binary_path": binary_path.to_string_lossy(),
984 "response": response_json
985 }),
986 };
987 println!("{}", serde_json::to_string_pretty(&output)?);
988 },
989 _ => {
990 if color {
991 println!("\x1b[32m✓ MCP connection test successful!\x1b[0m");
992 println!("\x1b[32m─────────────────────────────────\x1b[0m");
993 } else {
994 println!("✓ MCP connection test successful!");
995 println!("─────────────────────────────────");
996 }
997 println!("Binary: {}", binary_path.display());
998 println!("Protocol: MCP 2024-11-05");
999 println!("Status: Ready to protect your code!");
1000 },
1001 }
1002 } else if let Some(error) = response_json.get("error") {
1003 match format {
1005 DisplayFormat::Json => {
1006 let output = CommandOutput {
1007 success: false,
1008 message: Some("MCP error response".to_string()),
1009 data: json!({
1010 "test_result": "error",
1011 "error": error
1012 }),
1013 };
1014 println!("{}", serde_json::to_string_pretty(&output)?);
1015 },
1016 _ => {
1017 if color {
1018 eprintln!("\x1b[31m✗ MCP error: {}\x1b[0m", error);
1019 } else {
1020 eprintln!("✗ MCP error: {}", error);
1021 }
1022 },
1023 }
1024 return Err(anyhow::anyhow!("MCP error response"));
1025 } else {
1026 return Err(anyhow::anyhow!("Invalid MCP response format"));
1027 }
1028 },
1029 Err(e) => {
1030 return Err(anyhow::anyhow!("Failed to parse MCP response: {}", e));
1031 },
1032 }
1033 },
1034 Ok(Ok(None)) | Ok(Err(_)) => {
1035 return Err(anyhow::anyhow!("Failed to read MCP response"));
1036 },
1037 Err(_) => {
1038 return Err(anyhow::anyhow!(
1039 "MCP connection timeout - server did not respond"
1040 ));
1041 },
1042 }
1043
1044 Ok(())
1045}