spec_kit_mcp/tools/
check.rs1use 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#[derive(Debug, Deserialize, Serialize)]
16pub struct CheckParams {
17 #[serde(default = "default_true")]
19 check_speckit: bool,
20
21 #[serde(default = "default_true")]
23 check_git: bool,
24
25 #[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
44pub struct CheckTool {
46 #[allow(dead_code)]
47 cli: SpecKitCli,
48}
49
50impl CheckTool {
51 pub fn new(cli: SpecKitCli) -> Self {
53 Self { cli }
54 }
55
56 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 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 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 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 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 let result = tool.execute(json!({})).await.unwrap();
208 assert!(!result.content.is_empty());
209
210 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 assert!(tool.check_command("ls").await || tool.check_command("dir").await);
228 }
229}