spec_kit_mcp/tools/
tasks.rs

1//! Spec-Kit Tasks Tool
2//!
3//! Generates actionable task lists from technical plans.
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_tasks tool
16#[derive(Debug, Deserialize, Serialize)]
17pub struct TasksParams {
18    /// Path to plan file
19    plan_file: PathBuf,
20
21    /// Breakdown level
22    #[serde(default = "default_breakdown_level")]
23    breakdown_level: String,
24
25    /// Output path for tasks file
26    #[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
38/// Tool for generating task lists
39pub struct TasksTool {
40    cli: SpecKitCli,
41}
42
43impl TasksTool {
44    /// Create a new tasks tool
45    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        // Execute spec-kit tasks command
92        let result = self
93            .cli
94            .tasks(&params.plan_file, &params.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        // Create dummy plan file
151        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}