vcluster-yaml-mcp-server 1.0.6 → 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.
- package/README.md +10 -2
- package/package.json +15 -4
- package/src/cli.js +4 -5
- package/src/http-server.js +2 -1
- package/src/schema-validator.js +51 -22
- package/src/server.js +2 -743
- package/src/tool-handlers.js +384 -0
- package/src/tool-registry.js +45 -0
- package/src/validation-rules.js +181 -0
package/src/server.js
CHANGED
|
@@ -1,235 +1,7 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import yaml from 'js-yaml';
|
|
4
3
|
import { githubClient } from './github.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
// Helper function to get the type of a value
|
|
8
|
-
function getType(value) {
|
|
9
|
-
if (value === null) return 'null';
|
|
10
|
-
if (Array.isArray(value)) return 'array';
|
|
11
|
-
if (typeof value === 'object') return 'object';
|
|
12
|
-
if (typeof value === 'number') {
|
|
13
|
-
return Number.isInteger(value) ? 'integer' : 'number';
|
|
14
|
-
}
|
|
15
|
-
return typeof value; // 'string', 'boolean'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Helper function to format values for display
|
|
19
|
-
function formatValue(value, path, indent = 0) {
|
|
20
|
-
const spaces = ' '.repeat(indent);
|
|
21
|
-
|
|
22
|
-
// Primitives (string, number, boolean, null)
|
|
23
|
-
if (value === null) return 'null';
|
|
24
|
-
if (typeof value !== 'object') return String(value);
|
|
25
|
-
|
|
26
|
-
// Arrays
|
|
27
|
-
if (Array.isArray(value)) {
|
|
28
|
-
if (value.length === 0) return '[]';
|
|
29
|
-
if (value.length <= 5 && value.every(v => typeof v !== 'object')) {
|
|
30
|
-
// Small array of primitives - inline
|
|
31
|
-
return JSON.stringify(value);
|
|
32
|
-
}
|
|
33
|
-
// Multi-line array
|
|
34
|
-
return '\n' + value.map(v =>
|
|
35
|
-
`${spaces} - ${formatValue(v, path, indent + 1)}`
|
|
36
|
-
).join('\n');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Objects
|
|
40
|
-
const keys = Object.keys(value);
|
|
41
|
-
if (keys.length === 0) return '{}';
|
|
42
|
-
|
|
43
|
-
// Small object (≤5 fields) - show all fields
|
|
44
|
-
if (keys.length <= 5) {
|
|
45
|
-
return '\n' + keys.map(key =>
|
|
46
|
-
`${spaces} ${key}: ${formatValue(value[key], `${path}.${key}`, indent + 1)}`
|
|
47
|
-
).join('\n');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Large object - show structure only
|
|
51
|
-
return `\n${spaces} {object with ${keys.length} fields}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Helper function to get field hints for common field names
|
|
55
|
-
function getFieldHint(fieldName) {
|
|
56
|
-
// Common field patterns and their hints
|
|
57
|
-
const hints = {
|
|
58
|
-
'resources': 'Resource limits and requests',
|
|
59
|
-
'replicas': 'Number of replicas for HA',
|
|
60
|
-
'affinity': 'Pod affinity rules',
|
|
61
|
-
'tolerations': 'Pod toleration settings',
|
|
62
|
-
'nodeSelector': 'Node selection constraints',
|
|
63
|
-
'image': 'Container image configuration',
|
|
64
|
-
'enabled': 'Enable/disable this feature',
|
|
65
|
-
'annotations': 'Kubernetes annotations',
|
|
66
|
-
'labels': 'Kubernetes labels',
|
|
67
|
-
'ingress': 'Ingress configuration',
|
|
68
|
-
'service': 'Service configuration',
|
|
69
|
-
'storage': 'Storage configuration',
|
|
70
|
-
'persistence': 'Persistent volume settings',
|
|
71
|
-
'sync': 'Resource sync configuration',
|
|
72
|
-
'networking': 'Network settings'
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return hints[fieldName] || '';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Helper function to find related configs for a given item
|
|
79
|
-
function findRelatedConfigs(item, allInfo) {
|
|
80
|
-
const related = [];
|
|
81
|
-
const pathParts = item.path.split('.');
|
|
82
|
-
const lastKey = pathParts[pathParts.length - 1];
|
|
83
|
-
const parentPath = pathParts.slice(0, -1).join('.');
|
|
84
|
-
|
|
85
|
-
// Strategy 1: Find sibling fields (same parent path)
|
|
86
|
-
const siblings = allInfo.filter(info => {
|
|
87
|
-
const infoParent = info.path.split('.').slice(0, -1).join('.');
|
|
88
|
-
return infoParent === parentPath &&
|
|
89
|
-
info.path !== item.path &&
|
|
90
|
-
typeof info.value === 'object' &&
|
|
91
|
-
!Array.isArray(info.value);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Add up to 2 sibling configs
|
|
95
|
-
siblings.slice(0, 2).forEach(sibling => {
|
|
96
|
-
const siblingKey = sibling.path.split('.').pop();
|
|
97
|
-
related.push({
|
|
98
|
-
path: sibling.path,
|
|
99
|
-
hint: getFieldHint(siblingKey)
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Strategy 2: Find same key name elsewhere (commonly configured together)
|
|
104
|
-
if (related.length < 3) {
|
|
105
|
-
const sameKeyElsewhere = allInfo.filter(info => {
|
|
106
|
-
const infoKey = info.path.split('.').pop();
|
|
107
|
-
return infoKey === lastKey &&
|
|
108
|
-
info.path !== item.path &&
|
|
109
|
-
!info.path.startsWith(item.path) && // Not a child
|
|
110
|
-
typeof info.value === 'object' &&
|
|
111
|
-
!Array.isArray(info.value);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Add up to 1 same-key config from different section
|
|
115
|
-
sameKeyElsewhere.slice(0, 1).forEach(same => {
|
|
116
|
-
const section = same.path.split('.')[0];
|
|
117
|
-
related.push({
|
|
118
|
-
path: same.path,
|
|
119
|
-
hint: `${lastKey} in ${section} section`
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return related.slice(0, 3); // Max 3 related configs
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Helper function to format a single match result
|
|
128
|
-
function formatMatch(item, index, total, allInfo) {
|
|
129
|
-
const separator = '━'.repeat(60);
|
|
130
|
-
let output = [];
|
|
131
|
-
|
|
132
|
-
if (index > 0) output.push(''); // Blank line between matches
|
|
133
|
-
output.push(separator);
|
|
134
|
-
output.push('');
|
|
135
|
-
output.push(`MATCH: ${item.path}`);
|
|
136
|
-
output.push(`TYPE: ${getType(item.value)}`);
|
|
137
|
-
|
|
138
|
-
// For primitives and simple values
|
|
139
|
-
if (typeof item.value !== 'object' || item.value === null) {
|
|
140
|
-
output.push(`VALUE: ${item.value}`);
|
|
141
|
-
return output.join('\n');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// For arrays
|
|
145
|
-
if (Array.isArray(item.value)) {
|
|
146
|
-
if (item.value.length === 0) {
|
|
147
|
-
output.push('VALUE: []');
|
|
148
|
-
} else {
|
|
149
|
-
output.push(`VALUE: ${formatValue(item.value, item.path)}`);
|
|
150
|
-
}
|
|
151
|
-
return output.join('\n');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// For objects - show fields
|
|
155
|
-
const keys = Object.keys(item.value);
|
|
156
|
-
output.push('');
|
|
157
|
-
|
|
158
|
-
if (keys.length <= 10) {
|
|
159
|
-
// Show all fields for small objects
|
|
160
|
-
output.push('FIELDS:');
|
|
161
|
-
keys.forEach(key => {
|
|
162
|
-
const fieldValue = item.value[key];
|
|
163
|
-
const fieldType = getType(fieldValue);
|
|
164
|
-
output.push(` ${key} <${fieldType}>`);
|
|
165
|
-
if (typeof fieldValue !== 'object' || fieldValue === null) {
|
|
166
|
-
output.push(` value: ${fieldValue}`);
|
|
167
|
-
} else if (Array.isArray(fieldValue)) {
|
|
168
|
-
output.push(` value: [${fieldValue.length} items]`);
|
|
169
|
-
} else {
|
|
170
|
-
output.push(` value: {object with ${Object.keys(fieldValue).length} fields}`);
|
|
171
|
-
}
|
|
172
|
-
output.push('');
|
|
173
|
-
});
|
|
174
|
-
} else {
|
|
175
|
-
// Show first 5 fields for large objects
|
|
176
|
-
output.push(`FIELDS (${keys.length} total):`);
|
|
177
|
-
keys.slice(0, 5).forEach(key => {
|
|
178
|
-
const fieldType = getType(item.value[key]);
|
|
179
|
-
output.push(` ${key} <${fieldType}>`);
|
|
180
|
-
});
|
|
181
|
-
output.push('');
|
|
182
|
-
output.push(` ... ${keys.length - 5} more fields`);
|
|
183
|
-
output.push('');
|
|
184
|
-
output.push(`NOTE: Use query "${item.path}.fieldName" to see nested details`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Add related configs for objects
|
|
188
|
-
if (typeof item.value === 'object' && !Array.isArray(item.value)) {
|
|
189
|
-
const related = findRelatedConfigs(item, allInfo);
|
|
190
|
-
if (related.length > 0) {
|
|
191
|
-
output.push('');
|
|
192
|
-
output.push('RELATED CONFIGS:');
|
|
193
|
-
related.forEach(r => {
|
|
194
|
-
output.push(` • ${r.path}${r.hint ? ' - ' + r.hint : ''}`);
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return output.join('\n');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Helper function to rank search results by relevance
|
|
203
|
-
function rankResult(item, query) {
|
|
204
|
-
let score = 0;
|
|
205
|
-
const pathLower = item.path.toLowerCase();
|
|
206
|
-
const keyLower = item.key.toLowerCase();
|
|
207
|
-
const queryLower = query.toLowerCase();
|
|
208
|
-
|
|
209
|
-
// Exact key match (highest priority)
|
|
210
|
-
if (keyLower === queryLower) score += 100;
|
|
211
|
-
|
|
212
|
-
// Exact path match
|
|
213
|
-
if (pathLower === queryLower) score += 80;
|
|
214
|
-
|
|
215
|
-
// Path ends with query
|
|
216
|
-
if (pathLower.endsWith('.' + queryLower)) score += 50;
|
|
217
|
-
|
|
218
|
-
// Key contains query
|
|
219
|
-
if (keyLower.includes(queryLower)) score += 30;
|
|
220
|
-
|
|
221
|
-
// Path contains query
|
|
222
|
-
if (pathLower.includes(queryLower)) score += 10;
|
|
223
|
-
|
|
224
|
-
// Prefer leaf values over objects
|
|
225
|
-
if (item.isLeaf) score += 20;
|
|
226
|
-
|
|
227
|
-
// Prefer shorter paths (less nesting = more relevant)
|
|
228
|
-
const depth = item.path.split('.').length;
|
|
229
|
-
score -= depth;
|
|
230
|
-
|
|
231
|
-
return score;
|
|
232
|
-
}
|
|
4
|
+
import { executeToolHandler } from './tool-registry.js';
|
|
233
5
|
|
|
234
6
|
export function createServer() {
|
|
235
7
|
const server = new Server(
|
|
@@ -353,521 +125,8 @@ export function createServer() {
|
|
|
353
125
|
// Tool handler
|
|
354
126
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
355
127
|
const { name, arguments: args } = request.params;
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
switch (name) {
|
|
359
|
-
case 'create-vcluster-config': {
|
|
360
|
-
const { yaml_content, description, version } = args;
|
|
361
|
-
const targetVersion = version || 'main';
|
|
362
|
-
|
|
363
|
-
// Fetch schema for validation
|
|
364
|
-
try {
|
|
365
|
-
const schemaContent = await githubClient.getFileContent('chart/values.schema.json', targetVersion);
|
|
366
|
-
const fullSchema = JSON.parse(schemaContent);
|
|
367
|
-
|
|
368
|
-
// Validate the config
|
|
369
|
-
const validationResult = validateSnippet(
|
|
370
|
-
yaml_content,
|
|
371
|
-
fullSchema,
|
|
372
|
-
targetVersion,
|
|
373
|
-
null // Auto-detect section
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
// Format response based on validation result
|
|
377
|
-
let response = '';
|
|
378
|
-
|
|
379
|
-
if (description) {
|
|
380
|
-
response += `## ${description}\n\n`;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (validationResult.valid) {
|
|
384
|
-
response += `✅ **Configuration validated successfully!**\n\n`;
|
|
385
|
-
response += `Version: ${targetVersion}\n`;
|
|
386
|
-
if (validationResult.section) {
|
|
387
|
-
response += `Section: ${validationResult.section}\n`;
|
|
388
|
-
}
|
|
389
|
-
response += `Validation time: ${validationResult.elapsed_ms}ms\n\n`;
|
|
390
|
-
response += `### Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\`\n`;
|
|
391
|
-
} else {
|
|
392
|
-
response += `❌ **Validation failed**\n\n`;
|
|
393
|
-
if (validationResult.syntax_valid === false) {
|
|
394
|
-
response += `**Syntax Error:**\n${validationResult.syntax_error}\n\n`;
|
|
395
|
-
} else if (validationResult.errors && validationResult.errors.length > 0) {
|
|
396
|
-
response += `**Validation Errors:**\n`;
|
|
397
|
-
validationResult.errors.forEach((err, idx) => {
|
|
398
|
-
response += `${idx + 1}. **${err.path}**: ${err.message}\n`;
|
|
399
|
-
});
|
|
400
|
-
response += `\n`;
|
|
401
|
-
} else if (validationResult.error) {
|
|
402
|
-
response += `**Error:** ${validationResult.error}\n\n`;
|
|
403
|
-
if (validationResult.hint) {
|
|
404
|
-
response += `**Hint:** ${validationResult.hint}\n\n`;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
response += `### Provided Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\`\n`;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return {
|
|
411
|
-
content: [
|
|
412
|
-
{
|
|
413
|
-
type: 'text',
|
|
414
|
-
text: response
|
|
415
|
-
}
|
|
416
|
-
],
|
|
417
|
-
isError: !validationResult.valid
|
|
418
|
-
};
|
|
419
|
-
} catch (error) {
|
|
420
|
-
return {
|
|
421
|
-
content: [
|
|
422
|
-
{
|
|
423
|
-
type: 'text',
|
|
424
|
-
text: `❌ **Failed to validate configuration**\n\nError: ${error.message}\n\n### Provided Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\``
|
|
425
|
-
}
|
|
426
|
-
],
|
|
427
|
-
isError: true
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
case 'list-versions': {
|
|
433
|
-
const tags = await githubClient.getTags();
|
|
434
|
-
|
|
435
|
-
// Only show versions starting with 'v'
|
|
436
|
-
const versionTags = tags.filter(tag => tag.startsWith('v'));
|
|
437
|
-
|
|
438
|
-
// Always include main branch
|
|
439
|
-
const versions = ['main', ...versionTags];
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
content: [
|
|
443
|
-
{
|
|
444
|
-
type: 'text',
|
|
445
|
-
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` : ''}`
|
|
446
|
-
}
|
|
447
|
-
]
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
case 'smart-query': {
|
|
452
|
-
const version = args.version || 'main';
|
|
453
|
-
const fileName = args.file || 'chart/values.yaml';
|
|
454
|
-
let yamlData;
|
|
455
|
-
|
|
456
|
-
try {
|
|
457
|
-
yamlData = await githubClient.getYamlContent(fileName, version);
|
|
458
|
-
} catch (error) {
|
|
459
|
-
return {
|
|
460
|
-
content: [
|
|
461
|
-
{
|
|
462
|
-
type: 'text',
|
|
463
|
-
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`
|
|
464
|
-
}
|
|
465
|
-
]
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const searchTerm = args.query.toLowerCase();
|
|
470
|
-
const results = [];
|
|
471
|
-
const suggestions = new Set();
|
|
472
|
-
|
|
473
|
-
// Helper function to extract all paths and values
|
|
474
|
-
function extractInfo(obj, path = '') {
|
|
475
|
-
const info = [];
|
|
476
|
-
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
477
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
478
|
-
const currentPath = path ? `${path}.${key}` : key;
|
|
479
|
-
|
|
480
|
-
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
481
|
-
info.push({ path: currentPath, key, value, isLeaf: false });
|
|
482
|
-
info.push(...extractInfo(value, currentPath));
|
|
483
|
-
} else {
|
|
484
|
-
info.push({ path: currentPath, key, value, isLeaf: true });
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return info;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const allInfo = extractInfo(yamlData);
|
|
492
|
-
|
|
493
|
-
// Support dot notation queries (e.g., "controlPlane.ingress.enabled")
|
|
494
|
-
const isDotNotation = searchTerm.includes('.');
|
|
495
|
-
|
|
496
|
-
if (isDotNotation) {
|
|
497
|
-
// Exact and partial dot notation matching
|
|
498
|
-
for (const item of allInfo) {
|
|
499
|
-
const pathLower = item.path.toLowerCase();
|
|
500
|
-
|
|
501
|
-
// Exact match
|
|
502
|
-
if (pathLower === searchTerm) {
|
|
503
|
-
results.push(item);
|
|
504
|
-
}
|
|
505
|
-
// Ends with query (partial match)
|
|
506
|
-
else if (pathLower.endsWith(searchTerm)) {
|
|
507
|
-
results.push(item);
|
|
508
|
-
}
|
|
509
|
-
// Contains query
|
|
510
|
-
else if (pathLower.includes(searchTerm)) {
|
|
511
|
-
results.push(item);
|
|
512
|
-
suggestions.add(item.path.split('.')[0]); // Suggest top-level
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
} else {
|
|
516
|
-
// Keyword-based search
|
|
517
|
-
const keywords = searchTerm.split(/\s+/);
|
|
518
|
-
|
|
519
|
-
for (const item of allInfo) {
|
|
520
|
-
const pathLower = item.path.toLowerCase();
|
|
521
|
-
const keyLower = item.key.toLowerCase();
|
|
522
|
-
const valueStr = JSON.stringify(item.value).toLowerCase();
|
|
523
|
-
|
|
524
|
-
// Check if ALL keywords match (AND logic for multi-word)
|
|
525
|
-
const allKeywordsMatch = keywords.every(kw =>
|
|
526
|
-
pathLower.includes(kw) || keyLower.includes(kw) || valueStr.includes(kw)
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
if (allKeywordsMatch) {
|
|
530
|
-
results.push(item);
|
|
531
|
-
suggestions.add(item.path.split('.')[0]);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Sort results by relevance
|
|
537
|
-
results.sort((a, b) => {
|
|
538
|
-
const scoreA = rankResult(a, searchTerm);
|
|
539
|
-
const scoreB = rankResult(b, searchTerm);
|
|
540
|
-
return scoreB - scoreA; // Descending order
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
// Limit results to avoid token overflow
|
|
544
|
-
const maxResults = 50;
|
|
545
|
-
const limitedResults = results.slice(0, maxResults);
|
|
546
|
-
const hasMore = results.length > maxResults;
|
|
547
|
-
|
|
548
|
-
if (limitedResults.length === 0) {
|
|
549
|
-
// Find similar paths
|
|
550
|
-
const similarPaths = allInfo
|
|
551
|
-
.filter(item => {
|
|
552
|
-
const pathParts = item.path.toLowerCase().split('.');
|
|
553
|
-
return pathParts.some(part => part.includes(searchTerm) || searchTerm.includes(part));
|
|
554
|
-
})
|
|
555
|
-
.slice(0, 5)
|
|
556
|
-
.map(item => item.path);
|
|
557
|
-
|
|
558
|
-
return {
|
|
559
|
-
content: [
|
|
560
|
-
{
|
|
561
|
-
type: 'text',
|
|
562
|
-
text: `No matches found for "${args.query}" in ${fileName} (${version}).\n\n` +
|
|
563
|
-
(similarPaths.length > 0
|
|
564
|
-
? `Similar paths:\n${similarPaths.map(p => ` - ${p}`).join('\n')}\n\n`
|
|
565
|
-
: '') +
|
|
566
|
-
`Tips:\n` +
|
|
567
|
-
` - Use dot notation: "controlPlane.ingress.enabled"\n` +
|
|
568
|
-
` - Try broader terms: "${searchTerm.split('.')[0] || searchTerm.split(/\s+/)[0]}"\n` +
|
|
569
|
-
` - Use extract-validation-rules for section details\n\n` +
|
|
570
|
-
`Top-level sections:\n${Object.keys(yamlData || {}).map(k => ` - ${k}`).join('\n')}`
|
|
571
|
-
}
|
|
572
|
-
]
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Format all results
|
|
577
|
-
const formattedResults = limitedResults.map((item, idx) =>
|
|
578
|
-
formatMatch(item, idx, limitedResults.length, allInfo)
|
|
579
|
-
);
|
|
580
|
-
|
|
581
|
-
return {
|
|
582
|
-
content: [
|
|
583
|
-
{
|
|
584
|
-
type: 'text',
|
|
585
|
-
text: `Found ${results.length} match${results.length === 1 ? '' : 'es'} for "${args.query}" in ${fileName} (${version})\n\n` +
|
|
586
|
-
formattedResults.join('\n') +
|
|
587
|
-
(hasMore ? `\n\n... showing ${maxResults} of ${results.length} total matches` : '')
|
|
588
|
-
}
|
|
589
|
-
]
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
case 'extract-validation-rules': {
|
|
594
|
-
const version = args.version || 'main';
|
|
595
|
-
const fileName = args.file || 'chart/values.yaml';
|
|
596
|
-
let content;
|
|
597
|
-
|
|
598
|
-
try {
|
|
599
|
-
content = await githubClient.getFileContent(fileName, version);
|
|
600
|
-
} catch (error) {
|
|
601
|
-
return {
|
|
602
|
-
content: [
|
|
603
|
-
{
|
|
604
|
-
type: 'text',
|
|
605
|
-
text: `Error loading ${fileName}: ${error.message}`
|
|
606
|
-
}
|
|
607
|
-
]
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const rules = extractValidationRulesFromComments(content, args.section);
|
|
612
|
-
|
|
613
|
-
return {
|
|
614
|
-
content: [
|
|
615
|
-
{
|
|
616
|
-
type: 'text',
|
|
617
|
-
text: JSON.stringify(rules, null, 2)
|
|
618
|
-
}
|
|
619
|
-
]
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
case 'validate-config': {
|
|
624
|
-
const version = args.version || 'main';
|
|
625
|
-
let yamlContent;
|
|
626
|
-
|
|
627
|
-
// Get YAML content
|
|
628
|
-
try {
|
|
629
|
-
if (args.content) {
|
|
630
|
-
yamlContent = args.content;
|
|
631
|
-
} else if (args.file) {
|
|
632
|
-
yamlContent = await githubClient.getFileContent(args.file, version);
|
|
633
|
-
} else {
|
|
634
|
-
yamlContent = await githubClient.getFileContent('chart/values.yaml', version);
|
|
635
|
-
}
|
|
636
|
-
} catch (error) {
|
|
637
|
-
return {
|
|
638
|
-
content: [
|
|
639
|
-
{
|
|
640
|
-
type: 'text',
|
|
641
|
-
text: JSON.stringify({
|
|
642
|
-
valid: false,
|
|
643
|
-
error: `Failed to load YAML: ${error.message}`
|
|
644
|
-
}, null, 2)
|
|
645
|
-
}
|
|
646
|
-
]
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Fetch schema for validation
|
|
651
|
-
try {
|
|
652
|
-
const schemaContent = await githubClient.getFileContent('chart/values.schema.json', version);
|
|
653
|
-
const fullSchema = JSON.parse(schemaContent);
|
|
654
|
-
|
|
655
|
-
// Use snippet validator for validation
|
|
656
|
-
const result = validateSnippet(
|
|
657
|
-
yamlContent,
|
|
658
|
-
fullSchema,
|
|
659
|
-
version,
|
|
660
|
-
null // Let it auto-detect section
|
|
661
|
-
);
|
|
662
|
-
|
|
663
|
-
return {
|
|
664
|
-
content: [
|
|
665
|
-
{
|
|
666
|
-
type: 'text',
|
|
667
|
-
text: JSON.stringify(result, null, 2)
|
|
668
|
-
}
|
|
669
|
-
]
|
|
670
|
-
};
|
|
671
|
-
} catch (error) {
|
|
672
|
-
return {
|
|
673
|
-
content: [
|
|
674
|
-
{
|
|
675
|
-
type: 'text',
|
|
676
|
-
text: JSON.stringify({
|
|
677
|
-
valid: false,
|
|
678
|
-
error: `Validation failed: ${error.message}`,
|
|
679
|
-
version
|
|
680
|
-
}, null, 2)
|
|
681
|
-
}
|
|
682
|
-
]
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
default:
|
|
688
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
689
|
-
}
|
|
690
|
-
} catch (error) {
|
|
691
|
-
return {
|
|
692
|
-
content: [
|
|
693
|
-
{
|
|
694
|
-
type: 'text',
|
|
695
|
-
text: `Error: ${error.message}`
|
|
696
|
-
}
|
|
697
|
-
],
|
|
698
|
-
isError: true
|
|
699
|
-
};
|
|
700
|
-
}
|
|
128
|
+
return executeToolHandler(name, args, githubClient);
|
|
701
129
|
});
|
|
702
130
|
|
|
703
|
-
// Extract validation rules from YAML comments for AI validation
|
|
704
|
-
function extractValidationRulesFromComments(yamlContent, section) {
|
|
705
|
-
const lines = yamlContent.split('\n');
|
|
706
|
-
const rules = [];
|
|
707
|
-
const enums = {};
|
|
708
|
-
const dependencies = [];
|
|
709
|
-
const defaults = {};
|
|
710
|
-
|
|
711
|
-
let currentPath = [];
|
|
712
|
-
let currentComments = [];
|
|
713
|
-
let indentStack = [0];
|
|
714
|
-
|
|
715
|
-
for (let i = 0; i < lines.length; i++) {
|
|
716
|
-
const line = lines[i];
|
|
717
|
-
const trimmedLine = line.trim();
|
|
718
|
-
|
|
719
|
-
// Skip empty lines
|
|
720
|
-
if (!trimmedLine) {
|
|
721
|
-
currentComments = [];
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Collect comments
|
|
726
|
-
if (trimmedLine.startsWith('#')) {
|
|
727
|
-
const comment = trimmedLine.substring(1).trim();
|
|
728
|
-
if (comment && !comment.startsWith('#')) {
|
|
729
|
-
currentComments.push(comment);
|
|
730
|
-
}
|
|
731
|
-
continue;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Parse YAML structure
|
|
735
|
-
const indent = line.search(/\S/);
|
|
736
|
-
const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)?$/);
|
|
737
|
-
|
|
738
|
-
if (keyMatch) {
|
|
739
|
-
const key = keyMatch[2];
|
|
740
|
-
const value = keyMatch[3];
|
|
741
|
-
|
|
742
|
-
// Update path based on indentation
|
|
743
|
-
while (indentStack.length > 1 && indent <= indentStack[indentStack.length - 1]) {
|
|
744
|
-
indentStack.pop();
|
|
745
|
-
currentPath.pop();
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
if (indent > indentStack[indentStack.length - 1]) {
|
|
749
|
-
indentStack.push(indent);
|
|
750
|
-
} else if (indent < indentStack[indentStack.length - 1]) {
|
|
751
|
-
while (indentStack.length > 1 && indent < indentStack[indentStack.length - 1]) {
|
|
752
|
-
indentStack.pop();
|
|
753
|
-
currentPath.pop();
|
|
754
|
-
}
|
|
755
|
-
} else {
|
|
756
|
-
currentPath.pop();
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
currentPath.push(key);
|
|
760
|
-
const fullPath = currentPath.join('.');
|
|
761
|
-
|
|
762
|
-
// Filter by section if specified
|
|
763
|
-
if (section && !fullPath.startsWith(section)) {
|
|
764
|
-
currentComments = [];
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Extract validation instructions from comments
|
|
769
|
-
if (currentComments.length > 0) {
|
|
770
|
-
const instructions = [];
|
|
771
|
-
|
|
772
|
-
for (const comment of currentComments) {
|
|
773
|
-
// Extract enum values (e.g., "Valid values: a, b, c")
|
|
774
|
-
const enumMatch = comment.match(/(?:valid values?|options?|choices?|possible values?):\s*(.+)/i);
|
|
775
|
-
if (enumMatch) {
|
|
776
|
-
const values = enumMatch[1].split(/[,;]/).map(v => v.trim()).filter(v => v);
|
|
777
|
-
enums[fullPath] = values;
|
|
778
|
-
instructions.push(`Valid values: ${values.join(', ')}`);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Extract required dependencies
|
|
782
|
-
if (comment.match(/requires?|depends on|needs?/i)) {
|
|
783
|
-
dependencies.push(`${fullPath}: ${comment}`);
|
|
784
|
-
instructions.push(comment);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Extract defaults
|
|
788
|
-
const defaultMatch = comment.match(/default(?:s)?\s*(?:is|:)?\s*(.+)/i);
|
|
789
|
-
if (defaultMatch) {
|
|
790
|
-
defaults[fullPath] = defaultMatch[1].trim();
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Extract validation rules
|
|
794
|
-
if (comment.match(/must|should|cannot|only|at least|minimum|maximum|required/i)) {
|
|
795
|
-
instructions.push(comment);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Extract warnings
|
|
799
|
-
if (comment.match(/warning|note|important|deprecated/i)) {
|
|
800
|
-
instructions.push(`⚠️ ${comment}`);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
if (instructions.length > 0) {
|
|
805
|
-
rules.push({
|
|
806
|
-
path: fullPath,
|
|
807
|
-
instructions: instructions,
|
|
808
|
-
originalComments: currentComments
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
currentComments = [];
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// Generate AI validation instructions
|
|
818
|
-
const aiInstructions = {
|
|
819
|
-
summary: `Extracted ${rules.length} validation rules from YAML comments`,
|
|
820
|
-
rules: rules,
|
|
821
|
-
enums: enums,
|
|
822
|
-
dependencies: dependencies,
|
|
823
|
-
defaults: defaults,
|
|
824
|
-
instructions: generateAiValidationInstructions(rules, enums, dependencies)
|
|
825
|
-
};
|
|
826
|
-
|
|
827
|
-
return aiInstructions;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function generateAiValidationInstructions(rules, enums, dependencies) {
|
|
831
|
-
let instructions = '### AI Validation Instructions\n\n';
|
|
832
|
-
instructions += 'Please validate the configuration using these rules extracted from comments:\n\n';
|
|
833
|
-
|
|
834
|
-
if (rules.length > 0) {
|
|
835
|
-
instructions += '#### Field-Specific Rules:\n';
|
|
836
|
-
rules.forEach(rule => {
|
|
837
|
-
instructions += `- **${rule.path}**:\n`;
|
|
838
|
-
rule.instructions.forEach(inst => {
|
|
839
|
-
instructions += ` - ${inst}\n`;
|
|
840
|
-
});
|
|
841
|
-
});
|
|
842
|
-
instructions += '\n';
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (Object.keys(enums).length > 0) {
|
|
846
|
-
instructions += '#### Enumeration Constraints:\n';
|
|
847
|
-
instructions += 'Ensure these fields only contain the specified values:\n';
|
|
848
|
-
Object.entries(enums).forEach(([field, values]) => {
|
|
849
|
-
instructions += `- ${field}: [${values.join(', ')}]\n`;
|
|
850
|
-
});
|
|
851
|
-
instructions += '\n';
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
if (dependencies.length > 0) {
|
|
855
|
-
instructions += '#### Dependencies to Check:\n';
|
|
856
|
-
dependencies.forEach(dep => {
|
|
857
|
-
instructions += `- ${dep}\n`;
|
|
858
|
-
});
|
|
859
|
-
instructions += '\n';
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
instructions += '#### Validation Approach:\n';
|
|
863
|
-
instructions += '1. Check if all enumeration constraints are satisfied\n';
|
|
864
|
-
instructions += '2. Verify all dependency requirements are met\n';
|
|
865
|
-
instructions += '3. Validate against the specific rules for each field\n';
|
|
866
|
-
instructions += '4. Flag any deprecated fields or configurations\n';
|
|
867
|
-
instructions += '5. Provide helpful suggestions for fixing any issues found\n';
|
|
868
|
-
|
|
869
|
-
return instructions;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
131
|
return server;
|
|
873
132
|
}
|