mcp_execution_skill/types.rs
1//! Type definitions for skill generation.
2//!
3//! This module defines all parameter and result types for skill generation:
4//! - `GenerateSkillParams`: Parameters for generating a skill
5//! - `GenerateSkillResult`: Result from skill generation
6//! - `SaveSkillParams`: Parameters for saving a skill
7//! - `SaveSkillResult`: Result from saving a skill
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13/// Maximum `server_id` length (denial-of-service protection).
14const MAX_SERVER_ID_LENGTH: usize = 64;
15
16// ============================================================================
17// generate_skill types
18// ============================================================================
19
20/// Parameters for generating a skill.
21///
22/// # Examples
23///
24/// ```
25/// use mcp_execution_skill::types::GenerateSkillParams;
26///
27/// let params = GenerateSkillParams {
28/// server_id: "github".to_string(),
29/// servers_dir: None,
30/// skill_name: None,
31/// use_case_hints: None,
32/// };
33/// ```
34#[derive(Debug, Clone, Deserialize, JsonSchema)]
35pub struct GenerateSkillParams {
36 /// Server identifier (e.g., "github").
37 ///
38 /// Must contain only lowercase letters, digits, and hyphens.
39 pub server_id: String,
40
41 /// Base directory for generated servers.
42 ///
43 /// Default: `~/.claude/servers`
44 pub servers_dir: Option<PathBuf>,
45
46 /// Custom skill name.
47 ///
48 /// Default: `{server_id}-progressive`
49 pub skill_name: Option<String>,
50
51 /// Additional context about intended use cases.
52 ///
53 /// Helps generate more relevant documentation.
54 pub use_case_hints: Option<Vec<String>>,
55}
56
57/// Result from `generate_skill` tool.
58///
59/// Contains all context Claude needs to generate optimal SKILL.md content.
60#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
61pub struct GenerateSkillResult {
62 /// Server identifier.
63 pub server_id: String,
64
65 /// Suggested skill name.
66 pub skill_name: String,
67
68 /// Server description (inferred from tools).
69 pub server_description: Option<String>,
70
71 /// Tools grouped by category.
72 pub categories: Vec<SkillCategory>,
73
74 /// Total tool count.
75 pub tool_count: usize,
76
77 /// Example tool usages (for documentation).
78 pub example_tools: Vec<ToolExample>,
79
80 /// Prompt template for skill generation.
81 ///
82 /// Claude uses this prompt to generate SKILL.md content.
83 pub generation_prompt: String,
84
85 /// Output path for the skill file.
86 pub output_path: String,
87
88 /// Non-fatal drift warnings, e.g. `.ts` files on disk excluded from
89 /// `categories`/`tool_count` because `_meta.json` has no matching entry
90 /// for them. Empty when the scanned directory has no drift.
91 #[serde(default)]
92 pub warnings: Vec<String>,
93}
94
95/// A category of tools for the skill.
96#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
97pub struct SkillCategory {
98 /// Category name (e.g., "issues", "repositories").
99 pub name: String,
100
101 /// Human-readable display name.
102 pub display_name: String,
103
104 /// Tools in this category.
105 pub tools: Vec<SkillTool>,
106}
107
108/// Tool information for skill generation.
109#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
110pub struct SkillTool {
111 /// Original tool name.
112 pub name: String,
113
114 /// TypeScript function name.
115 pub typescript_name: String,
116
117 /// Short description.
118 pub description: String,
119
120 /// Keywords for discovery.
121 pub keywords: Vec<String>,
122
123 /// Required parameters.
124 pub required_params: Vec<String>,
125
126 /// Optional parameters.
127 pub optional_params: Vec<String>,
128}
129
130/// Example tool usage for documentation.
131#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
132pub struct ToolExample {
133 /// Tool name.
134 pub tool_name: String,
135
136 /// Natural language description of what this does.
137 pub description: String,
138
139 /// Example CLI command.
140 pub cli_command: String,
141
142 /// Example parameters as JSON.
143 pub params_json: String,
144}
145
146// ============================================================================
147// save_skill types
148// ============================================================================
149
150/// Parameters for saving a skill.
151///
152/// # Examples
153///
154/// ```
155/// use mcp_execution_skill::types::SaveSkillParams;
156///
157/// let params = SaveSkillParams {
158/// server_id: "github".to_string(),
159/// content: "---\nname: github\n---\n# GitHub".to_string(),
160/// output_path: None,
161/// overwrite: false,
162/// };
163/// ```
164#[derive(Debug, Clone, Deserialize, JsonSchema)]
165pub struct SaveSkillParams {
166 /// Server identifier.
167 pub server_id: String,
168
169 /// SKILL.md content (markdown with YAML frontmatter).
170 pub content: String,
171
172 /// Custom output path.
173 ///
174 /// Default: `~/.claude/skills/{server_id}/SKILL.md`
175 pub output_path: Option<PathBuf>,
176
177 /// Overwrite if exists.
178 #[serde(default)]
179 pub overwrite: bool,
180}
181
182/// Result from saving a skill.
183#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
184pub struct SaveSkillResult {
185 /// Whether save was successful.
186 pub success: bool,
187
188 /// Path where skill was saved.
189 pub output_path: String,
190
191 /// Whether an existing file was overwritten.
192 pub overwritten: bool,
193
194 /// Skill metadata extracted from content.
195 pub metadata: SkillMetadata,
196}
197
198/// Metadata extracted from saved skill.
199#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
200pub struct SkillMetadata {
201 /// Skill name from frontmatter.
202 pub name: String,
203
204 /// Description from frontmatter.
205 pub description: String,
206
207 /// Section count (H2 headers).
208 pub section_count: usize,
209
210 /// Approximate word count.
211 pub word_count: usize,
212}
213
214// ============================================================================
215// Validation functions
216// ============================================================================
217
218/// Validate `server_id` format and length.
219///
220/// # Arguments
221///
222/// * `server_id` - Server identifier to validate
223///
224/// # Returns
225///
226/// `Ok(())` if valid.
227///
228/// # Errors
229///
230/// Returns `Err` with descriptive message if:
231/// - Length exceeds 64 characters
232/// - Contains characters other than lowercase letters, digits, and hyphens
233///
234/// # Validation Rules
235///
236/// - Length must not exceed 64 characters
237/// - Must contain only lowercase letters, digits, and hyphens
238///
239/// # Examples
240///
241/// ```
242/// use mcp_execution_skill::validate_server_id;
243///
244/// assert!(validate_server_id("github").is_ok());
245/// assert!(validate_server_id("my-server-123").is_ok());
246/// assert!(validate_server_id("GitHub").is_err()); // uppercase
247/// assert!(validate_server_id("my_server").is_err()); // underscore
248/// ```
249pub fn validate_server_id(server_id: &str) -> Result<(), String> {
250 // Check length
251 if server_id.len() > MAX_SERVER_ID_LENGTH {
252 return Err(format!(
253 "server_id too long: {} chars exceeds {} limit",
254 server_id.len(),
255 MAX_SERVER_ID_LENGTH
256 ));
257 }
258
259 // Check format
260 if !server_id
261 .chars()
262 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
263 {
264 return Err(
265 "server_id must contain only lowercase letters, digits, and hyphens".to_string(),
266 );
267 }
268
269 Ok(())
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_validate_server_id_valid() {
278 assert!(validate_server_id("github").is_ok());
279 assert!(validate_server_id("my-server").is_ok());
280 assert!(validate_server_id("server123").is_ok());
281 assert!(validate_server_id("my-server-123").is_ok());
282 }
283
284 #[test]
285 fn test_validate_server_id_uppercase() {
286 let result = validate_server_id("GitHub");
287 assert!(result.is_err());
288 assert!(result.unwrap_err().contains("lowercase"));
289 }
290
291 #[test]
292 fn test_validate_server_id_underscore() {
293 let result = validate_server_id("my_server");
294 assert!(result.is_err());
295 assert!(result.unwrap_err().contains("lowercase"));
296 }
297
298 #[test]
299 fn test_validate_server_id_special_chars() {
300 let result = validate_server_id("my@server");
301 assert!(result.is_err());
302 assert!(result.unwrap_err().contains("lowercase"));
303 }
304
305 #[test]
306 fn test_validate_server_id_too_long() {
307 let long_id = "a".repeat(65);
308 let result = validate_server_id(&long_id);
309 assert!(result.is_err());
310 assert!(result.unwrap_err().contains("too long"));
311 }
312
313 #[test]
314 fn test_validate_server_id_max_length() {
315 let max_id = "a".repeat(64);
316 assert!(validate_server_id(&max_id).is_ok());
317 }
318}