spec_kit_mcp/tools/
check.rs

1//! Spec-Kit Check Tool
2//!
3//! Validates that required tools are installed.
4
5use anyhow::{Context, Result};
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9
10use crate::mcp::types::{ContentBlock, ToolDefinition, ToolResult};
11use crate::speckit::SpecKitCli;
12use crate::tools::Tool;
13
14/// Parameters for the speckit_check tool
15#[derive(Debug, Deserialize, Serialize)]
16pub struct CheckParams {
17    /// Check for spec-kit CLI
18    #[serde(default = "default_true")]
19    check_speckit: bool,
20
21    /// Check for git
22    #[serde(default = "default_true")]
23    check_git: bool,
24
25    /// Check for common AI coding assistants
26    #[serde(default = "default_true")]
27    check_ai_tools: bool,
28}
29
30fn default_true() -> bool {
31    true
32}
33
34impl Default for CheckParams {
35    fn default() -> Self {
36        Self {
37            check_speckit: true,
38            check_git: true,
39            check_ai_tools: true,
40        }
41    }
42}
43
44/// Tool for checking required tool installations
45pub struct CheckTool {
46    #[allow(dead_code)]
47    cli: SpecKitCli,
48}
49
50impl CheckTool {
51    /// Create a new check tool
52    pub fn new(cli: SpecKitCli) -> Self {
53        Self { cli }
54    }
55
56    /// Check if a command is available
57    async fn check_command(&self, command: &str) -> bool {
58        async_process::Command::new("which")
59            .arg(command)
60            .stdout(async_process::Stdio::null())
61            .stderr(async_process::Stdio::null())
62            .status()
63            .await
64            .map(|s| s.success())
65            .unwrap_or(false)
66    }
67}
68
69#[async_trait]
70impl Tool for CheckTool {
71    fn definition(&self) -> ToolDefinition {
72        ToolDefinition {
73            name: "speckit_check".to_string(),
74            description: "Validate that required tools are installed for spec-kit development"
75                .to_string(),
76            input_schema: json!({
77                "type": "object",
78                "properties": {
79                    "check_speckit": {
80                        "type": "boolean",
81                        "default": true,
82                        "description": "Check if spec-kit CLI is installed"
83                    },
84                    "check_git": {
85                        "type": "boolean",
86                        "default": true,
87                        "description": "Check if git is installed"
88                    },
89                    "check_ai_tools": {
90                        "type": "boolean",
91                        "default": true,
92                        "description": "Check for AI coding assistants (claude, cursor, etc.)"
93                    }
94                },
95                "required": []
96            }),
97        }
98    }
99
100    async fn execute(&self, params: Value) -> Result<ToolResult> {
101        let params: CheckParams =
102            if params.is_null() || params.as_object().is_some_and(|o| o.is_empty()) {
103                CheckParams::default()
104            } else {
105                serde_json::from_value(params).context("Failed to parse check parameters")?
106            };
107
108        tracing::info!("Checking tool installations");
109
110        let mut report = String::from("# Tool Installation Check\n\n");
111        let mut all_good = true;
112
113        // Check spec-kit CLI
114        if params.check_speckit {
115            report.push_str("## Spec-Kit CLI\n\n");
116            let has_specify = self.check_command("specify").await;
117            if has_specify {
118                report.push_str("✅ `specify` command is available\n");
119            } else {
120                report.push_str("❌ `specify` command not found\n");
121                report.push_str("   Install with: `uv tool install specify-cli`\n");
122                all_good = false;
123            }
124            report.push('\n');
125        }
126
127        // Check git
128        if params.check_git {
129            report.push_str("## Version Control\n\n");
130            let has_git = self.check_command("git").await;
131            if has_git {
132                report.push_str("✅ `git` is available\n");
133            } else {
134                report.push_str("❌ `git` not found\n");
135                report.push_str("   Install from: https://git-scm.com/\n");
136                all_good = false;
137            }
138            report.push('\n');
139        }
140
141        // Check AI tools
142        if params.check_ai_tools {
143            report.push_str("## AI Coding Assistants\n\n");
144
145            let ai_tools = vec![
146                ("code", "Visual Studio Code"),
147                ("code-insiders", "VS Code Insiders"),
148                ("cursor", "Cursor"),
149                ("windsurf", "Windsurf"),
150            ];
151
152            let mut found_any = false;
153            for (cmd, name) in &ai_tools {
154                if self.check_command(cmd).await {
155                    report.push_str(&format!("✅ `{}` ({}) is available\n", cmd, name));
156                    found_any = true;
157                }
158            }
159
160            if !found_any {
161                report.push_str("⚠️  No AI coding assistants found\n");
162                report.push_str("   Consider installing:\n");
163                report.push_str("   - VS Code: https://code.visualstudio.com/\n");
164                report.push_str("   - Cursor: https://cursor.sh/\n");
165                report.push_str("   - Windsurf: https://codeium.com/windsurf\n");
166            }
167            report.push('\n');
168        }
169
170        // Summary
171        report.push_str("## Summary\n\n");
172        if all_good {
173            report.push_str("✅ All required tools are installed!\n");
174            report.push_str("\nYou're ready to use spec-kit for spec-driven development.\n");
175        } else {
176            report.push_str("⚠️  Some required tools are missing.\n");
177            report.push_str("\nPlease install the missing tools to use spec-kit effectively.\n");
178        }
179
180        Ok(ToolResult {
181            content: vec![ContentBlock::text(report)],
182            is_error: Some(!all_good),
183        })
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[tokio::test]
192    async fn test_check_tool_definition() {
193        let cli = SpecKitCli::new();
194        let tool = CheckTool::new(cli);
195        let def = tool.definition();
196
197        assert_eq!(def.name, "speckit_check");
198        assert!(!def.description.is_empty());
199    }
200
201    #[tokio::test]
202    async fn test_check_tool_execute() {
203        let cli = SpecKitCli::new_test_mode();
204        let tool = CheckTool::new(cli);
205
206        // Test with default params
207        let result = tool.execute(json!({})).await.unwrap();
208        assert!(!result.content.is_empty());
209
210        // Test with specific params
211        let params = json!({
212            "check_speckit": true,
213            "check_git": true,
214            "check_ai_tools": false
215        });
216
217        let result = tool.execute(params).await.unwrap();
218        assert!(!result.content.is_empty());
219    }
220
221    #[tokio::test]
222    async fn test_check_command() {
223        let cli = SpecKitCli::new_test_mode();
224        let tool = CheckTool::new(cli);
225
226        // Should find common commands
227        assert!(tool.check_command("ls").await || tool.check_command("dir").await);
228    }
229}