Skip to main content

chant/mcp/tools/
lifecycle.rs

1//! MCP tools for spec lifecycle transitions
2
3use anyhow::Result;
4use serde_json::{json, Value};
5
6use crate::operations;
7use crate::spec::{load_all_specs, resolve_spec, SpecStatus};
8
9use super::super::handlers::mcp_ensure_initialized;
10
11pub fn tool_chant_finalize(arguments: Option<&Value>) -> Result<Value> {
12    let specs_dir = match mcp_ensure_initialized() {
13        Ok(dir) => dir,
14        Err(err_response) => return Ok(err_response),
15    };
16
17    let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
18
19    let id = args
20        .get("id")
21        .and_then(|v| v.as_str())
22        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
23
24    let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
25
26    let mut spec = match resolve_spec(&specs_dir, id) {
27        Ok(s) => s,
28        Err(e) => {
29            return Ok(json!({
30                "content": [
31                    {
32                        "type": "text",
33                        "text": e.to_string()
34                    }
35                ],
36                "isError": true
37            }));
38        }
39    };
40
41    let spec_id = spec.id.clone();
42
43    // Check if spec is in valid state for finalization
44    match spec.frontmatter.status {
45        SpecStatus::Completed | SpecStatus::InProgress | SpecStatus::Failed => {
46            // Valid for finalization
47        }
48        _ => {
49            return Ok(json!({
50                "content": [
51                    {
52                        "type": "text",
53                        "text": format!("Spec '{}' must be in_progress, completed, or failed to finalize. Current status: {:?}", spec_id, spec.frontmatter.status)
54                    }
55                ],
56                "isError": true
57            }));
58        }
59    }
60
61    // Check for unchecked acceptance criteria
62    let unchecked = spec.count_unchecked_checkboxes();
63    if unchecked > 0 {
64        return Ok(json!({
65            "content": [
66                {
67                    "type": "text",
68                    "text": format!("Spec '{}' has {} unchecked acceptance criteria. All criteria must be checked before finalization.", spec_id, unchecked)
69                }
70            ],
71            "isError": true
72        }));
73    }
74
75    // Load config and all specs for finalization
76    let config = match crate::config::Config::load() {
77        Ok(c) => c,
78        Err(e) => {
79            return Ok(json!({
80                "content": [{ "type": "text", "text": format!("Failed to load config: {}", e) }],
81                "isError": true
82            }));
83        }
84    };
85
86    let all_specs = match load_all_specs(&specs_dir) {
87        Ok(specs) => specs,
88        Err(e) => {
89            return Ok(json!({
90                "content": [{ "type": "text", "text": format!("Failed to load specs: {}", e) }],
91                "isError": true
92            }));
93        }
94    };
95
96    // Use operations module for finalization with full validation
97    let spec_repo = crate::repository::spec_repository::FileSpecRepository::new(specs_dir.clone());
98    let options = crate::operations::finalize::FinalizeOptions {
99        allow_no_commits: false,
100        commits: None, // Auto-detect commits
101        force,
102    };
103
104    match crate::operations::finalize::finalize_spec(
105        &mut spec, &spec_repo, &config, &all_specs, options,
106    ) {
107        Ok(_) => Ok(json!({
108            "content": [
109                {
110                    "type": "text",
111                    "text": format!("Finalized spec: {}", spec_id)
112                }
113            ]
114        })),
115        Err(e) => Ok(json!({
116            "content": [
117                {
118                    "type": "text",
119                    "text": format!("Failed to finalize spec: {}", e)
120                }
121            ],
122            "isError": true
123        })),
124    }
125}
126
127pub fn tool_chant_reset(arguments: Option<&Value>) -> Result<Value> {
128    let specs_dir = match mcp_ensure_initialized() {
129        Ok(dir) => dir,
130        Err(err_response) => return Ok(err_response),
131    };
132
133    let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
134
135    let id = args
136        .get("id")
137        .and_then(|v| v.as_str())
138        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
139
140    let mut spec = match resolve_spec(&specs_dir, id) {
141        Ok(s) => s,
142        Err(e) => {
143            return Ok(json!({
144                "content": [
145                    {
146                        "type": "text",
147                        "text": e.to_string()
148                    }
149                ],
150                "isError": true
151            }));
152        }
153    };
154
155    let spec_id = spec.id.clone();
156    let spec_path = specs_dir.join(format!("{}.md", spec.id));
157
158    // Use operations module for reset
159    let options = crate::operations::reset::ResetOptions::default();
160
161    match crate::operations::reset::reset_spec(&mut spec, &spec_path, options) {
162        Ok(_) => Ok(json!({
163            "content": [
164                {
165                    "type": "text",
166                    "text": format!("Reset spec '{}' to pending", spec_id)
167                }
168            ]
169        })),
170        Err(e) => Ok(json!({
171            "content": [
172                {
173                    "type": "text",
174                    "text": format!("Failed to reset spec: {}", e)
175                }
176            ],
177            "isError": true
178        })),
179    }
180}
181
182pub fn tool_chant_cancel(arguments: Option<&Value>) -> Result<Value> {
183    let specs_dir = match mcp_ensure_initialized() {
184        Ok(dir) => dir,
185        Err(err_response) => return Ok(err_response),
186    };
187
188    let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
189
190    let id = args
191        .get("id")
192        .and_then(|v| v.as_str())
193        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
194
195    let options = operations::CancelOptions::default();
196
197    match operations::cancel_spec(&specs_dir, id, &options) {
198        Ok(spec) => Ok(json!({
199            "content": [
200                {
201                    "type": "text",
202                    "text": format!("Cancelled spec: {}", spec.id)
203                }
204            ]
205        })),
206        Err(e) => Ok(json!({
207            "content": [
208                {
209                    "type": "text",
210                    "text": e.to_string()
211                }
212            ],
213            "isError": true
214        })),
215    }
216}
217
218pub fn tool_chant_archive(arguments: Option<&Value>) -> Result<Value> {
219    let specs_dir = match mcp_ensure_initialized() {
220        Ok(dir) => dir,
221        Err(err_response) => return Ok(err_response),
222    };
223
224    let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
225
226    let id = args
227        .get("id")
228        .and_then(|v| v.as_str())
229        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
230
231    let options = operations::ArchiveOptions::default();
232
233    match operations::archive_spec(&specs_dir, id, &options) {
234        Ok(dest_path) => Ok(json!({
235            "content": [
236                {
237                    "type": "text",
238                    "text": format!("Archived spec: {} -> {}", id, dest_path.display())
239                }
240            ]
241        })),
242        Err(e) => Ok(json!({
243            "content": [
244                {
245                    "type": "text",
246                    "text": e.to_string()
247                }
248            ],
249            "isError": true
250        })),
251    }
252}