Skip to main content

mcp_execution_skill/
template.rs

1//! Template rendering for skill generation.
2//!
3//! Uses Handlebars templates to render the skill generation prompt.
4//! The template is embedded at compile time for reliability.
5
6use std::sync::LazyLock;
7
8use handlebars::Handlebars;
9use thiserror::Error;
10
11use crate::types::GenerateSkillResult;
12
13/// Errors that can occur during template rendering.
14#[derive(Debug, Error)]
15pub enum TemplateError {
16    /// Template rendering failed.
17    #[error("template rendering failed: {0}")]
18    RenderFailed(#[from] handlebars::RenderError),
19
20    /// Template registration failed.
21    #[error("template registration failed: {0}")]
22    RegistrationFailed(#[from] handlebars::TemplateError),
23}
24
25/// Embedded Handlebars template for skill generation.
26const SKILL_GENERATION_TEMPLATE: &str = include_str!("templates/skill-generation.hbs");
27
28/// Handlebars instance with pre-registered templates.
29///
30/// Initialized once per process using `LazyLock` for optimal performance.
31/// Template is parsed and validated on first access.
32static HANDLEBARS: LazyLock<Handlebars<'static>> = LazyLock::new(|| {
33    let mut hb = Handlebars::new();
34    hb.register_template_string("skill", SKILL_GENERATION_TEMPLATE)
35        .expect("embedded template must be valid Handlebars syntax");
36    hb
37});
38
39/// Render the skill generation prompt.
40///
41/// Takes the `GenerateSkillResult` context and renders it using
42/// the embedded Handlebars template.
43///
44/// # Arguments
45///
46/// * `context` - Skill generation context from `build_skill_context`
47///
48/// # Returns
49///
50/// Rendered prompt string for the LLM.
51///
52/// # Errors
53///
54/// Returns `TemplateError` if template rendering fails.
55///
56/// # Examples
57///
58/// ```no_run
59/// use mcp_execution_skill::{build_skill_context, render_generation_prompt};
60///
61/// let context = build_skill_context("github", &[], None);
62/// let prompt = render_generation_prompt(&context).unwrap();
63/// ```
64pub fn render_generation_prompt(context: &GenerateSkillResult) -> Result<String, TemplateError> {
65    // Use pre-compiled template instance
66    let rendered = HANDLEBARS.render("skill", context)?;
67    Ok(rendered)
68}
69
70/// Alternative: Use the pre-built prompt from context.
71///
72/// The `GenerateSkillResult` already contains a `generation_prompt` field
73/// built by `build_skill_context`. This function is provided for cases
74/// where custom template rendering is needed.
75#[allow(dead_code)]
76pub fn get_prebuilt_prompt(context: &GenerateSkillResult) -> &str {
77    &context.generation_prompt
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::types::{SkillCategory, SkillTool, ToolExample};
84
85    fn create_test_context() -> GenerateSkillResult {
86        GenerateSkillResult {
87            server_id: "test".to_string(),
88            skill_name: "test-progressive".to_string(),
89            server_description: Some("Test server".to_string()),
90            categories: vec![SkillCategory {
91                name: "test".to_string(),
92                display_name: "Test".to_string(),
93                tools: vec![SkillTool {
94                    name: "test_tool".to_string(),
95                    typescript_name: "testTool".to_string(),
96                    description: "Test tool description".to_string(),
97                    keywords: vec!["test".to_string()],
98                    required_params: vec!["param1".to_string()],
99                    optional_params: vec![],
100                }],
101            }],
102            tool_count: 1,
103            example_tools: vec![ToolExample {
104                tool_name: "test_tool".to_string(),
105                description: "Test tool".to_string(),
106                cli_command: "node test.ts".to_string(),
107                params_json: "{}".to_string(),
108            }],
109            generation_prompt: "Pre-built prompt".to_string(),
110            output_path: "~/.claude/skills/test/SKILL.md".to_string(),
111        }
112    }
113
114    #[test]
115    fn test_render_generation_prompt() {
116        let context = create_test_context();
117        let result = render_generation_prompt(&context);
118
119        match result {
120            Ok(prompt) => {
121                // Verify key sections are present
122                assert!(prompt.contains("test"));
123                assert!(prompt.contains("SKILL.md"));
124            }
125            Err(e) => panic!("Template rendering failed: {e}"),
126        }
127    }
128
129    #[test]
130    fn test_get_prebuilt_prompt() {
131        let context = create_test_context();
132        let prompt = get_prebuilt_prompt(&context);
133
134        assert_eq!(prompt, "Pre-built prompt");
135    }
136}