vcluster-yaml-mcp-server 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Pure tool handler functions
3
+ * Each handler is a pure function: input → output (no side effects except I/O)
4
+ * Easy to test in isolation
5
+ */
6
+
7
+ import { validateSnippet } from './snippet-validator.js';
8
+ import { extractValidationRulesFromComments } from './validation-rules.js';
9
+
10
+ // ============================================================================
11
+ // RESPONSE BUILDERS (Pure Functions)
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Build success response
16
+ * Pure function: same input → same output
17
+ */
18
+ export function buildSuccessResponse(text) {
19
+ return {
20
+ content: [{ type: 'text', text }],
21
+ isError: false
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Build error response
27
+ * Pure function: same input → same output
28
+ */
29
+ export function buildErrorResponse(text) {
30
+ return {
31
+ content: [{ type: 'text', text }],
32
+ isError: true
33
+ };
34
+ }
35
+
36
+ // ============================================================================
37
+ // FORMATTING (Pure Functions)
38
+ // ============================================================================
39
+
40
+ /**
41
+ * Format validation result to markdown
42
+ * Pure function - no side effects
43
+ */
44
+ export function formatValidationResult(result, {yaml_content, description, version}) {
45
+ let response = '';
46
+
47
+ if (description) {
48
+ response += `## ${description}\n\n`;
49
+ }
50
+
51
+ if (result.valid) {
52
+ response += `✅ **Configuration validated successfully!**\n\n`;
53
+ response += `Version: ${version}\n`;
54
+ if (result.section) {
55
+ response += `Section: ${result.section}\n`;
56
+ }
57
+ response += `Validation time: ${result.elapsed_ms}ms\n\n`;
58
+ response += `### Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\`\n`;
59
+ } else {
60
+ response += `❌ **Validation failed**\n\n`;
61
+
62
+ if (result.syntax_valid === false) {
63
+ response += `**Syntax Error:**\n${result.syntax_error}\n\n`;
64
+ } else if (result.errors && result.errors.length > 0) {
65
+ response += `**Validation Errors:**\n`;
66
+ result.errors.forEach((err, idx) => {
67
+ response += `${idx + 1}. **${err.path}**: ${err.message}\n`;
68
+ });
69
+ response += `\n`;
70
+ } else if (result.error) {
71
+ response += `**Error:** ${result.error}\n\n`;
72
+ if (result.hint) {
73
+ response += `**Hint:** ${result.hint}\n\n`;
74
+ }
75
+ }
76
+ response += `### Provided Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\`\n`;
77
+ }
78
+
79
+ return response;
80
+ }
81
+
82
+ /**
83
+ * Format versions list
84
+ * Pure function
85
+ */
86
+ export function formatVersionsList(versions) {
87
+ const display = versions.slice(0, 20);
88
+ const more = versions.length > 20 ? `... and ${versions.length - 20} more\n` : '';
89
+
90
+ return `Available vCluster versions:\n\n${display.map(v => `- ${v}`).join('\n')}\n${more}`;
91
+ }
92
+
93
+ /**
94
+ * Format query results
95
+ * Pure function
96
+ */
97
+ export function formatQueryResults(results, { query, fileName, version, maxResults }) {
98
+ const limitedResults = results.slice(0, maxResults);
99
+ const hasMore = results.length > maxResults;
100
+
101
+ const formattedResults = limitedResults.map((item, idx) =>
102
+ formatMatch(item, idx, limitedResults.length)
103
+ );
104
+
105
+ return `Found ${results.length} match${results.length === 1 ? '' : 'es'} for "${query}" in ${fileName} (${version})\n\n` +
106
+ formattedResults.join('\n') +
107
+ (hasMore ? `\n\n... showing ${maxResults} of ${results.length} total matches` : '');
108
+ }
109
+
110
+ /**
111
+ * Format single match
112
+ * Pure function
113
+ */
114
+ function formatMatch(item, idx, total) {
115
+ const prefix = `**[${idx + 1}/${total}]** \`${item.path}\``;
116
+
117
+ if (item.isLeaf) {
118
+ const valueStr = JSON.stringify(item.value, null, 2);
119
+ return `${prefix}\n\`\`\`\n${valueStr}\n\`\`\`\n`;
120
+ } else {
121
+ const keys = Object.keys(item.value || {});
122
+ return `${prefix}\n Contains: ${keys.join(', ')}\n`;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Format no-match message
128
+ * Pure function
129
+ */
130
+ export function formatNoMatches({ query, fileName, version, similarPaths, yamlData }) {
131
+ return `No matches found for "${query}" in ${fileName} (${version}).\n\n` +
132
+ (similarPaths.length > 0 ? `Similar paths:\n${similarPaths.map(p => ` - ${p}`).join('\n')}\n\n` : '') +
133
+ `Tips:\n` +
134
+ ` - Use dot notation: "controlPlane.ingress.enabled"\n` +
135
+ ` - Try broader terms: "${query.split('.')[0] || query.split(/\s+/)[0]}"\n` +
136
+ ` - Use extract-validation-rules for section details\n\n` +
137
+ `Top-level sections:\n${Object.keys(yamlData || {}).map(k => ` - ${k}`).join('\n')}`;
138
+ }
139
+
140
+ // ============================================================================
141
+ // TOOL HANDLERS (Each is a pure async function)
142
+ // ============================================================================
143
+
144
+ /**
145
+ * Handle: create-vcluster-config
146
+ * Pure function except for I/O (githubClient)
147
+ */
148
+ export async function handleCreateConfig(args, githubClient) {
149
+ const { yaml_content, description, version = 'main' } = args;
150
+
151
+ const schemaContent = await githubClient.getFileContent('chart/values.schema.json', version);
152
+ const fullSchema = JSON.parse(schemaContent);
153
+
154
+ const validationResult = validateSnippet(
155
+ yaml_content,
156
+ fullSchema,
157
+ version,
158
+ null // Auto-detect section
159
+ );
160
+
161
+ const formattedResponse = formatValidationResult(validationResult, {
162
+ yaml_content,
163
+ description,
164
+ version
165
+ });
166
+
167
+ return {
168
+ content: [{ type: 'text', text: formattedResponse }],
169
+ isError: !validationResult.valid
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Handle: list-versions
175
+ * Pure function except for I/O
176
+ */
177
+ export async function handleListVersions(args, githubClient) {
178
+ const tags = await githubClient.getTags();
179
+ const versionTags = tags.filter(tag => tag.startsWith('v'));
180
+ const versions = ['main', ...versionTags];
181
+
182
+ const formatted = formatVersionsList(versions);
183
+ return buildSuccessResponse(formatted);
184
+ }
185
+
186
+ /**
187
+ * Handle: smart-query
188
+ * Pure function except for I/O
189
+ */
190
+ export async function handleSmartQuery(args, githubClient) {
191
+ const { query, version = 'main', file = 'chart/values.yaml' } = args;
192
+
193
+ const yamlData = await githubClient.getYamlContent(file, version);
194
+ const searchTerm = query.toLowerCase();
195
+
196
+ // Extract all paths (pure function)
197
+ const allInfo = extractYamlInfo(yamlData);
198
+
199
+ // Search (pure function)
200
+ const results = searchYaml(allInfo, searchTerm);
201
+
202
+ // Handle no matches
203
+ if (results.length === 0) {
204
+ const similarPaths = findSimilarPaths(allInfo, searchTerm);
205
+ const formatted = formatNoMatches({ query, fileName: file, version, similarPaths, yamlData });
206
+ return buildSuccessResponse(formatted);
207
+ }
208
+
209
+ // Sort by relevance (pure function)
210
+ const sorted = sortByRelevance(results, searchTerm);
211
+
212
+ // Format results
213
+ const formatted = formatQueryResults(sorted, {
214
+ query,
215
+ fileName: file,
216
+ version,
217
+ maxResults: 50
218
+ });
219
+
220
+ return buildSuccessResponse(formatted);
221
+ }
222
+
223
+ /**
224
+ * Handle: extract-validation-rules
225
+ * Pure function except for I/O
226
+ */
227
+ export async function handleExtractRules(args, githubClient) {
228
+ const { version = 'main', file = 'chart/values.yaml', section } = args;
229
+
230
+ const content = await githubClient.getFileContent(file, version);
231
+ const rules = extractValidationRulesFromComments(content, section);
232
+
233
+ return buildSuccessResponse(JSON.stringify(rules, null, 2));
234
+ }
235
+
236
+ /**
237
+ * Handle: validate-config
238
+ * Pure function except for I/O
239
+ */
240
+ export async function handleValidateConfig(args, githubClient) {
241
+ const { version = 'main', content, file } = args;
242
+
243
+ // Get YAML content
244
+ let yamlContent;
245
+ if (content) {
246
+ yamlContent = content;
247
+ } else if (file) {
248
+ yamlContent = await githubClient.getFileContent(file, version);
249
+ } else {
250
+ yamlContent = await githubClient.getFileContent('chart/values.yaml', version);
251
+ }
252
+
253
+ // Get schema
254
+ const schemaContent = await githubClient.getFileContent('chart/values.schema.json', version);
255
+ const schema = JSON.parse(schemaContent);
256
+
257
+ // Validate (pure function)
258
+ const result = validateSnippet(yamlContent, schema, version, null);
259
+
260
+ return buildSuccessResponse(JSON.stringify(result, null, 2));
261
+ }
262
+
263
+ // ============================================================================
264
+ // QUERY HELPERS (Pure Functions)
265
+ // ============================================================================
266
+
267
+ /**
268
+ * Extract all paths and values from YAML
269
+ * Pure function
270
+ */
271
+ export function extractYamlInfo(obj, path = '') {
272
+ const info = [];
273
+
274
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
275
+ for (const [key, value] of Object.entries(obj)) {
276
+ const currentPath = path ? `${path}.${key}` : key;
277
+
278
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
279
+ info.push({ path: currentPath, key, value, isLeaf: false });
280
+ info.push(...extractYamlInfo(value, currentPath));
281
+ } else {
282
+ info.push({ path: currentPath, key, value, isLeaf: true });
283
+ }
284
+ }
285
+ }
286
+
287
+ return info;
288
+ }
289
+
290
+ /**
291
+ * Search YAML info for query
292
+ * Pure function
293
+ */
294
+ export function searchYaml(allInfo, searchTerm) {
295
+ const results = [];
296
+ const isDotNotation = searchTerm.includes('.');
297
+
298
+ if (isDotNotation) {
299
+ // Exact and partial dot notation matching
300
+ for (const item of allInfo) {
301
+ const pathLower = item.path.toLowerCase();
302
+
303
+ if (pathLower === searchTerm) {
304
+ results.push(item);
305
+ } else if (pathLower.endsWith(searchTerm)) {
306
+ results.push(item);
307
+ } else if (pathLower.includes(searchTerm)) {
308
+ results.push(item);
309
+ }
310
+ }
311
+ } else {
312
+ // Keyword-based search
313
+ const keywords = searchTerm.split(/\s+/);
314
+
315
+ for (const item of allInfo) {
316
+ const pathLower = item.path.toLowerCase();
317
+ const keyLower = item.key.toLowerCase();
318
+ const valueStr = JSON.stringify(item.value).toLowerCase();
319
+
320
+ const allKeywordsMatch = keywords.every(kw =>
321
+ pathLower.includes(kw) || keyLower.includes(kw) || valueStr.includes(kw)
322
+ );
323
+
324
+ if (allKeywordsMatch) {
325
+ results.push(item);
326
+ }
327
+ }
328
+ }
329
+
330
+ return results;
331
+ }
332
+
333
+ /**
334
+ * Find similar paths
335
+ * Pure function
336
+ */
337
+ export function findSimilarPaths(allInfo, searchTerm) {
338
+ return allInfo
339
+ .filter(item => {
340
+ const pathParts = item.path.toLowerCase().split('.');
341
+ return pathParts.some(part => part.includes(searchTerm) || searchTerm.includes(part));
342
+ })
343
+ .slice(0, 5)
344
+ .map(item => item.path);
345
+ }
346
+
347
+ /**
348
+ * Sort results by relevance
349
+ * Pure function
350
+ */
351
+ export function sortByRelevance(results, searchTerm) {
352
+ return [...results].sort((a, b) => {
353
+ const scoreA = rankResult(a, searchTerm);
354
+ const scoreB = rankResult(b, searchTerm);
355
+ return scoreB - scoreA;
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Rank search result
361
+ * Pure function
362
+ */
363
+ function rankResult(item, searchTerm) {
364
+ let score = 0;
365
+ const pathLower = item.path.toLowerCase();
366
+ const keyLower = item.key.toLowerCase();
367
+
368
+ // Exact path match (highest priority)
369
+ if (pathLower === searchTerm) score += 100;
370
+
371
+ // Exact key match
372
+ if (keyLower === searchTerm) score += 50;
373
+
374
+ // Path contains exact term
375
+ if (pathLower.includes(searchTerm)) score += 20;
376
+
377
+ // Key contains term
378
+ if (keyLower.includes(searchTerm)) score += 10;
379
+
380
+ // Leaf nodes are more relevant
381
+ if (item.isLeaf) score += 5;
382
+
383
+ return score;
384
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Tool Registry (Strategy Pattern)
3
+ * Maps tool names to handler functions
4
+ * Complexity: 1 (just a lookup, no conditionals)
5
+ */
6
+
7
+ import {
8
+ handleCreateConfig,
9
+ handleListVersions,
10
+ handleSmartQuery,
11
+ handleExtractRules,
12
+ handleValidateConfig,
13
+ buildErrorResponse
14
+ } from './tool-handlers.js';
15
+
16
+ /**
17
+ * Tool registry - Strategy pattern instead of switch statement
18
+ * Pure object mapping: tool name → handler function
19
+ */
20
+ export const toolHandlers = {
21
+ 'create-vcluster-config': handleCreateConfig,
22
+ 'list-versions': handleListVersions,
23
+ 'smart-query': handleSmartQuery,
24
+ 'extract-validation-rules': handleExtractRules,
25
+ 'validate-config': handleValidateConfig
26
+ };
27
+
28
+ /**
29
+ * Execute tool handler
30
+ * Complexity: 2 (single if statement)
31
+ * Pure function except for handler execution
32
+ */
33
+ export async function executeToolHandler(toolName, args, githubClient) {
34
+ const handler = toolHandlers[toolName];
35
+
36
+ if (!handler) {
37
+ return buildErrorResponse(`Unknown tool: ${toolName}`);
38
+ }
39
+
40
+ try {
41
+ return await handler(args, githubClient);
42
+ } catch (error) {
43
+ return buildErrorResponse(`Error executing ${toolName}: ${error.message}`);
44
+ }
45
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Validation Rules Extraction
3
+ * Pure functions for parsing YAML comments and extracting validation rules
4
+ */
5
+
6
+ /**
7
+ * Extract validation rules from YAML comments
8
+ * Pure function except for complex internal state management
9
+ * Complexity: 23 → Will refactor separately
10
+ */
11
+ export function extractValidationRulesFromComments(yamlContent, section) {
12
+ const lines = yamlContent.split('\n');
13
+ const rules = [];
14
+ const enums = {};
15
+ const dependencies = [];
16
+ const defaults = {};
17
+
18
+ let currentPath = [];
19
+ let currentComments = [];
20
+ let indentStack = [0];
21
+
22
+ for (let i = 0; i < lines.length; i++) {
23
+ const line = lines[i];
24
+ const trimmedLine = line.trim();
25
+
26
+ // Skip empty lines
27
+ if (!trimmedLine) {
28
+ currentComments = [];
29
+ continue;
30
+ }
31
+
32
+ // Collect comments
33
+ if (trimmedLine.startsWith('#')) {
34
+ const comment = trimmedLine.substring(1).trim();
35
+ if (comment && !comment.startsWith('#')) {
36
+ currentComments.push(comment);
37
+ }
38
+ continue;
39
+ }
40
+
41
+ // Parse YAML structure
42
+ const indent = line.search(/\S/);
43
+ const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)?$/);
44
+
45
+ if (keyMatch) {
46
+ const key = keyMatch[2];
47
+ const value = keyMatch[3];
48
+
49
+ // Update path based on indentation
50
+ while (indentStack.length > 1 && indent <= indentStack[indentStack.length - 1]) {
51
+ indentStack.pop();
52
+ currentPath.pop();
53
+ }
54
+
55
+ if (indent > indentStack[indentStack.length - 1]) {
56
+ indentStack.push(indent);
57
+ } else if (indent < indentStack[indentStack.length - 1]) {
58
+ while (indentStack.length > 1 && indent < indentStack[indentStack.length - 1]) {
59
+ indentStack.pop();
60
+ currentPath.pop();
61
+ }
62
+ } else {
63
+ currentPath.pop();
64
+ }
65
+
66
+ currentPath.push(key);
67
+ const fullPath = currentPath.join('.');
68
+
69
+ // Filter by section if specified
70
+ if (section && !fullPath.startsWith(section)) {
71
+ currentComments = [];
72
+ continue;
73
+ }
74
+
75
+ // Extract validation instructions from comments
76
+ if (currentComments.length > 0) {
77
+ const instructions = [];
78
+
79
+ for (const comment of currentComments) {
80
+ // Extract enum values (e.g., "Valid values: a, b, c")
81
+ const enumMatch = comment.match(/(?:valid values?|options?|choices?|possible values?):\s*(.+)/i);
82
+ if (enumMatch) {
83
+ const values = enumMatch[1].split(/[,;]/).map(v => v.trim()).filter(v => v);
84
+ enums[fullPath] = values;
85
+ instructions.push(`Valid values: ${values.join(', ')}`);
86
+ }
87
+
88
+ // Extract required dependencies
89
+ if (comment.match(/requires?|depends on|needs?/i)) {
90
+ dependencies.push(`${fullPath}: ${comment}`);
91
+ instructions.push(comment);
92
+ }
93
+
94
+ // Extract defaults
95
+ const defaultMatch = comment.match(/default(?:s)?\s*(?:is|:)?\s*(.+)/i);
96
+ if (defaultMatch) {
97
+ defaults[fullPath] = defaultMatch[1].trim();
98
+ }
99
+
100
+ // Extract validation rules
101
+ if (comment.match(/must|should|cannot|only|at least|minimum|maximum|required/i)) {
102
+ instructions.push(comment);
103
+ }
104
+
105
+ // Extract warnings
106
+ if (comment.match(/warning|note|important|deprecated/i)) {
107
+ instructions.push(`⚠️ ${comment}`);
108
+ }
109
+ }
110
+
111
+ if (instructions.length > 0) {
112
+ rules.push({
113
+ path: fullPath,
114
+ instructions: instructions,
115
+ originalComments: currentComments
116
+ });
117
+ }
118
+ }
119
+
120
+ currentComments = [];
121
+ }
122
+ }
123
+
124
+ // Generate AI validation instructions
125
+ const aiInstructions = {
126
+ summary: `Extracted ${rules.length} validation rules from YAML comments`,
127
+ rules: rules,
128
+ enums: enums,
129
+ dependencies: dependencies,
130
+ defaults: defaults,
131
+ instructions: generateAiValidationInstructions(rules, enums, dependencies)
132
+ };
133
+
134
+ return aiInstructions;
135
+ }
136
+
137
+ /**
138
+ * Generate AI validation instructions
139
+ * Pure function - formats rules into markdown
140
+ */
141
+ function generateAiValidationInstructions(rules, enums, dependencies) {
142
+ let instructions = '### AI Validation Instructions\n\n';
143
+ instructions += 'Please validate the configuration using these rules extracted from comments:\n\n';
144
+
145
+ if (rules.length > 0) {
146
+ instructions += '#### Field-Specific Rules:\n';
147
+ rules.forEach(rule => {
148
+ instructions += `- **${rule.path}**:\n`;
149
+ rule.instructions.forEach(inst => {
150
+ instructions += ` - ${inst}\n`;
151
+ });
152
+ });
153
+ instructions += '\n';
154
+ }
155
+
156
+ if (Object.keys(enums).length > 0) {
157
+ instructions += '#### Enumeration Constraints:\n';
158
+ instructions += 'Ensure these fields only contain the specified values:\n';
159
+ Object.entries(enums).forEach(([field, values]) => {
160
+ instructions += `- ${field}: [${values.join(', ')}]\n`;
161
+ });
162
+ instructions += '\n';
163
+ }
164
+
165
+ if (dependencies.length > 0) {
166
+ instructions += '#### Dependencies to Check:\n';
167
+ dependencies.forEach(dep => {
168
+ instructions += `- ${dep}\n`;
169
+ });
170
+ instructions += '\n';
171
+ }
172
+
173
+ instructions += '#### Validation Approach:\n';
174
+ instructions += '1. Check if all enumeration constraints are satisfied\n';
175
+ instructions += '2. Verify all dependency requirements are met\n';
176
+ instructions += '3. Validate against the specific rules for each field\n';
177
+ instructions += '4. Flag any deprecated fields or configurations\n';
178
+ instructions += '5. Provide helpful suggestions for fixing any issues found\n';
179
+
180
+ return instructions;
181
+ }