chant/mcp/tools/
lifecycle.rs1use 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 match spec.frontmatter.status {
45 SpecStatus::Completed | SpecStatus::InProgress | SpecStatus::Failed => {
46 }
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 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 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 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, 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 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}