spec_kit_mcp/tools/
plan.rs

1//! Spec-Kit Plan Tool
2//!
3//! Creates technical implementation 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_plan tool
16#[derive(Debug, Deserialize, Serialize)]
17pub struct PlanParams {
18    /// Path to specification file
19    spec_file: PathBuf,
20
21    /// Tech stack to use
22    #[serde(default)]
23    tech_stack: Option<String>,
24
25    /// Output path for plan file
26    #[serde(default = "default_plan_path")]
27    output_path: PathBuf,
28}
29
30fn default_plan_path() -> PathBuf {
31    PathBuf::from("./speckit.plan")
32}
33
34/// Tool for creating technical plans
35pub struct PlanTool {
36    cli: SpecKitCli,
37}
38
39impl PlanTool {
40    /// Create a new plan tool
41    pub fn new(cli: SpecKitCli) -> Self {
42        Self { cli }
43    }
44}
45
46#[async_trait]
47impl Tool for PlanTool {
48    fn definition(&self) -> ToolDefinition {
49        ToolDefinition {
50            name: "speckit_plan".to_string(),
51            description: "Create a technical implementation plan based on the specification, including architecture, tech stack, and approach".to_string(),
52            input_schema: json!({
53                "type": "object",
54                "properties": {
55                    "spec_file": {
56                        "type": "string",
57                        "description": "Path to the specification file (speckit.specify)"
58                    },
59                    "tech_stack": {
60                        "type": "string",
61                        "description": "Technology stack to use (e.g., 'Rust + Tokio', 'Python + FastAPI')"
62                    },
63                    "output_path": {
64                        "type": "string",
65                        "description": "Path where the plan file will be written",
66                        "default": "./speckit.plan"
67                    }
68                },
69                "required": ["spec_file"]
70            })
71        }
72    }
73
74    async fn execute(&self, params: Value) -> Result<ToolResult> {
75        let params: PlanParams =
76            serde_json::from_value(params).context("Failed to parse plan parameters")?;
77
78        tracing::info!(
79            spec_file = %params.spec_file.display(),
80            output_path = %params.output_path.display(),
81            "Creating technical plan"
82        );
83
84        // Execute spec-kit plan command
85        let result = self
86            .cli
87            .plan(&params.spec_file, &params.output_path)
88            .await?;
89
90        if !result.is_success() {
91            return Ok(ToolResult {
92                content: vec![ContentBlock::text(format!(
93                    "Failed to create plan: {}",
94                    result.stderr
95                ))],
96                is_error: Some(true),
97            });
98        }
99
100        let message = format!(
101            "Technical plan created successfully at {}\n\n\
102            The plan includes:\n\
103            - Architecture and system design\n\
104            - Technology stack and frameworks\n\
105            - Implementation approach\n\
106            - Module breakdown\n\n\
107            Next step: Use speckit_tasks tool to generate actionable tasks",
108            params.output_path.display()
109        );
110
111        Ok(ToolResult {
112            content: vec![ContentBlock::text(message)],
113            is_error: None,
114        })
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use tempfile::tempdir;
122    use tokio::fs;
123
124    #[tokio::test]
125    async fn test_plan_tool_definition() {
126        let cli = SpecKitCli::new();
127        let tool = PlanTool::new(cli);
128        let def = tool.definition();
129
130        assert_eq!(def.name, "speckit_plan");
131        assert!(!def.description.is_empty());
132    }
133
134    #[tokio::test]
135    async fn test_plan_tool_execute() {
136        let cli = SpecKitCli::new_test_mode();
137        let tool = PlanTool::new(cli);
138
139        let dir = tempdir().unwrap();
140        let spec_file = dir.path().join("spec.md");
141        let output_path = dir.path().join("plan.md");
142
143        // Create dummy spec file
144        fs::write(&spec_file, "Test specification").await.unwrap();
145
146        let params = json!({
147            "spec_file": spec_file.to_str().unwrap(),
148            "tech_stack": "Rust + Tokio",
149            "output_path": output_path.to_str().unwrap()
150        });
151
152        let result = tool.execute(params).await.unwrap();
153        assert!(result.is_error.is_none() || !result.is_error.unwrap());
154    }
155}