spec_kit_mcp/tools/
clarify.rs

1//! Spec-Kit Clarify Tool
2//!
3//! Identifies and clarifies ambiguous requirements.
4
5use 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/// Parameters for the speckit_clarify tool
16#[derive(Debug, Deserialize, Serialize)]
17pub struct ClarifyParams {
18    /// Path to specification file
19    spec_file: PathBuf,
20
21    /// Specific questions to clarify (optional)
22    #[serde(default)]
23    questions: Option<Vec<String>>,
24
25    /// Output path for clarifications
26    #[serde(default = "default_clarify_path")]
27    output_path: PathBuf,
28}
29
30fn default_clarify_path() -> PathBuf {
31    PathBuf::from("./speckit.clarify")
32}
33
34/// Tool for clarifying specifications
35pub struct ClarifyTool {
36    #[allow(dead_code)]
37    cli: SpecKitCli,
38}
39
40impl ClarifyTool {
41    /// Create a new clarify tool
42    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        // Read the specification
86        let spec_content = tokio::fs::read_to_string(&params.spec_file)
87            .await
88            .context("Failed to read specification file")?;
89
90        // Analyze for common ambiguities
91        let mut clarifications = Vec::new();
92
93        // Check for vague terms
94        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        // Check for missing details
105        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        // Add user-provided questions
116        if let Some(questions) = params.questions {
117            clarifications.extend(questions.into_iter().map(|q| format!("Question: {}", q)));
118        }
119
120        // Create clarification document
121        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        // Write clarifications
140        tokio::fs::write(&params.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        // Create spec with ambiguities
193        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}