vcluster-yaml-mcp-server 0.1.0

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.
package/src/server.js ADDED
@@ -0,0 +1,1006 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
+ import yaml from 'js-yaml';
4
+ import jq from 'node-jq';
5
+ import { githubClient } from './github.js';
6
+ import { validateSnippet } from './snippet-validator.js';
7
+
8
+ // Helper function to get the type of a value
9
+ function getType(value) {
10
+ if (value === null) return 'null';
11
+ if (Array.isArray(value)) return 'array';
12
+ if (typeof value === 'object') return 'object';
13
+ if (typeof value === 'number') {
14
+ return Number.isInteger(value) ? 'integer' : 'number';
15
+ }
16
+ return typeof value; // 'string', 'boolean'
17
+ }
18
+
19
+ // Helper function to format values for display
20
+ function formatValue(value, path, indent = 0) {
21
+ const spaces = ' '.repeat(indent);
22
+
23
+ // Primitives (string, number, boolean, null)
24
+ if (value === null) return 'null';
25
+ if (typeof value !== 'object') return String(value);
26
+
27
+ // Arrays
28
+ if (Array.isArray(value)) {
29
+ if (value.length === 0) return '[]';
30
+ if (value.length <= 5 && value.every(v => typeof v !== 'object')) {
31
+ // Small array of primitives - inline
32
+ return JSON.stringify(value);
33
+ }
34
+ // Multi-line array
35
+ return '\n' + value.map(v =>
36
+ `${spaces} - ${formatValue(v, path, indent + 1)}`
37
+ ).join('\n');
38
+ }
39
+
40
+ // Objects
41
+ const keys = Object.keys(value);
42
+ if (keys.length === 0) return '{}';
43
+
44
+ // Small object (≤5 fields) - show all fields
45
+ if (keys.length <= 5) {
46
+ return '\n' + keys.map(key =>
47
+ `${spaces} ${key}: ${formatValue(value[key], `${path}.${key}`, indent + 1)}`
48
+ ).join('\n');
49
+ }
50
+
51
+ // Large object - show structure only
52
+ return `\n${spaces} {object with ${keys.length} fields}`;
53
+ }
54
+
55
+ // Helper function to get field hints for common field names
56
+ function getFieldHint(fieldName) {
57
+ // Common field patterns and their hints
58
+ const hints = {
59
+ 'resources': 'Resource limits and requests',
60
+ 'replicas': 'Number of replicas for HA',
61
+ 'affinity': 'Pod affinity rules',
62
+ 'tolerations': 'Pod toleration settings',
63
+ 'nodeSelector': 'Node selection constraints',
64
+ 'image': 'Container image configuration',
65
+ 'enabled': 'Enable/disable this feature',
66
+ 'annotations': 'Kubernetes annotations',
67
+ 'labels': 'Kubernetes labels',
68
+ 'ingress': 'Ingress configuration',
69
+ 'service': 'Service configuration',
70
+ 'storage': 'Storage configuration',
71
+ 'persistence': 'Persistent volume settings',
72
+ 'sync': 'Resource sync configuration',
73
+ 'networking': 'Network settings'
74
+ };
75
+
76
+ return hints[fieldName] || '';
77
+ }
78
+
79
+ // Helper function to find related configs for a given item
80
+ function findRelatedConfigs(item, allInfo) {
81
+ const related = [];
82
+ const pathParts = item.path.split('.');
83
+ const lastKey = pathParts[pathParts.length - 1];
84
+ const parentPath = pathParts.slice(0, -1).join('.');
85
+
86
+ // Strategy 1: Find sibling fields (same parent path)
87
+ const siblings = allInfo.filter(info => {
88
+ const infoParent = info.path.split('.').slice(0, -1).join('.');
89
+ return infoParent === parentPath &&
90
+ info.path !== item.path &&
91
+ typeof info.value === 'object' &&
92
+ !Array.isArray(info.value);
93
+ });
94
+
95
+ // Add up to 2 sibling configs
96
+ siblings.slice(0, 2).forEach(sibling => {
97
+ const siblingKey = sibling.path.split('.').pop();
98
+ related.push({
99
+ path: sibling.path,
100
+ hint: getFieldHint(siblingKey)
101
+ });
102
+ });
103
+
104
+ // Strategy 2: Find same key name elsewhere (commonly configured together)
105
+ if (related.length < 3) {
106
+ const sameKeyElsewhere = allInfo.filter(info => {
107
+ const infoKey = info.path.split('.').pop();
108
+ return infoKey === lastKey &&
109
+ info.path !== item.path &&
110
+ !info.path.startsWith(item.path) && // Not a child
111
+ typeof info.value === 'object' &&
112
+ !Array.isArray(info.value);
113
+ });
114
+
115
+ // Add up to 1 same-key config from different section
116
+ sameKeyElsewhere.slice(0, 1).forEach(same => {
117
+ const section = same.path.split('.')[0];
118
+ related.push({
119
+ path: same.path,
120
+ hint: `${lastKey} in ${section} section`
121
+ });
122
+ });
123
+ }
124
+
125
+ return related.slice(0, 3); // Max 3 related configs
126
+ }
127
+
128
+ // Helper function to format a single match result
129
+ function formatMatch(item, index, total, allInfo) {
130
+ const separator = '━'.repeat(60);
131
+ let output = [];
132
+
133
+ if (index > 0) output.push(''); // Blank line between matches
134
+ output.push(separator);
135
+ output.push('');
136
+ output.push(`MATCH: ${item.path}`);
137
+ output.push(`TYPE: ${getType(item.value)}`);
138
+
139
+ // For primitives and simple values
140
+ if (typeof item.value !== 'object' || item.value === null) {
141
+ output.push(`VALUE: ${item.value}`);
142
+ return output.join('\n');
143
+ }
144
+
145
+ // For arrays
146
+ if (Array.isArray(item.value)) {
147
+ if (item.value.length === 0) {
148
+ output.push('VALUE: []');
149
+ } else {
150
+ output.push(`VALUE: ${formatValue(item.value, item.path)}`);
151
+ }
152
+ return output.join('\n');
153
+ }
154
+
155
+ // For objects - show fields
156
+ const keys = Object.keys(item.value);
157
+ output.push('');
158
+
159
+ if (keys.length <= 10) {
160
+ // Show all fields for small objects
161
+ output.push('FIELDS:');
162
+ keys.forEach(key => {
163
+ const fieldValue = item.value[key];
164
+ const fieldType = getType(fieldValue);
165
+ output.push(` ${key} <${fieldType}>`);
166
+ if (typeof fieldValue !== 'object' || fieldValue === null) {
167
+ output.push(` value: ${fieldValue}`);
168
+ } else if (Array.isArray(fieldValue)) {
169
+ output.push(` value: [${fieldValue.length} items]`);
170
+ } else {
171
+ output.push(` value: {object with ${Object.keys(fieldValue).length} fields}`);
172
+ }
173
+ output.push('');
174
+ });
175
+ } else {
176
+ // Show first 5 fields for large objects
177
+ output.push(`FIELDS (${keys.length} total):`);
178
+ keys.slice(0, 5).forEach(key => {
179
+ const fieldType = getType(item.value[key]);
180
+ output.push(` ${key} <${fieldType}>`);
181
+ });
182
+ output.push('');
183
+ output.push(` ... ${keys.length - 5} more fields`);
184
+ output.push('');
185
+ output.push(`NOTE: Use query "${item.path}.fieldName" to see nested details`);
186
+ }
187
+
188
+ // Add related configs for objects
189
+ if (typeof item.value === 'object' && !Array.isArray(item.value)) {
190
+ const related = findRelatedConfigs(item, allInfo);
191
+ if (related.length > 0) {
192
+ output.push('');
193
+ output.push('RELATED CONFIGS:');
194
+ related.forEach(r => {
195
+ output.push(` • ${r.path}${r.hint ? ' - ' + r.hint : ''}`);
196
+ });
197
+ }
198
+ }
199
+
200
+ return output.join('\n');
201
+ }
202
+
203
+ // Helper function to rank search results by relevance
204
+ function rankResult(item, query) {
205
+ let score = 0;
206
+ const pathLower = item.path.toLowerCase();
207
+ const keyLower = item.key.toLowerCase();
208
+ const queryLower = query.toLowerCase();
209
+
210
+ // Exact key match (highest priority)
211
+ if (keyLower === queryLower) score += 100;
212
+
213
+ // Exact path match
214
+ if (pathLower === queryLower) score += 80;
215
+
216
+ // Path ends with query
217
+ if (pathLower.endsWith('.' + queryLower)) score += 50;
218
+
219
+ // Key contains query
220
+ if (keyLower.includes(queryLower)) score += 30;
221
+
222
+ // Path contains query
223
+ if (pathLower.includes(queryLower)) score += 10;
224
+
225
+ // Prefer leaf values over objects
226
+ if (item.isLeaf) score += 20;
227
+
228
+ // Prefer shorter paths (less nesting = more relevant)
229
+ const depth = item.path.split('.').length;
230
+ score -= depth;
231
+
232
+ return score;
233
+ }
234
+
235
+ export function createServer() {
236
+ const server = new Server(
237
+ {
238
+ name: 'vcluster-yaml-mcp-server',
239
+ version: '0.1.0'
240
+ },
241
+ {
242
+ capabilities: {
243
+ tools: {}
244
+ }
245
+ }
246
+ );
247
+
248
+ // Tool definitions
249
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
250
+ return {
251
+ tools: [
252
+ {
253
+ name: 'list-versions',
254
+ description: 'DISCOVERY: Find all available vCluster versions. Returns GitHub tags (stable releases) and branches (development versions). Use this to discover what versions are available before querying specific versions.',
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: {},
258
+ required: []
259
+ }
260
+ },
261
+ {
262
+ name: 'smart-query',
263
+ description: 'UNIVERSAL SEARCH: Your go-to tool for finding ANY vCluster configuration! Understands natural language, searches intelligently, and finds related settings. USE THIS FIRST for any config questions! Examples: "show me namespace settings", "how is etcd configured?", "what networking options exist?", "find service CIDR". Searches chart/values.yaml by default.',
264
+ inputSchema: {
265
+ type: 'object',
266
+ properties: {
267
+ query: {
268
+ type: 'string',
269
+ description: 'Natural language query (e.g., "namespace syncing", "high availability", "storage options")'
270
+ },
271
+ file: {
272
+ type: 'string',
273
+ description: 'Optional: specific file to search (default: "chart/values.yaml")'
274
+ },
275
+ version: {
276
+ type: 'string',
277
+ description: 'Version tag or branch (e.g., "v0.24.0", "main"). Defaults to "main".'
278
+ }
279
+ },
280
+ required: ['query']
281
+ }
282
+ },
283
+ {
284
+ name: 'create-vcluster-config',
285
+ description: 'CONFIG CREATION WORKFLOW: Use this when generating vCluster configurations for users. This tool REQUIRES you to provide the YAML you created and automatically validates it before returning to the user. Returns validation result + formatted config. This ensures every config you create is validated.',
286
+ inputSchema: {
287
+ type: 'object',
288
+ properties: {
289
+ yaml_content: {
290
+ type: 'string',
291
+ description: 'The vCluster YAML configuration you generated (required)'
292
+ },
293
+ description: {
294
+ type: 'string',
295
+ description: 'Brief description of what this config does (optional, helpful for user)'
296
+ },
297
+ version: {
298
+ type: 'string',
299
+ description: 'Version tag or branch (e.g., "v0.24.0", "main"). Defaults to "main".'
300
+ }
301
+ },
302
+ required: ['yaml_content']
303
+ }
304
+ },
305
+ {
306
+ name: 'validate-config',
307
+ description: 'VALIDATION ONLY: Validates existing vCluster YAML (full config or partial snippet) against the schema. Use create-vcluster-config for configs you generate. Use this to validate user-provided configs or files from GitHub.',
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: {
311
+ file: {
312
+ type: 'string',
313
+ description: 'File path in GitHub repo to validate. Optional if content is provided.'
314
+ },
315
+ content: {
316
+ type: 'string',
317
+ description: 'YAML content to validate (full config or partial snippet)'
318
+ },
319
+ version: {
320
+ type: 'string',
321
+ description: 'Version tag or branch (e.g., "v0.24.0", "main"). Defaults to "main".'
322
+ }
323
+ },
324
+ required: []
325
+ }
326
+ },
327
+ {
328
+ name: 'extract-validation-rules',
329
+ description: 'AI ASSISTANT: Extract validation rules, constraints, and best practices directly from values.yaml comments. Returns structured rules for AI to understand complex relationships and semantic validations that procedural code cannot handle. USE THIS when you need to understand the "why" behind configurations or validate semantic correctness beyond syntax.',
330
+ inputSchema: {
331
+ type: 'object',
332
+ properties: {
333
+ file: {
334
+ type: 'string',
335
+ description: 'File path in GitHub repo (default: "chart/values.yaml")',
336
+ default: 'chart/values.yaml'
337
+ },
338
+ section: {
339
+ type: 'string',
340
+ description: 'Focus on specific section (e.g., "controlPlane", "sync", "networking")'
341
+ },
342
+ version: {
343
+ type: 'string',
344
+ description: 'Version tag or branch (e.g., "v0.24.0", "main"). Defaults to "main".'
345
+ }
346
+ },
347
+ required: []
348
+ }
349
+ }
350
+ ]
351
+ };
352
+ });
353
+
354
+ // Tool handler
355
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
356
+ const { name, arguments: args } = request.params;
357
+
358
+ try {
359
+ switch (name) {
360
+ case 'create-vcluster-config': {
361
+ const { yaml_content, description, version } = args;
362
+ const targetVersion = version || 'main';
363
+
364
+ // Fetch schema for validation
365
+ try {
366
+ const schemaContent = await githubClient.getFileContent('chart/values.schema.json', targetVersion);
367
+ const fullSchema = JSON.parse(schemaContent);
368
+
369
+ // Validate the config
370
+ const validationResult = validateSnippet(
371
+ yaml_content,
372
+ fullSchema,
373
+ targetVersion,
374
+ null // Auto-detect section
375
+ );
376
+
377
+ // Format response based on validation result
378
+ let response = '';
379
+
380
+ if (description) {
381
+ response += `## ${description}\n\n`;
382
+ }
383
+
384
+ if (validationResult.valid) {
385
+ response += `✅ **Configuration validated successfully!**\n\n`;
386
+ response += `Version: ${targetVersion}\n`;
387
+ if (validationResult.section) {
388
+ response += `Section: ${validationResult.section}\n`;
389
+ }
390
+ response += `Validation time: ${validationResult.elapsed_ms}ms\n\n`;
391
+ response += `### Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\`\n`;
392
+ } else {
393
+ response += `❌ **Validation failed**\n\n`;
394
+ if (validationResult.syntax_valid === false) {
395
+ response += `**Syntax Error:**\n${validationResult.syntax_error}\n\n`;
396
+ } else if (validationResult.errors && validationResult.errors.length > 0) {
397
+ response += `**Validation Errors:**\n`;
398
+ validationResult.errors.forEach((err, idx) => {
399
+ response += `${idx + 1}. **${err.path}**: ${err.message}\n`;
400
+ });
401
+ response += `\n`;
402
+ } else if (validationResult.error) {
403
+ response += `**Error:** ${validationResult.error}\n\n`;
404
+ if (validationResult.hint) {
405
+ response += `**Hint:** ${validationResult.hint}\n\n`;
406
+ }
407
+ }
408
+ response += `### Provided Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\`\n`;
409
+ }
410
+
411
+ return {
412
+ content: [
413
+ {
414
+ type: 'text',
415
+ text: response
416
+ }
417
+ ],
418
+ isError: !validationResult.valid
419
+ };
420
+ } catch (error) {
421
+ return {
422
+ content: [
423
+ {
424
+ type: 'text',
425
+ text: `❌ **Failed to validate configuration**\n\nError: ${error.message}\n\n### Provided Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\``
426
+ }
427
+ ],
428
+ isError: true
429
+ };
430
+ }
431
+ }
432
+
433
+ case 'list-versions': {
434
+ const tags = await githubClient.getTags();
435
+
436
+ // Only show versions starting with 'v'
437
+ const versionTags = tags.filter(tag => tag.startsWith('v'));
438
+
439
+ // Always include main branch
440
+ const versions = ['main', ...versionTags];
441
+
442
+ return {
443
+ content: [
444
+ {
445
+ type: 'text',
446
+ text: `Available vCluster versions:\n\n${versions.slice(0, 20).map(v => `- ${v}`).join('\n')}\n${versions.length > 20 ? `... and ${versions.length - 20} more\n` : ''}`
447
+ }
448
+ ]
449
+ };
450
+ }
451
+
452
+ case 'smart-query': {
453
+ const version = args.version || 'main';
454
+ const fileName = args.file || 'chart/values.yaml';
455
+ let yamlData;
456
+
457
+ try {
458
+ yamlData = await githubClient.getYamlContent(fileName, version);
459
+ } catch (error) {
460
+ return {
461
+ content: [
462
+ {
463
+ type: 'text',
464
+ text: `Could not load ${fileName} from GitHub (version: ${version}). Error: ${error.message}\n\nTry:\n1. Check if the file exists in this version\n2. Try querying chart/values.yaml (default config file)\n3. Try a different version`
465
+ }
466
+ ]
467
+ };
468
+ }
469
+
470
+ const searchTerm = args.query.toLowerCase();
471
+ const results = [];
472
+ const suggestions = new Set();
473
+
474
+ // Helper function to extract all paths and values
475
+ function extractInfo(obj, path = '') {
476
+ const info = [];
477
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
478
+ for (const [key, value] of Object.entries(obj)) {
479
+ const currentPath = path ? `${path}.${key}` : key;
480
+
481
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
482
+ info.push({ path: currentPath, key, value, isLeaf: false });
483
+ info.push(...extractInfo(value, currentPath));
484
+ } else {
485
+ info.push({ path: currentPath, key, value, isLeaf: true });
486
+ }
487
+ }
488
+ }
489
+ return info;
490
+ }
491
+
492
+ const allInfo = extractInfo(yamlData);
493
+
494
+ // Support dot notation queries (e.g., "controlPlane.ingress.enabled")
495
+ const isDotNotation = searchTerm.includes('.');
496
+
497
+ if (isDotNotation) {
498
+ // Exact and partial dot notation matching
499
+ for (const item of allInfo) {
500
+ const pathLower = item.path.toLowerCase();
501
+
502
+ // Exact match
503
+ if (pathLower === searchTerm) {
504
+ results.push(item);
505
+ }
506
+ // Ends with query (partial match)
507
+ else if (pathLower.endsWith(searchTerm)) {
508
+ results.push(item);
509
+ }
510
+ // Contains query
511
+ else if (pathLower.includes(searchTerm)) {
512
+ results.push(item);
513
+ suggestions.add(item.path.split('.')[0]); // Suggest top-level
514
+ }
515
+ }
516
+ } else {
517
+ // Keyword-based search
518
+ const keywords = searchTerm.split(/\s+/);
519
+
520
+ for (const item of allInfo) {
521
+ const pathLower = item.path.toLowerCase();
522
+ const keyLower = item.key.toLowerCase();
523
+ const valueStr = JSON.stringify(item.value).toLowerCase();
524
+
525
+ // Check if ALL keywords match (AND logic for multi-word)
526
+ const allKeywordsMatch = keywords.every(kw =>
527
+ pathLower.includes(kw) || keyLower.includes(kw) || valueStr.includes(kw)
528
+ );
529
+
530
+ if (allKeywordsMatch) {
531
+ results.push(item);
532
+ suggestions.add(item.path.split('.')[0]);
533
+ }
534
+ }
535
+ }
536
+
537
+ // Sort results by relevance
538
+ results.sort((a, b) => {
539
+ const scoreA = rankResult(a, searchTerm);
540
+ const scoreB = rankResult(b, searchTerm);
541
+ return scoreB - scoreA; // Descending order
542
+ });
543
+
544
+ // Limit results to avoid token overflow
545
+ const maxResults = 50;
546
+ const limitedResults = results.slice(0, maxResults);
547
+ const hasMore = results.length > maxResults;
548
+
549
+ if (limitedResults.length === 0) {
550
+ // Find similar paths
551
+ const similarPaths = allInfo
552
+ .filter(item => {
553
+ const pathParts = item.path.toLowerCase().split('.');
554
+ return pathParts.some(part => part.includes(searchTerm) || searchTerm.includes(part));
555
+ })
556
+ .slice(0, 5)
557
+ .map(item => item.path);
558
+
559
+ return {
560
+ content: [
561
+ {
562
+ type: 'text',
563
+ text: `No matches found for "${args.query}" in ${fileName} (${version}).\n\n` +
564
+ (similarPaths.length > 0
565
+ ? `Similar paths:\n${similarPaths.map(p => ` - ${p}`).join('\n')}\n\n`
566
+ : '') +
567
+ `Tips:\n` +
568
+ ` - Use dot notation: "controlPlane.ingress.enabled"\n` +
569
+ ` - Try broader terms: "${searchTerm.split('.')[0] || searchTerm.split(/\s+/)[0]}"\n` +
570
+ ` - Use extract-validation-rules for section details\n\n` +
571
+ `Top-level sections:\n${Object.keys(yamlData || {}).map(k => ` - ${k}`).join('\n')}`
572
+ }
573
+ ]
574
+ };
575
+ }
576
+
577
+ // Format all results
578
+ const formattedResults = limitedResults.map((item, idx) =>
579
+ formatMatch(item, idx, limitedResults.length, allInfo)
580
+ );
581
+
582
+ return {
583
+ content: [
584
+ {
585
+ type: 'text',
586
+ text: `Found ${results.length} match${results.length === 1 ? '' : 'es'} for "${args.query}" in ${fileName} (${version})\n\n` +
587
+ formattedResults.join('\n') +
588
+ (hasMore ? `\n\n... showing ${maxResults} of ${results.length} total matches` : '')
589
+ }
590
+ ]
591
+ };
592
+ }
593
+
594
+ case 'query-config': {
595
+ const version = args.version || 'main';
596
+ let yamlData;
597
+
598
+ // Load YAML data from GitHub or content
599
+ if (args.content) {
600
+ yamlData = yaml.load(args.content);
601
+ } else if (args.file) {
602
+ yamlData = await githubClient.getYamlContent(args.file, version);
603
+ } else {
604
+ // Default to chart/values.yaml
605
+ yamlData = await githubClient.getYamlContent('chart/values.yaml', version);
606
+ }
607
+
608
+ // Convert YAML to JSON for jq processing
609
+ const jsonData = JSON.stringify(yamlData);
610
+
611
+ // Run jq query
612
+ const options = {
613
+ input: 'string',
614
+ output: args.raw ? 'string' : 'json'
615
+ };
616
+
617
+ const result = await jq.run(args.query, jsonData, options);
618
+
619
+ return {
620
+ content: [
621
+ {
622
+ type: 'text',
623
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
624
+ }
625
+ ]
626
+ };
627
+ }
628
+
629
+ case 'get-config-value': {
630
+ const version = args.version || 'main';
631
+ let yamlData;
632
+
633
+ // Load YAML data from GitHub or content
634
+ if (args.content) {
635
+ yamlData = yaml.load(args.content);
636
+ } else if (args.file) {
637
+ yamlData = await githubClient.getYamlContent(args.file, version);
638
+ } else {
639
+ yamlData = await githubClient.getYamlContent('config/values.yaml', version);
640
+ }
641
+
642
+ // Convert dot notation to jq query
643
+ const jqQuery = '.' + args.path.split('.').map(part => {
644
+ // Handle array indices
645
+ if (/^\d+$/.test(part)) {
646
+ return `[${part}]`;
647
+ }
648
+ // Handle special characters in keys
649
+ return `["${part}"]`;
650
+ }).join('');
651
+
652
+ const jsonData = JSON.stringify(yamlData);
653
+ const result = await jq.run(jqQuery, jsonData, { input: 'string', output: 'json' });
654
+
655
+ return {
656
+ content: [
657
+ {
658
+ type: 'text',
659
+ text: `Value at ${args.path}: ${JSON.stringify(result, null, 2)}`
660
+ }
661
+ ]
662
+ };
663
+ }
664
+
665
+ case 'extract-validation-rules': {
666
+ const version = args.version || 'main';
667
+ const fileName = args.file || 'chart/values.yaml';
668
+ let content;
669
+
670
+ try {
671
+ content = await githubClient.getFileContent(fileName, version);
672
+ } catch (error) {
673
+ return {
674
+ content: [
675
+ {
676
+ type: 'text',
677
+ text: `Error loading ${fileName}: ${error.message}`
678
+ }
679
+ ]
680
+ };
681
+ }
682
+
683
+ const rules = extractValidationRulesFromComments(content, args.section);
684
+
685
+ return {
686
+ content: [
687
+ {
688
+ type: 'text',
689
+ text: JSON.stringify(rules, null, 2)
690
+ }
691
+ ]
692
+ };
693
+ }
694
+
695
+ case 'validate-config': {
696
+ const version = args.version || 'main';
697
+ let yamlContent;
698
+
699
+ // Get YAML content
700
+ try {
701
+ if (args.content) {
702
+ yamlContent = args.content;
703
+ } else if (args.file) {
704
+ yamlContent = await githubClient.getFileContent(args.file, version);
705
+ } else {
706
+ yamlContent = await githubClient.getFileContent('chart/values.yaml', version);
707
+ }
708
+ } catch (error) {
709
+ return {
710
+ content: [
711
+ {
712
+ type: 'text',
713
+ text: JSON.stringify({
714
+ valid: false,
715
+ error: `Failed to load YAML: ${error.message}`
716
+ }, null, 2)
717
+ }
718
+ ]
719
+ };
720
+ }
721
+
722
+ // Fetch schema for validation
723
+ try {
724
+ const schemaContent = await githubClient.getFileContent('chart/values.schema.json', version);
725
+ const fullSchema = JSON.parse(schemaContent);
726
+
727
+ // Use snippet validator for validation
728
+ const result = validateSnippet(
729
+ yamlContent,
730
+ fullSchema,
731
+ version,
732
+ null // Let it auto-detect section
733
+ );
734
+
735
+ return {
736
+ content: [
737
+ {
738
+ type: 'text',
739
+ text: JSON.stringify(result, null, 2)
740
+ }
741
+ ]
742
+ };
743
+ } catch (error) {
744
+ return {
745
+ content: [
746
+ {
747
+ type: 'text',
748
+ text: JSON.stringify({
749
+ valid: false,
750
+ error: `Validation failed: ${error.message}`,
751
+ version
752
+ }, null, 2)
753
+ }
754
+ ]
755
+ };
756
+ }
757
+ }
758
+
759
+ case 'search-config': {
760
+ const version = args.version || 'main';
761
+ let yamlData;
762
+
763
+ // Load YAML data from GitHub or content
764
+ if (args.content) {
765
+ yamlData = yaml.load(args.content);
766
+ } else if (args.file) {
767
+ yamlData = await githubClient.getYamlContent(args.file, version);
768
+ } else {
769
+ yamlData = await githubClient.getYamlContent('config/values.yaml', version);
770
+ }
771
+
772
+ const searchTerm = args.search.toLowerCase();
773
+ const matches = [];
774
+
775
+ // Recursive search function
776
+ function searchObject(obj, path = '') {
777
+ if (obj && typeof obj === 'object') {
778
+ for (const [key, value] of Object.entries(obj)) {
779
+ const currentPath = path ? `${path}.${key}` : key;
780
+
781
+ // Check if key matches
782
+ if (key.toLowerCase().includes(searchTerm)) {
783
+ if (args.keysOnly) {
784
+ matches.push(`Key: ${currentPath}`);
785
+ } else {
786
+ matches.push(`Key: ${currentPath} = ${JSON.stringify(value)}`);
787
+ }
788
+ }
789
+
790
+ // Check if value matches (if not keysOnly)
791
+ if (!args.keysOnly && value !== null && value !== undefined) {
792
+ const valueStr = JSON.stringify(value).toLowerCase();
793
+ if (valueStr.includes(searchTerm)) {
794
+ matches.push(`Value at ${currentPath}: ${JSON.stringify(value)}`);
795
+ }
796
+ }
797
+
798
+ // Recurse into objects
799
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
800
+ searchObject(value, currentPath);
801
+ }
802
+ }
803
+ }
804
+ }
805
+
806
+ searchObject(yamlData);
807
+
808
+ return {
809
+ content: [
810
+ {
811
+ type: 'text',
812
+ text: matches.length > 0
813
+ ? `Found ${matches.length} match(es):\n${matches.join('\n')}`
814
+ : `No matches found for "${args.search}"`
815
+ }
816
+ ]
817
+ };
818
+ }
819
+
820
+ default:
821
+ throw new Error(`Unknown tool: ${name}`);
822
+ }
823
+ } catch (error) {
824
+ return {
825
+ content: [
826
+ {
827
+ type: 'text',
828
+ text: `Error: ${error.message}`
829
+ }
830
+ ],
831
+ isError: true
832
+ };
833
+ }
834
+ });
835
+
836
+ // Extract validation rules from YAML comments for AI validation
837
+ function extractValidationRulesFromComments(yamlContent, section) {
838
+ const lines = yamlContent.split('\n');
839
+ const rules = [];
840
+ const enums = {};
841
+ const dependencies = [];
842
+ const defaults = {};
843
+
844
+ let currentPath = [];
845
+ let currentComments = [];
846
+ let indentStack = [0];
847
+
848
+ for (let i = 0; i < lines.length; i++) {
849
+ const line = lines[i];
850
+ const trimmedLine = line.trim();
851
+
852
+ // Skip empty lines
853
+ if (!trimmedLine) {
854
+ currentComments = [];
855
+ continue;
856
+ }
857
+
858
+ // Collect comments
859
+ if (trimmedLine.startsWith('#')) {
860
+ const comment = trimmedLine.substring(1).trim();
861
+ if (comment && !comment.startsWith('#')) {
862
+ currentComments.push(comment);
863
+ }
864
+ continue;
865
+ }
866
+
867
+ // Parse YAML structure
868
+ const indent = line.search(/\S/);
869
+ const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)?$/);
870
+
871
+ if (keyMatch) {
872
+ const key = keyMatch[2];
873
+ const value = keyMatch[3];
874
+
875
+ // Update path based on indentation
876
+ while (indentStack.length > 1 && indent <= indentStack[indentStack.length - 1]) {
877
+ indentStack.pop();
878
+ currentPath.pop();
879
+ }
880
+
881
+ if (indent > indentStack[indentStack.length - 1]) {
882
+ indentStack.push(indent);
883
+ } else if (indent < indentStack[indentStack.length - 1]) {
884
+ while (indentStack.length > 1 && indent < indentStack[indentStack.length - 1]) {
885
+ indentStack.pop();
886
+ currentPath.pop();
887
+ }
888
+ } else {
889
+ currentPath.pop();
890
+ }
891
+
892
+ currentPath.push(key);
893
+ const fullPath = currentPath.join('.');
894
+
895
+ // Filter by section if specified
896
+ if (section && !fullPath.startsWith(section)) {
897
+ currentComments = [];
898
+ continue;
899
+ }
900
+
901
+ // Extract validation instructions from comments
902
+ if (currentComments.length > 0) {
903
+ const instructions = [];
904
+
905
+ for (const comment of currentComments) {
906
+ // Extract enum values (e.g., "Valid values: a, b, c")
907
+ const enumMatch = comment.match(/(?:valid values?|options?|choices?|possible values?):\s*(.+)/i);
908
+ if (enumMatch) {
909
+ const values = enumMatch[1].split(/[,;]/).map(v => v.trim()).filter(v => v);
910
+ enums[fullPath] = values;
911
+ instructions.push(`Valid values: ${values.join(', ')}`);
912
+ }
913
+
914
+ // Extract required dependencies
915
+ if (comment.match(/requires?|depends on|needs?/i)) {
916
+ dependencies.push(`${fullPath}: ${comment}`);
917
+ instructions.push(comment);
918
+ }
919
+
920
+ // Extract defaults
921
+ const defaultMatch = comment.match(/default(?:s)?\s*(?:is|:)?\s*(.+)/i);
922
+ if (defaultMatch) {
923
+ defaults[fullPath] = defaultMatch[1].trim();
924
+ }
925
+
926
+ // Extract validation rules
927
+ if (comment.match(/must|should|cannot|only|at least|minimum|maximum|required/i)) {
928
+ instructions.push(comment);
929
+ }
930
+
931
+ // Extract warnings
932
+ if (comment.match(/warning|note|important|deprecated/i)) {
933
+ instructions.push(`⚠️ ${comment}`);
934
+ }
935
+ }
936
+
937
+ if (instructions.length > 0) {
938
+ rules.push({
939
+ path: fullPath,
940
+ instructions: instructions,
941
+ originalComments: currentComments
942
+ });
943
+ }
944
+ }
945
+
946
+ currentComments = [];
947
+ }
948
+ }
949
+
950
+ // Generate AI validation instructions
951
+ const aiInstructions = {
952
+ summary: `Extracted ${rules.length} validation rules from YAML comments`,
953
+ rules: rules,
954
+ enums: enums,
955
+ dependencies: dependencies,
956
+ defaults: defaults,
957
+ instructions: generateAiValidationInstructions(rules, enums, dependencies)
958
+ };
959
+
960
+ return aiInstructions;
961
+ }
962
+
963
+ function generateAiValidationInstructions(rules, enums, dependencies) {
964
+ let instructions = '### AI Validation Instructions\n\n';
965
+ instructions += 'Please validate the configuration using these rules extracted from comments:\n\n';
966
+
967
+ if (rules.length > 0) {
968
+ instructions += '#### Field-Specific Rules:\n';
969
+ rules.forEach(rule => {
970
+ instructions += `- **${rule.path}**:\n`;
971
+ rule.instructions.forEach(inst => {
972
+ instructions += ` - ${inst}\n`;
973
+ });
974
+ });
975
+ instructions += '\n';
976
+ }
977
+
978
+ if (Object.keys(enums).length > 0) {
979
+ instructions += '#### Enumeration Constraints:\n';
980
+ instructions += 'Ensure these fields only contain the specified values:\n';
981
+ Object.entries(enums).forEach(([field, values]) => {
982
+ instructions += `- ${field}: [${values.join(', ')}]\n`;
983
+ });
984
+ instructions += '\n';
985
+ }
986
+
987
+ if (dependencies.length > 0) {
988
+ instructions += '#### Dependencies to Check:\n';
989
+ dependencies.forEach(dep => {
990
+ instructions += `- ${dep}\n`;
991
+ });
992
+ instructions += '\n';
993
+ }
994
+
995
+ instructions += '#### Validation Approach:\n';
996
+ instructions += '1. Check if all enumeration constraints are satisfied\n';
997
+ instructions += '2. Verify all dependency requirements are met\n';
998
+ instructions += '3. Validate against the specific rules for each field\n';
999
+ instructions += '4. Flag any deprecated fields or configurations\n';
1000
+ instructions += '5. Provide helpful suggestions for fixing any issues found\n';
1001
+
1002
+ return instructions;
1003
+ }
1004
+
1005
+ return server;
1006
+ }