spec_kit_mcp/tools/
specify.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 SpecifyParams {
18 requirements: String,
20
21 #[serde(default)]
23 user_stories: Option<String>,
24
25 #[serde(default = "default_specify_path")]
27 output_path: PathBuf,
28
29 #[serde(default = "default_format")]
31 format: String,
32}
33
34fn default_specify_path() -> PathBuf {
35 PathBuf::from("./speckit.specify")
36}
37
38fn default_format() -> String {
39 "markdown".to_string()
40}
41
42pub struct SpecifyTool {
44 cli: SpecKitCli,
45}
46
47impl SpecifyTool {
48 pub fn new(cli: SpecKitCli) -> Self {
50 Self { cli }
51 }
52}
53
54#[async_trait]
55impl Tool for SpecifyTool {
56 fn definition(&self) -> ToolDefinition {
57 ToolDefinition {
58 name: "speckit_specify".to_string(),
59 description: "Define what you want to build - requirements, user stories, and acceptance criteria".to_string(),
60 input_schema: json!({
61 "type": "object",
62 "properties": {
63 "requirements": {
64 "type": "string",
65 "description": "The requirements to specify. Can include features, constraints, user needs, etc."
66 },
67 "user_stories": {
68 "type": "string",
69 "description": "Optional user stories in 'As a... I want... So that...' format"
70 },
71 "output_path": {
72 "type": "string",
73 "description": "Path where the specification file will be written",
74 "default": "./speckit.specify"
75 },
76 "format": {
77 "type": "string",
78 "enum": ["markdown", "yaml", "json"],
79 "default": "markdown",
80 "description": "Output format for the specification"
81 }
82 },
83 "required": ["requirements"]
84 })
85 }
86 }
87
88 async fn execute(&self, params: Value) -> Result<ToolResult> {
89 let params: SpecifyParams =
90 serde_json::from_value(params).context("Failed to parse specify parameters")?;
91
92 tracing::info!(
93 output_path = %params.output_path.display(),
94 format = %params.format,
95 "Creating specification"
96 );
97
98 let mut content = format!(
100 "# Specification\n\n## Requirements\n\n{}\n",
101 params.requirements
102 );
103
104 if let Some(stories) = params.user_stories {
105 content.push_str(&format!("\n## User Stories\n\n{}\n", stories));
106 }
107
108 let result = self
110 .cli
111 .specify(&content, ¶ms.output_path, ¶ms.format)
112 .await?;
113
114 if !result.is_success() {
115 return Ok(ToolResult {
116 content: vec![ContentBlock::text(format!(
117 "Failed to create specification: {}",
118 result.stderr
119 ))],
120 is_error: Some(true),
121 });
122 }
123
124 let message = format!(
125 "Specification created successfully at {}\n\n\
126 The specification defines:\n\
127 - What needs to be built (requirements)\n\
128 - Who it's for and why (user stories)\n\
129 - Success criteria (acceptance criteria)\n\n\
130 Next step: Use speckit_plan tool to create a technical plan",
131 params.output_path.display()
132 );
133
134 Ok(ToolResult {
135 content: vec![ContentBlock::text(message)],
136 is_error: None,
137 })
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use tempfile::tempdir;
145
146 #[tokio::test]
147 async fn test_specify_tool_definition() {
148 let cli = SpecKitCli::new();
149 let tool = SpecifyTool::new(cli);
150 let def = tool.definition();
151
152 assert_eq!(def.name, "speckit_specify");
153 assert!(!def.description.is_empty());
154 }
155
156 #[tokio::test]
157 async fn test_specify_tool_execute() {
158 let cli = SpecKitCli::new_test_mode();
159 let tool = SpecifyTool::new(cli);
160
161 let dir = tempdir().unwrap();
162 let output_path = dir.path().join("specification.md");
163
164 let params = json!({
165 "requirements": "User authentication system with OAuth2 support",
166 "user_stories": "As a user, I want to login with Google, so that I don't need another password",
167 "output_path": output_path.to_str().unwrap()
168 });
169
170 let result = tool.execute(params).await.unwrap();
171 assert!(result.is_error.is_none() || !result.is_error.unwrap());
172 assert!(output_path.exists());
173 }
174}