spec_kit_mcp/tools/
constitution.rs

1//! Spec-Kit Constitution Tool
2//!
3//! Creates project governing principles and development guidelines.
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_constitution tool
16#[derive(Debug, Deserialize, Serialize)]
17pub struct ConstitutionParams {
18    /// Core principles and values
19    principles: String,
20
21    /// Technical constraints (optional)
22    #[serde(default)]
23    constraints: Option<String>,
24
25    /// Output path for constitution file
26    #[serde(default = "default_constitution_path")]
27    output_path: PathBuf,
28}
29
30fn default_constitution_path() -> PathBuf {
31    PathBuf::from("./speckit.constitution")
32}
33
34/// Tool for creating project constitutions
35pub struct ConstitutionTool {
36    cli: SpecKitCli,
37}
38
39impl ConstitutionTool {
40    /// Create a new constitution tool
41    pub fn new(cli: SpecKitCli) -> Self {
42        Self { cli }
43    }
44}
45
46#[async_trait]
47impl Tool for ConstitutionTool {
48    fn definition(&self) -> ToolDefinition {
49        ToolDefinition {
50            name: "speckit_constitution".to_string(),
51            description: "Create or update project governing principles, development standards, and technical constraints".to_string(),
52            input_schema: json!({
53                "type": "object",
54                "properties": {
55                    "principles": {
56                        "type": "string",
57                        "description": "Core principles and values that govern the project (e.g., simplicity, performance, security)"
58                    },
59                    "constraints": {
60                        "type": "string",
61                        "description": "Technical constraints and boundaries (optional)"
62                    },
63                    "output_path": {
64                        "type": "string",
65                        "description": "Path where the constitution file will be written",
66                        "default": "./speckit.constitution"
67                    }
68                },
69                "required": ["principles"]
70            })
71        }
72    }
73
74    async fn execute(&self, params: Value) -> Result<ToolResult> {
75        let params: ConstitutionParams =
76            serde_json::from_value(params).context("Failed to parse constitution parameters")?;
77
78        tracing::info!(
79            output_path = %params.output_path.display(),
80            "Creating constitution"
81        );
82
83        // Format the constitution content
84        let mut content = format!(
85            "# Project Constitution\n\n## Core Principles\n\n{}\n",
86            params.principles
87        );
88
89        if let Some(constraints) = params.constraints {
90            content.push_str(&format!("\n## Technical Constraints\n\n{}\n", constraints));
91        }
92
93        // Write constitution file
94        let result = self.cli.constitution(&content, &params.output_path).await?;
95
96        if !result.is_success() {
97            return Ok(ToolResult {
98                content: vec![ContentBlock::text(format!(
99                    "Failed to create constitution: {}",
100                    result.stderr
101                ))],
102                is_error: Some(true),
103            });
104        }
105
106        let message = format!(
107            "Constitution created successfully at {}\n\n\
108            The constitution defines:\n\
109            - Core principles that guide development\n\
110            - Technical constraints and boundaries\n\
111            - Standards for code quality and architecture\n\n\
112            Next step: Use speckit_specify tool to define requirements",
113            params.output_path.display()
114        );
115
116        Ok(ToolResult {
117            content: vec![ContentBlock::text(message)],
118            is_error: None,
119        })
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use tempfile::tempdir;
127
128    #[tokio::test]
129    async fn test_constitution_tool_definition() {
130        let cli = SpecKitCli::new();
131        let tool = ConstitutionTool::new(cli);
132        let def = tool.definition();
133
134        assert_eq!(def.name, "speckit_constitution");
135        assert!(!def.description.is_empty());
136    }
137
138    #[tokio::test]
139    async fn test_constitution_tool_execute() {
140        let cli = SpecKitCli::new_test_mode();
141        let tool = ConstitutionTool::new(cli);
142
143        let dir = tempdir().unwrap();
144        let output_path = dir.path().join("constitution.md");
145
146        let params = json!({
147            "principles": "Simplicity, Performance, Security",
148            "constraints": "Must support Python 3.11+",
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        assert!(output_path.exists());
155    }
156}