spec_kit_mcp/tools/
constitution.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 ConstitutionParams {
18 principles: String,
20
21 #[serde(default)]
23 constraints: Option<String>,
24
25 #[serde(default = "default_constitution_path")]
27 output_path: PathBuf,
28}
29
30fn default_constitution_path() -> PathBuf {
31 PathBuf::from("./speckit.constitution")
32}
33
34pub struct ConstitutionTool {
36 cli: SpecKitCli,
37}
38
39impl ConstitutionTool {
40 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 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 let result = self.cli.constitution(&content, ¶ms.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}