Skip to main content

chant/mcp/tools/
lifecycle.rs

1//! MCP tools for spec lifecycle transitions
2
3use anyhow::Result;
4use serde_json::Value;
5
6use crate::operations;
7use crate::spec::{load_all_specs, resolve_spec, SpecStatus};
8
9use super::super::handlers::mcp_ensure_initialized;
10use super::super::response::{mcp_error_response, mcp_text_response};
11
12pub fn tool_chant_finalize(arguments: Option<&Value>) -> Result<Value> {
13    let specs_dir = match mcp_ensure_initialized() {
14        Ok(dir) => dir,
15        Err(err_response) => return Ok(err_response),
16    };
17
18    let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
19
20    let id = args
21        .get("id")
22        .and_then(|v| v.as_str())
23        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
24
25    let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
26
27    let mut spec = match resolve_spec(&specs_dir, id) {
28        Ok(s) => s,
29        Err(e) => {
30            return Ok(mcp_error_response(e.to_string()));
31        }
32    };
33
34    let spec_id = spec.id.clone();
35
36    // Check if spec is in valid state for finalization
37    match spec.frontmatter.status {
38        SpecStatus::Completed | SpecStatus::InProgress | SpecStatus::Failed => {
39            // Valid for finalization
40        }
41        _ => {
42            return Ok(mcp_error_response(format!("Spec '{}' must be in_progress, completed, or failed to finalize. Current status: {:?}", spec_id, spec.frontmatter.status)));
43        }
44    }
45
46    // Check for unchecked acceptance criteria
47    let unchecked = spec.count_unchecked_checkboxes();
48    if unchecked > 0 {
49        return Ok(mcp_error_response(format!("Spec '{}' has {} unchecked acceptance criteria. All criteria must be checked before finalization.", spec_id, unchecked)));
50    }
51
52    // Load config and all specs for finalization
53    let config = match crate::config::Config::load() {
54        Ok(c) => c,
55        Err(e) => {
56            return Ok(mcp_error_response(format!("Failed to load config: {}", e)));
57        }
58    };
59
60    let all_specs = match load_all_specs(&specs_dir) {
61        Ok(specs) => specs,
62        Err(e) => {
63            return Ok(mcp_error_response(format!("Failed to load specs: {}", e)));
64        }
65    };
66
67    // Use operations module for finalization with full validation
68    let spec_repo = crate::repository::spec_repository::FileSpecRepository::new(specs_dir.clone());
69    let options = crate::operations::finalize::FinalizeOptions {
70        allow_no_commits: false,
71        commits: None, // Auto-detect commits
72        force,
73    };
74
75    match crate::operations::finalize::finalize_spec(
76        &mut spec, &spec_repo, &config, &all_specs, options,
77    ) {
78        Ok(_) => Ok(mcp_text_response(format!("Finalized spec: {}", spec_id))),
79        Err(e) => Ok(mcp_error_response(format!(
80            "Failed to finalize spec: {}",
81            e
82        ))),
83    }
84}
85
86pub fn tool_chant_reset(arguments: Option<&Value>) -> Result<Value> {
87    let specs_dir = match mcp_ensure_initialized() {
88        Ok(dir) => dir,
89        Err(err_response) => return Ok(err_response),
90    };
91
92    let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
93
94    let id = args
95        .get("id")
96        .and_then(|v| v.as_str())
97        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
98
99    let mut spec = match resolve_spec(&specs_dir, id) {
100        Ok(s) => s,
101        Err(e) => {
102            return Ok(mcp_error_response(e.to_string()));
103        }
104    };
105
106    let spec_id = spec.id.clone();
107    let spec_path = specs_dir.join(format!("{}.md", spec.id));
108
109    // Use operations module for reset
110    let options = crate::operations::reset::ResetOptions::default();
111
112    match crate::operations::reset::reset_spec(&mut spec, &spec_path, options) {
113        Ok(_) => Ok(mcp_text_response(format!(
114            "Reset spec '{}' to pending",
115            spec_id
116        ))),
117        Err(e) => Ok(mcp_error_response(format!("Failed to reset spec: {}", e))),
118    }
119}
120
121pub fn tool_chant_cancel(arguments: Option<&Value>) -> Result<Value> {
122    let specs_dir = match mcp_ensure_initialized() {
123        Ok(dir) => dir,
124        Err(err_response) => return Ok(err_response),
125    };
126
127    let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
128
129    let id = args
130        .get("id")
131        .and_then(|v| v.as_str())
132        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
133
134    let options = operations::CancelOptions::default();
135
136    match operations::cancel_spec(&specs_dir, id, &options) {
137        Ok(spec) => Ok(mcp_text_response(format!("Cancelled spec: {}", spec.id))),
138        Err(e) => Ok(mcp_error_response(e.to_string())),
139    }
140}
141
142pub fn tool_chant_archive(arguments: Option<&Value>) -> Result<Value> {
143    let specs_dir = match mcp_ensure_initialized() {
144        Ok(dir) => dir,
145        Err(err_response) => return Ok(err_response),
146    };
147
148    let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
149
150    let id = args
151        .get("id")
152        .and_then(|v| v.as_str())
153        .ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
154
155    let options = operations::ArchiveOptions::default();
156
157    match operations::archive_spec(&specs_dir, id, &options) {
158        Ok(dest_path) => Ok(mcp_text_response(format!(
159            "Archived spec: {} -> {}",
160            id,
161            dest_path.display()
162        ))),
163        Err(e) => Ok(mcp_error_response(e.to_string())),
164    }
165}