spec_kit_mcp/tools/
clarify.rs1use anyhow::{Context, Result};
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use std::path::PathBuf;
10
11use crate::mcp::types::{ContentBlock, ToolDefinition, ToolResult};
12use crate::speckit::SpecKitCli;
13use crate::tools::Tool;
14
15#[derive(Debug, Deserialize, Serialize)]
17pub struct ClarifyParams {
18 spec_file: PathBuf,
20
21 #[serde(default)]
23 questions: Option<Vec<String>>,
24
25 #[serde(default = "default_clarify_path")]
27 output_path: PathBuf,
28}
29
30fn default_clarify_path() -> PathBuf {
31 PathBuf::from("./speckit.clarify")
32}
33
34pub struct ClarifyTool {
36 #[allow(dead_code)]
37 cli: SpecKitCli,
38}
39
40impl ClarifyTool {
41 pub fn new(cli: SpecKitCli) -> Self {
43 Self { cli }
44 }
45}
46
47#[async_trait]
48impl Tool for ClarifyTool {
49 fn definition(&self) -> ToolDefinition {
50 ToolDefinition {
51 name: "speckit_clarify".to_string(),
52 description: "Identify underspecified areas in the specification and generate clarification questions".to_string(),
53 input_schema: json!({
54 "type": "object",
55 "properties": {
56 "spec_file": {
57 "type": "string",
58 "description": "Path to the specification file to analyze"
59 },
60 "questions": {
61 "type": "array",
62 "items": { "type": "string" },
63 "description": "Specific questions to address (optional - will auto-detect if not provided)"
64 },
65 "output_path": {
66 "type": "string",
67 "description": "Path where clarifications will be written",
68 "default": "./speckit.clarify"
69 }
70 },
71 "required": ["spec_file"]
72 })
73 }
74 }
75
76 async fn execute(&self, params: Value) -> Result<ToolResult> {
77 let params: ClarifyParams =
78 serde_json::from_value(params).context("Failed to parse clarify parameters")?;
79
80 tracing::info!(
81 spec_file = %params.spec_file.display(),
82 "Analyzing specification for ambiguities"
83 );
84
85 let spec_content = tokio::fs::read_to_string(¶ms.spec_file)
87 .await
88 .context("Failed to read specification file")?;
89
90 let mut clarifications = Vec::new();
92
93 let vague_terms = ["maybe", "probably", "might", "could", "should consider"];
95 for term in &vague_terms {
96 if spec_content.to_lowercase().contains(term) {
97 clarifications.push(format!(
98 "Found vague term '{}' - needs concrete definition",
99 term
100 ));
101 }
102 }
103
104 if !spec_content.to_lowercase().contains("performance") {
106 clarifications.push("Performance requirements not specified".to_string());
107 }
108 if !spec_content.to_lowercase().contains("error") {
109 clarifications.push("Error handling approach not specified".to_string());
110 }
111 if !spec_content.to_lowercase().contains("test") {
112 clarifications.push("Testing strategy not specified".to_string());
113 }
114
115 if let Some(questions) = params.questions {
117 clarifications.extend(questions.into_iter().map(|q| format!("Question: {}", q)));
118 }
119
120 let mut content = String::from("# Specification Clarifications\n\n");
122 content.push_str(&format!("Source: {}\n\n", params.spec_file.display()));
123 content.push_str("## Issues Found\n\n");
124
125 if clarifications.is_empty() {
126 content.push_str("✓ No major ambiguities detected.\n");
127 content.push_str("\nThe specification appears well-defined.\n");
128 } else {
129 for (i, clarification) in clarifications.iter().enumerate() {
130 content.push_str(&format!("{}. {}\n", i + 1, clarification));
131 }
132 content.push_str("\n## Recommendations\n\n");
133 content.push_str("1. Address each issue above\n");
134 content.push_str("2. Update the specification with concrete details\n");
135 content.push_str("3. Review with stakeholders\n");
136 content.push_str("4. Re-run clarify to verify improvements\n");
137 }
138
139 tokio::fs::write(¶ms.output_path, &content)
141 .await
142 .context("Failed to write clarifications")?;
143
144 let message = format!(
145 "Clarification analysis complete!\n\n\
146 Analyzed: {}\n\
147 Issues found: {}\n\
148 Output: {}\n\n\
149 {}",
150 params.spec_file.display(),
151 clarifications.len(),
152 params.output_path.display(),
153 if clarifications.is_empty() {
154 "✓ Specification is well-defined"
155 } else {
156 "⚠ Please review and address the identified issues"
157 }
158 );
159
160 Ok(ToolResult {
161 content: vec![ContentBlock::text(message)],
162 is_error: None,
163 })
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use tempfile::tempdir;
171 use tokio::fs;
172
173 #[tokio::test]
174 async fn test_clarify_tool_definition() {
175 let cli = SpecKitCli::new();
176 let tool = ClarifyTool::new(cli);
177 let def = tool.definition();
178
179 assert_eq!(def.name, "speckit_clarify");
180 assert!(!def.description.is_empty());
181 }
182
183 #[tokio::test]
184 async fn test_clarify_tool_execute() {
185 let cli = SpecKitCli::new_test_mode();
186 let tool = ClarifyTool::new(cli);
187
188 let dir = tempdir().unwrap();
189 let spec_file = dir.path().join("spec.md");
190 let output_path = dir.path().join("clarify.md");
191
192 fs::write(
194 &spec_file,
195 "We might add OAuth. Performance should be good.",
196 )
197 .await
198 .unwrap();
199
200 let params = json!({
201 "spec_file": spec_file.to_str().unwrap(),
202 "output_path": output_path.to_str().unwrap()
203 });
204
205 let result = tool.execute(params).await.unwrap();
206 assert!(result.is_error.is_none() || !result.is_error.unwrap());
207 assert!(output_path.exists());
208 }
209}