Skip to main content

subcog/mcp/tools/handlers/
context_templates.rs

1//! Context template tool execution handlers.
2//!
3//! Contains handlers for context template management operations:
4//! save, list, get, render, delete.
5//!
6//! Provides both consolidated (`execute_templates`) and legacy handlers.
7
8use 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
24/// Parses output format from string.
25fn 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
34/// Formats a field value for display, returning "(none)" if empty.
35fn 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
43/// Formats a list of items for display, returning "(none)" if empty.
44fn format_list_or_none(items: &[String]) -> String {
45    if items.is_empty() {
46        "(none)".to_string()
47    } else {
48        items.join(", ")
49    }
50}
51
52/// Executes the `context_template_save` tool.
53pub 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    // Parse domain scope
58    let domain = parse_domain_scope(args.domain.as_deref());
59
60    // Parse output format
61    let output_format = args
62        .output_format
63        .as_deref()
64        .and_then(parse_output_format)
65        .unwrap_or(OutputFormat::Markdown);
66
67    // Build template
68    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    // Convert variable args to TemplateVariable
80    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                // Set required - with_default sets required to false
92                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    // Create service and save
102    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
131/// Executes the `context_template_list` tool.
132pub 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    // Parse domain scope
137    let domain = args.domain.as_deref().map(|s| parse_domain_scope(Some(s)));
138
139    // Build filter
140    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    // Create service and list
157    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    // Format results
170    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
195/// Executes the `context_template_get` tool.
196pub 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    // Parse domain scope
201    let domain = args.domain.as_deref().map(|s| parse_domain_scope(Some(s)));
202
203    // Create service and get
204    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
279/// Executes the `context_template_render` tool.
280pub 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    // Parse output format override
285    let format_override = args.format.as_deref().and_then(parse_output_format);
286
287    // Get memories via search if query provided
288    let (memories, statistics) = if let Some(query) = &args.query {
289        // Get service container for recall
290        let services = ServiceContainer::from_current_dir_or_user()?;
291        let recall_service = services.recall()?;
292
293        // Build search filter
294        let limit = args.limit.unwrap_or(10) as usize;
295        let mut filter = SearchFilter::new();
296
297        // Add namespace filter if provided
298        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        // Execute search
306        let results = recall_service.search(query, SearchMode::Hybrid, &filter, limit)?;
307
308        // Extract memories
309        let mems: Vec<crate::models::Memory> =
310            results.memories.into_iter().map(|r| r.memory).collect();
311
312        // Build statistics
313        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        // No query - use empty memories
330        (
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    // Get custom variables
342    let custom_vars = args.variables.unwrap_or_default();
343
344    // Create service and render
345    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
366/// Executes the `context_template_delete` tool.
367pub 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    // Parse domain scope
372    let domain = parse_domain_scope(Some(&args.domain));
373
374    // Create service and delete
375    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
409// =============================================================================
410// Consolidated Context Template Handler
411// =============================================================================
412
413/// Executes the consolidated `subcog_templates` tool.
414///
415/// Dispatches to the appropriate action handler based on the `action` field.
416/// Valid actions: save, list, get, render, delete.
417pub 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
434/// Handles the `save` action for `subcog_templates`.
435fn 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    // Parse domain scope
447    let domain = parse_domain_scope(args.domain.as_deref());
448
449    // Parse output format
450    let output_format = args
451        .output_format
452        .as_deref()
453        .and_then(parse_output_format)
454        .unwrap_or(OutputFormat::Markdown);
455
456    // Build template
457    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    // Convert variable args to TemplateVariable
469    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                // Set required - with_default sets required to false
481                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    // Create service and save
491    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
520/// Handles the `list` action for `subcog_templates`.
521fn execute_templates_list(args: &TemplatesArgs) -> Result<ToolResult> {
522    // Parse domain scope
523    let domain = args.domain.as_deref().map(|s| parse_domain_scope(Some(s)));
524
525    // Build filter
526    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    // Create service and list
543    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    // Format results
556    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
581/// Handles the `get` action for `subcog_templates`.
582fn 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    // Parse domain scope
589    let domain = args.domain.as_deref().map(|s| parse_domain_scope(Some(s)));
590
591    // Create service and get
592    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
667/// Handles the `render` action for `subcog_templates`.
668fn 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    // Parse output format override
675    let format_override = args.format.as_deref().and_then(parse_output_format);
676
677    // Get memories via search if query provided
678    let (memories, statistics) = if let Some(query) = &args.query {
679        // Get service container for recall
680        let services = ServiceContainer::from_current_dir_or_user()?;
681        let recall_service = services.recall()?;
682
683        // Build search filter
684        let limit = args.limit.unwrap_or(10) as usize;
685        let mut filter = SearchFilter::new();
686
687        // Add namespace filter if provided
688        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        // Execute search
696        let results = recall_service.search(query, SearchMode::Hybrid, &filter, limit)?;
697
698        // Extract memories
699        let mems: Vec<crate::models::Memory> =
700            results.memories.into_iter().map(|r| r.memory).collect();
701
702        // Build statistics
703        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        // No query - use empty memories
720        (
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    // Get custom variables
732    let custom_vars = args.variables.clone().unwrap_or_default();
733
734    // Create service and render
735    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
756/// Handles the `delete` action for `subcog_templates`.
757fn 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    // Parse domain scope
768    let domain = parse_domain_scope(Some(domain_str));
769
770    // Create service and delete
771    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}