spec_kit_mcp/tools/
tasks.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 TasksParams {
18 plan_file: PathBuf,
20
21 #[serde(default = "default_breakdown_level")]
23 breakdown_level: String,
24
25 #[serde(default = "default_tasks_path")]
27 output_path: PathBuf,
28}
29
30fn default_breakdown_level() -> String {
31 "medium".to_string()
32}
33
34fn default_tasks_path() -> PathBuf {
35 PathBuf::from("./speckit.tasks")
36}
37
38pub struct TasksTool {
40 cli: SpecKitCli,
41}
42
43impl TasksTool {
44 pub fn new(cli: SpecKitCli) -> Self {
46 Self { cli }
47 }
48}
49
50#[async_trait]
51impl Tool for TasksTool {
52 fn definition(&self) -> ToolDefinition {
53 ToolDefinition {
54 name: "speckit_tasks".to_string(),
55 description: "Generate actionable task lists from the technical plan, breaking down work into manageable items".to_string(),
56 input_schema: json!({
57 "type": "object",
58 "properties": {
59 "plan_file": {
60 "type": "string",
61 "description": "Path to the plan file (speckit.plan)"
62 },
63 "breakdown_level": {
64 "type": "string",
65 "enum": ["high", "medium", "detailed"],
66 "default": "medium",
67 "description": "Level of task breakdown (high=major milestones, detailed=granular tasks)"
68 },
69 "output_path": {
70 "type": "string",
71 "description": "Path where the tasks file will be written",
72 "default": "./speckit.tasks"
73 }
74 },
75 "required": ["plan_file"]
76 })
77 }
78 }
79
80 async fn execute(&self, params: Value) -> Result<ToolResult> {
81 let params: TasksParams =
82 serde_json::from_value(params).context("Failed to parse tasks parameters")?;
83
84 tracing::info!(
85 plan_file = %params.plan_file.display(),
86 breakdown_level = %params.breakdown_level,
87 output_path = %params.output_path.display(),
88 "Generating task list"
89 );
90
91 let result = self
93 .cli
94 .tasks(¶ms.plan_file, ¶ms.output_path)
95 .await?;
96
97 if !result.is_success() {
98 return Ok(ToolResult {
99 content: vec![ContentBlock::text(format!(
100 "Failed to generate tasks: {}",
101 result.stderr
102 ))],
103 is_error: Some(true),
104 });
105 }
106
107 let message = format!(
108 "Task list generated successfully at {}\n\n\
109 The task list includes:\n\
110 - Prioritized actionable items\n\
111 - Clear acceptance criteria\n\
112 - Dependencies between tasks\n\
113 - Estimated effort levels\n\n\
114 Next step: Use speckit_implement tool to execute the tasks",
115 params.output_path.display()
116 );
117
118 Ok(ToolResult {
119 content: vec![ContentBlock::text(message)],
120 is_error: None,
121 })
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use tempfile::tempdir;
129 use tokio::fs;
130
131 #[tokio::test]
132 async fn test_tasks_tool_definition() {
133 let cli = SpecKitCli::new();
134 let tool = TasksTool::new(cli);
135 let def = tool.definition();
136
137 assert_eq!(def.name, "speckit_tasks");
138 assert!(!def.description.is_empty());
139 }
140
141 #[tokio::test]
142 async fn test_tasks_tool_execute() {
143 let cli = SpecKitCli::new_test_mode();
144 let tool = TasksTool::new(cli);
145
146 let dir = tempdir().unwrap();
147 let plan_file = dir.path().join("plan.md");
148 let output_path = dir.path().join("tasks.md");
149
150 fs::write(&plan_file, "Test plan").await.unwrap();
152
153 let params = json!({
154 "plan_file": plan_file.to_str().unwrap(),
155 "breakdown_level": "medium",
156 "output_path": output_path.to_str().unwrap()
157 });
158
159 let result = tool.execute(params).await.unwrap();
160 assert!(result.is_error.is_none() || !result.is_error.unwrap());
161 }
162}