1use std::collections::HashMap;
9
10use crate::mcp::tool_types::{
11 ContextTemplateDeleteArgs, ContextTemplateGetArgs, ContextTemplateListArgs,
12 ContextTemplateRenderArgs, ContextTemplateSaveArgs, TemplatesArgs, domain_scope_to_display,
13 parse_domain_scope,
14};
15use crate::models::{ContextTemplate, OutputFormat, SearchFilter, SearchMode, TemplateVariable};
16use crate::services::{
17 ContextTemplateFilter, ContextTemplateService, MemoryStatistics, ServiceContainer,
18};
19use crate::{Error, Result};
20use serde_json::Value;
21
22use super::super::{ToolContent, ToolResult};
23
24fn parse_output_format(s: &str) -> Option<OutputFormat> {
26 match s.to_lowercase().as_str() {
27 "markdown" | "md" => Some(OutputFormat::Markdown),
28 "json" => Some(OutputFormat::Json),
29 "xml" => Some(OutputFormat::Xml),
30 _ => None,
31 }
32}
33
34fn format_field_or_none(value: &str) -> String {
36 if value.is_empty() {
37 "(none)".to_string()
38 } else {
39 value.to_string()
40 }
41}
42
43fn format_list_or_none(items: &[String]) -> String {
45 if items.is_empty() {
46 "(none)".to_string()
47 } else {
48 items.join(", ")
49 }
50}
51
52pub fn execute_context_template_save(arguments: Value) -> Result<ToolResult> {
54 let args: ContextTemplateSaveArgs =
55 serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
56
57 let domain = parse_domain_scope(args.domain.as_deref());
59
60 let output_format = args
62 .output_format
63 .as_deref()
64 .and_then(parse_output_format)
65 .unwrap_or(OutputFormat::Markdown);
66
67 let mut template = ContextTemplate::new(&args.name, &args.content);
69 template.output_format = output_format;
70
71 if let Some(desc) = args.description {
72 template = template.with_description(desc);
73 }
74
75 if let Some(tags) = args.tags {
76 template = template.with_tags(tags);
77 }
78
79 if let Some(vars) = args.variables {
81 let variables: Vec<TemplateVariable> = vars
82 .into_iter()
83 .map(|v| {
84 let mut tv = TemplateVariable::new(&v.name);
85 if let Some(desc) = v.description {
86 tv = tv.with_description(desc);
87 }
88 if let Some(default) = v.default {
89 tv = tv.with_default(default);
90 }
91 if v.required.unwrap_or(true) && tv.default.is_none() {
93 tv.required = true;
94 }
95 tv
96 })
97 .collect();
98 template = template.with_variables(variables);
99 }
100
101 let mut service = ContextTemplateService::new();
103 let (name, version) = service.save(&template, domain)?;
104
105 let var_names: Vec<String> = template.variables.iter().map(|v| v.name.clone()).collect();
106
107 Ok(ToolResult {
108 content: vec![ToolContent::Text {
109 text: format!(
110 "Context template saved successfully!\n\n\
111 Name: {}\n\
112 Version: {}\n\
113 Domain: {}\n\
114 Format: {}\n\
115 Description: {}\n\
116 Tags: {}\n\
117 Variables: {}",
118 name,
119 version,
120 domain_scope_to_display(domain),
121 template.output_format,
122 format_field_or_none(&template.description),
123 format_list_or_none(&template.tags),
124 format_list_or_none(&var_names),
125 ),
126 }],
127 is_error: false,
128 })
129}
130
131pub fn execute_context_template_list(arguments: Value) -> Result<ToolResult> {
133 let args: ContextTemplateListArgs =
134 serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
135
136 let domain = args.domain.as_deref().map(|s| parse_domain_scope(Some(s)));
138
139 let mut filter = ContextTemplateFilter::new();
141 if let Some(d) = domain {
142 filter = filter.with_domain(d);
143 }
144 if let Some(tags) = args.tags {
145 filter = filter.with_tags(tags);
146 }
147 if let Some(pattern) = args.name_pattern {
148 filter = filter.with_name_pattern(pattern);
149 }
150 if let Some(limit) = args.limit {
151 filter = filter.with_limit(limit.min(100));
152 } else {
153 filter = filter.with_limit(20);
154 }
155
156 let mut service = ContextTemplateService::new();
158 let templates = service.list(&filter)?;
159
160 if templates.is_empty() {
161 return Ok(ToolResult {
162 content: vec![ToolContent::Text {
163 text: "No context templates found matching the criteria.".to_string(),
164 }],
165 is_error: false,
166 });
167 }
168
169 let mut lines = vec![format!("Found {} context template(s):\n", templates.len())];
171
172 for template in templates {
173 lines.push(format!(
174 "- **{}** (v{})\n {}\n Tags: {}\n Format: {}",
175 template.name,
176 template.version,
177 if template.description.is_empty() {
178 "(no description)"
179 } else {
180 &template.description
181 },
182 format_list_or_none(&template.tags),
183 template.output_format,
184 ));
185 }
186
187 Ok(ToolResult {
188 content: vec![ToolContent::Text {
189 text: lines.join("\n"),
190 }],
191 is_error: false,
192 })
193}
194
195pub fn execute_context_template_get(arguments: Value) -> Result<ToolResult> {
197 let args: ContextTemplateGetArgs =
198 serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
199
200 let domain = args.domain.as_deref().map(|s| parse_domain_scope(Some(s)));
202
203 let mut service = ContextTemplateService::new();
205 let template = service.get(&args.name, args.version, domain)?;
206
207 match template {
208 Some(t) => {
209 let var_info: Vec<String> = t
210 .variables
211 .iter()
212 .map(|v| {
213 let req = if v.required { "required" } else { "optional" };
214 let default = v
215 .default
216 .as_ref()
217 .map(|d| format!(", default: \"{d}\""))
218 .unwrap_or_default();
219 format!(
220 " - **{}** ({}{}){}",
221 v.name,
222 req,
223 default,
224 v.description
225 .as_ref()
226 .map(|d| format!(": {d}"))
227 .unwrap_or_default()
228 )
229 })
230 .collect();
231
232 let variables_section = if var_info.is_empty() {
233 "(none)".to_string()
234 } else {
235 format!("\n{}", var_info.join("\n"))
236 };
237
238 Ok(ToolResult {
239 content: vec![ToolContent::Text {
240 text: format!(
241 "# Context Template: {}\n\n\
242 **Version**: {}\n\
243 **Format**: {}\n\
244 **Description**: {}\n\
245 **Tags**: {}\n\
246 **Created**: {}\n\
247 **Updated**: {}\n\n\
248 ## Variables\n{}\n\n\
249 ## Content\n\n```\n{}\n```",
250 t.name,
251 t.version,
252 t.output_format,
253 format_field_or_none(&t.description),
254 format_list_or_none(&t.tags),
255 t.created_at,
256 t.updated_at,
257 variables_section,
258 t.content,
259 ),
260 }],
261 is_error: false,
262 })
263 },
264 None => Ok(ToolResult {
265 content: vec![ToolContent::Text {
266 text: format!(
267 "Context template '{}' not found{}.",
268 args.name,
269 args.version
270 .map(|v| format!(" (version {v})"))
271 .unwrap_or_default()
272 ),
273 }],
274 is_error: true,
275 }),
276 }
277}
278
279pub fn execute_context_template_render(arguments: Value) -> Result<ToolResult> {
281 let args: ContextTemplateRenderArgs =
282 serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
283
284 let format_override = args.format.as_deref().and_then(parse_output_format);
286
287 let (memories, statistics) = if let Some(query) = &args.query {
289 let services = ServiceContainer::from_current_dir_or_user()?;
291 let recall_service = services.recall()?;
292
293 let limit = args.limit.unwrap_or(10) as usize;
295 let mut filter = SearchFilter::new();
296
297 if let Some(ns_list) = &args.namespaces {
299 filter.namespaces = ns_list
300 .iter()
301 .map(|s| crate::mcp::tool_types::parse_namespace(s))
302 .collect();
303 }
304
305 let results = recall_service.search(query, SearchMode::Hybrid, &filter, limit)?;
307
308 let mems: Vec<crate::models::Memory> =
310 results.memories.into_iter().map(|r| r.memory).collect();
311
312 let mut namespace_counts: HashMap<String, usize> = HashMap::new();
314 for m in &mems {
315 *namespace_counts
316 .entry(m.namespace.as_str().to_string())
317 .or_insert(0) += 1;
318 }
319
320 let stats = MemoryStatistics {
321 total_count: mems.len(),
322 namespace_counts,
323 top_tags: vec![],
324 recent_topics: vec![],
325 };
326
327 (mems, stats)
328 } else {
329 (
331 vec![],
332 MemoryStatistics {
333 total_count: 0,
334 namespace_counts: HashMap::new(),
335 top_tags: vec![],
336 recent_topics: vec![],
337 },
338 )
339 };
340
341 let custom_vars = args.variables.unwrap_or_default();
343
344 let mut service = ContextTemplateService::new();
346 let result = service.render_with_memories(
347 &args.name,
348 args.version,
349 &memories,
350 &statistics,
351 &custom_vars,
352 format_override,
353 )?;
354
355 Ok(ToolResult {
356 content: vec![ToolContent::Text {
357 text: format!(
358 "# Rendered: {} (v{}, {})\n\n{}",
359 result.template_name, result.template_version, result.format, result.output
360 ),
361 }],
362 is_error: false,
363 })
364}
365
366pub fn execute_context_template_delete(arguments: Value) -> Result<ToolResult> {
368 let args: ContextTemplateDeleteArgs =
369 serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
370
371 let domain = parse_domain_scope(Some(&args.domain));
373
374 let mut service = ContextTemplateService::new();
376 let deleted = service.delete(&args.name, args.version, domain)?;
377
378 if deleted {
379 let version_info = args.version.map_or_else(
380 || " (all versions)".to_string(),
381 |v| format!(" (version {v})"),
382 );
383
384 Ok(ToolResult {
385 content: vec![ToolContent::Text {
386 text: format!(
387 "Context template '{}'{} deleted from {} scope.",
388 args.name,
389 version_info,
390 domain_scope_to_display(domain)
391 ),
392 }],
393 is_error: false,
394 })
395 } else {
396 Ok(ToolResult {
397 content: vec![ToolContent::Text {
398 text: format!(
399 "Context template '{}' not found in {} scope.",
400 args.name,
401 domain_scope_to_display(domain)
402 ),
403 }],
404 is_error: true,
405 })
406 }
407}
408
409pub fn execute_templates(arguments: Value) -> Result<ToolResult> {
418 let args: TemplatesArgs =
419 serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
420
421 match args.action.as_str() {
422 "save" => execute_templates_save(&args),
423 "list" => execute_templates_list(&args),
424 "get" => execute_templates_get(&args),
425 "render" => execute_templates_render(&args),
426 "delete" => execute_templates_delete(&args),
427 _ => Err(Error::InvalidInput(format!(
428 "Unknown templates action: '{}'. Valid actions: save, list, get, render, delete",
429 args.action
430 ))),
431 }
432}
433
434fn execute_templates_save(args: &TemplatesArgs) -> Result<ToolResult> {
436 let name = args
437 .name
438 .as_ref()
439 .ok_or_else(|| Error::InvalidInput("'name' is required for save action".to_string()))?;
440
441 let content = args
442 .content
443 .as_ref()
444 .ok_or_else(|| Error::InvalidInput("'content' is required for save action".to_string()))?;
445
446 let domain = parse_domain_scope(args.domain.as_deref());
448
449 let output_format = args
451 .output_format
452 .as_deref()
453 .and_then(parse_output_format)
454 .unwrap_or(OutputFormat::Markdown);
455
456 let mut template = ContextTemplate::new(name, content);
458 template.output_format = output_format;
459
460 if let Some(desc) = &args.description {
461 template = template.with_description(desc.clone());
462 }
463
464 if let Some(tags) = &args.tags {
465 template = template.with_tags(tags.clone());
466 }
467
468 if let Some(vars) = &args.variables_def {
470 let variables: Vec<TemplateVariable> = vars
471 .iter()
472 .map(|v| {
473 let mut tv = TemplateVariable::new(&v.name);
474 if let Some(desc) = &v.description {
475 tv = tv.with_description(desc.clone());
476 }
477 if let Some(default) = &v.default {
478 tv = tv.with_default(default.clone());
479 }
480 if v.required.unwrap_or(true) && tv.default.is_none() {
482 tv.required = true;
483 }
484 tv
485 })
486 .collect();
487 template = template.with_variables(variables);
488 }
489
490 let mut service = ContextTemplateService::new();
492 let (saved_name, version) = service.save(&template, domain)?;
493
494 let var_names: Vec<String> = template.variables.iter().map(|v| v.name.clone()).collect();
495
496 Ok(ToolResult {
497 content: vec![ToolContent::Text {
498 text: format!(
499 "Context template saved successfully!\n\n\
500 Name: {}\n\
501 Version: {}\n\
502 Domain: {}\n\
503 Format: {}\n\
504 Description: {}\n\
505 Tags: {}\n\
506 Variables: {}",
507 saved_name,
508 version,
509 domain_scope_to_display(domain),
510 template.output_format,
511 format_field_or_none(&template.description),
512 format_list_or_none(&template.tags),
513 format_list_or_none(&var_names),
514 ),
515 }],
516 is_error: false,
517 })
518}
519
520fn execute_templates_list(args: &TemplatesArgs) -> Result<ToolResult> {
522 let domain = args.domain.as_deref().map(|s| parse_domain_scope(Some(s)));
524
525 let mut filter = ContextTemplateFilter::new();
527 if let Some(d) = domain {
528 filter = filter.with_domain(d);
529 }
530 if let Some(tags) = &args.tags {
531 filter = filter.with_tags(tags.clone());
532 }
533 if let Some(pattern) = &args.name_pattern {
534 filter = filter.with_name_pattern(pattern.clone());
535 }
536 if let Some(limit) = args.limit {
537 filter = filter.with_limit(limit.min(100) as usize);
538 } else {
539 filter = filter.with_limit(20);
540 }
541
542 let mut service = ContextTemplateService::new();
544 let templates = service.list(&filter)?;
545
546 if templates.is_empty() {
547 return Ok(ToolResult {
548 content: vec![ToolContent::Text {
549 text: "No context templates found matching the criteria.".to_string(),
550 }],
551 is_error: false,
552 });
553 }
554
555 let mut lines = vec![format!("Found {} context template(s):\n", templates.len())];
557
558 for template in templates {
559 lines.push(format!(
560 "- **{}** (v{})\n {}\n Tags: {}\n Format: {}",
561 template.name,
562 template.version,
563 if template.description.is_empty() {
564 "(no description)"
565 } else {
566 &template.description
567 },
568 format_list_or_none(&template.tags),
569 template.output_format,
570 ));
571 }
572
573 Ok(ToolResult {
574 content: vec![ToolContent::Text {
575 text: lines.join("\n"),
576 }],
577 is_error: false,
578 })
579}
580
581fn execute_templates_get(args: &TemplatesArgs) -> Result<ToolResult> {
583 let name = args
584 .name
585 .as_ref()
586 .ok_or_else(|| Error::InvalidInput("'name' is required for get action".to_string()))?;
587
588 let domain = args.domain.as_deref().map(|s| parse_domain_scope(Some(s)));
590
591 let mut service = ContextTemplateService::new();
593 let template = service.get(name, args.version, domain)?;
594
595 match template {
596 Some(t) => {
597 let var_info: Vec<String> = t
598 .variables
599 .iter()
600 .map(|v| {
601 let req = if v.required { "required" } else { "optional" };
602 let default = v
603 .default
604 .as_ref()
605 .map(|d| format!(", default: \"{d}\""))
606 .unwrap_or_default();
607 format!(
608 " - **{}** ({}{}){}\n",
609 v.name,
610 req,
611 default,
612 v.description
613 .as_ref()
614 .map(|d| format!(": {d}"))
615 .unwrap_or_default()
616 )
617 })
618 .collect();
619
620 let variables_section = if var_info.is_empty() {
621 "(none)".to_string()
622 } else {
623 format!("\n{}", var_info.join(""))
624 };
625
626 Ok(ToolResult {
627 content: vec![ToolContent::Text {
628 text: format!(
629 "# Context Template: {}\n\n\
630 **Version**: {}\n\
631 **Format**: {}\n\
632 **Description**: {}\n\
633 **Tags**: {}\n\
634 **Created**: {}\n\
635 **Updated**: {}\n\n\
636 ## Variables\n{}\n\n\
637 ## Content\n\n```\n{}\n```",
638 t.name,
639 t.version,
640 t.output_format,
641 format_field_or_none(&t.description),
642 format_list_or_none(&t.tags),
643 t.created_at,
644 t.updated_at,
645 variables_section,
646 t.content,
647 ),
648 }],
649 is_error: false,
650 })
651 },
652 None => Ok(ToolResult {
653 content: vec![ToolContent::Text {
654 text: format!(
655 "Context template '{}' not found{}.",
656 name,
657 args.version
658 .map(|v| format!(" (version {v})"))
659 .unwrap_or_default()
660 ),
661 }],
662 is_error: true,
663 }),
664 }
665}
666
667fn execute_templates_render(args: &TemplatesArgs) -> Result<ToolResult> {
669 let name = args
670 .name
671 .as_ref()
672 .ok_or_else(|| Error::InvalidInput("'name' is required for render action".to_string()))?;
673
674 let format_override = args.format.as_deref().and_then(parse_output_format);
676
677 let (memories, statistics) = if let Some(query) = &args.query {
679 let services = ServiceContainer::from_current_dir_or_user()?;
681 let recall_service = services.recall()?;
682
683 let limit = args.limit.unwrap_or(10) as usize;
685 let mut filter = SearchFilter::new();
686
687 if let Some(ns_list) = &args.namespaces {
689 filter.namespaces = ns_list
690 .iter()
691 .map(|s| crate::mcp::tool_types::parse_namespace(s))
692 .collect();
693 }
694
695 let results = recall_service.search(query, SearchMode::Hybrid, &filter, limit)?;
697
698 let mems: Vec<crate::models::Memory> =
700 results.memories.into_iter().map(|r| r.memory).collect();
701
702 let mut namespace_counts: HashMap<String, usize> = HashMap::new();
704 for m in &mems {
705 *namespace_counts
706 .entry(m.namespace.as_str().to_string())
707 .or_insert(0) += 1;
708 }
709
710 let stats = MemoryStatistics {
711 total_count: mems.len(),
712 namespace_counts,
713 top_tags: vec![],
714 recent_topics: vec![],
715 };
716
717 (mems, stats)
718 } else {
719 (
721 vec![],
722 MemoryStatistics {
723 total_count: 0,
724 namespace_counts: HashMap::new(),
725 top_tags: vec![],
726 recent_topics: vec![],
727 },
728 )
729 };
730
731 let custom_vars = args.variables.clone().unwrap_or_default();
733
734 let mut service = ContextTemplateService::new();
736 let result = service.render_with_memories(
737 name,
738 args.version,
739 &memories,
740 &statistics,
741 &custom_vars,
742 format_override,
743 )?;
744
745 Ok(ToolResult {
746 content: vec![ToolContent::Text {
747 text: format!(
748 "# Rendered: {} (v{}, {})\n\n{}",
749 result.template_name, result.template_version, result.format, result.output
750 ),
751 }],
752 is_error: false,
753 })
754}
755
756fn execute_templates_delete(args: &TemplatesArgs) -> Result<ToolResult> {
758 let name = args
759 .name
760 .as_ref()
761 .ok_or_else(|| Error::InvalidInput("'name' is required for delete action".to_string()))?;
762
763 let domain_str = args.domain.as_ref().ok_or_else(|| {
764 Error::InvalidInput("'domain' is required for delete action (for safety)".to_string())
765 })?;
766
767 let domain = parse_domain_scope(Some(domain_str));
769
770 let mut service = ContextTemplateService::new();
772 let deleted = service.delete(name, args.version, domain)?;
773
774 if deleted {
775 let version_info = args.version.map_or_else(
776 || " (all versions)".to_string(),
777 |v| format!(" (version {v})"),
778 );
779
780 Ok(ToolResult {
781 content: vec![ToolContent::Text {
782 text: format!(
783 "Context template '{}'{} deleted from {} scope.",
784 name,
785 version_info,
786 domain_scope_to_display(domain)
787 ),
788 }],
789 is_error: false,
790 })
791 } else {
792 Ok(ToolResult {
793 content: vec![ToolContent::Text {
794 text: format!(
795 "Context template '{}' not found in {} scope.",
796 name,
797 domain_scope_to_display(domain)
798 ),
799 }],
800 is_error: true,
801 })
802 }
803}