vme-mcp-server 0.1.5 → 0.1.6
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/dist/lib/api-utils.js +251 -0
- package/dist/lib/intent-recognition.js +159 -0
- package/dist/lib/session.js +59 -0
- package/dist/lib/vm-parsing.js +82 -0
- package/dist/server.js +36 -918
- package/dist/server_old.js +933 -0
- package/dist/tools/create-vm.js +205 -0
- package/dist/tools/export-training-data.js +115 -0
- package/dist/tools/get-resources.js +40 -0
- package/dist/tools/index.js +41 -0
- package/dist/tools/parse-intent.js +52 -0
- package/dist/tools/provide-feedback.js +61 -0
- package/dist/types/interfaces.js +2 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env node
|
2
|
+
#!/usr/bin/env node
|
2
3
|
"use strict";
|
3
4
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
4
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
@@ -6,929 +7,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
7
8
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
8
9
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
9
|
-
const
|
10
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
10
11
|
const dotenv_1 = __importDefault(require("dotenv"));
|
11
|
-
const
|
12
|
-
|
12
|
+
const index_js_2 = require("./tools/index.js");
|
13
|
+
// Load environment variables
|
13
14
|
dotenv_1.default.config();
|
15
|
+
// Disable TLS verification for VME API (if needed)
|
14
16
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
15
|
-
//
|
16
|
-
const
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
}
|
24
|
-
try {
|
25
|
-
// Ensure logs directory exists
|
26
|
-
const logsDir = (0, path_1.join)(process.cwd(), 'ai-training-logs');
|
27
|
-
if (!(0, fs_1.existsSync)(logsDir)) {
|
28
|
-
(0, fs_1.mkdirSync)(logsDir, { recursive: true });
|
29
|
-
}
|
30
|
-
// Filter fields based on user preferences
|
31
|
-
const filteredInteraction = {
|
32
|
-
id: interaction.id || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
33
|
-
timestamp: new Date().toISOString(),
|
34
|
-
session_id: interaction.session_id || 'unknown',
|
35
|
-
tool_name: interaction.tool_name || 'unknown'
|
36
|
-
};
|
37
|
-
// Only include fields user has opted into
|
38
|
-
if (AI_TRAINING_FIELDS.includes('user_input') && interaction.user_input) {
|
39
|
-
filteredInteraction.user_input = interaction.user_input;
|
40
|
-
}
|
41
|
-
if (AI_TRAINING_FIELDS.includes('parsed_output') && interaction.parsed_output) {
|
42
|
-
filteredInteraction.parsed_output = interaction.parsed_output;
|
43
|
-
}
|
44
|
-
if (AI_TRAINING_FIELDS.includes('api_calls') && interaction.api_calls) {
|
45
|
-
filteredInteraction.api_calls = interaction.api_calls;
|
46
|
-
}
|
47
|
-
if (AI_TRAINING_FIELDS.includes('success_metrics') && interaction.success_metrics) {
|
48
|
-
filteredInteraction.success_metrics = interaction.success_metrics;
|
49
|
-
}
|
50
|
-
if (AI_TRAINING_FIELDS.includes('timing') && interaction.timing) {
|
51
|
-
filteredInteraction.timing = interaction.timing;
|
52
|
-
}
|
53
|
-
// Append to daily log file (JSONL format)
|
54
|
-
const logFile = (0, path_1.join)(logsDir, `interactions-${new Date().toISOString().split('T')[0]}.jsonl`);
|
55
|
-
const logEntry = JSON.stringify(filteredInteraction) + '\n';
|
56
|
-
(0, fs_1.writeFileSync)(logFile, logEntry, { flag: 'a' });
|
57
|
-
}
|
58
|
-
catch (error) {
|
59
|
-
// Fail silently to not disrupt user experience
|
60
|
-
console.warn('AI training data logging failed:', error);
|
61
|
-
}
|
62
|
-
}
|
63
|
-
// Generate session ID for interaction tracking
|
64
|
-
function generateSessionId() {
|
65
|
-
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
66
|
-
}
|
67
|
-
const api = axios_1.default.create({
|
68
|
-
baseURL: process.env.VME_API_BASE_URL,
|
69
|
-
headers: {
|
70
|
-
Authorization: `Bearer ${process.env.VME_API_TOKEN}`,
|
71
|
-
"Content-Type": "application/json"
|
72
|
-
}
|
17
|
+
// Create VME MCP server with modular architecture
|
18
|
+
const server = new index_js_1.Server({
|
19
|
+
name: "vme-mcp-server",
|
20
|
+
version: "1.5.0",
|
21
|
+
}, {
|
22
|
+
capabilities: {
|
23
|
+
tools: {},
|
24
|
+
},
|
73
25
|
});
|
74
|
-
//
|
75
|
-
|
76
|
-
// Handle range patterns like "web01->web03" or "web01 -> web03"
|
77
|
-
const rangeMatch = nameInput.match(/^(.+?)(\d+)\s*->\s*(.+?)(\d+)$/);
|
78
|
-
if (rangeMatch) {
|
79
|
-
const [, prefix1, start, prefix2, end] = rangeMatch;
|
80
|
-
if (prefix1.trim() === prefix2.trim()) {
|
81
|
-
const basePrefix = prefix1.trim();
|
82
|
-
const startNum = parseInt(start);
|
83
|
-
const endNum = parseInt(end);
|
84
|
-
const names = [];
|
85
|
-
for (let i = startNum; i <= endNum; i++) {
|
86
|
-
const paddedNum = i.toString().padStart(start.length, '0');
|
87
|
-
names.push(`${basePrefix}${paddedNum}`);
|
88
|
-
}
|
89
|
-
return names;
|
90
|
-
}
|
91
|
-
}
|
92
|
-
// Handle count-based generation
|
93
|
-
if (count && count > 1) {
|
94
|
-
const names = [];
|
95
|
-
// Extract base name and number pattern
|
96
|
-
const match = nameInput.match(/^(.+?)(\d+)$/);
|
97
|
-
if (match) {
|
98
|
-
const [, prefix, startStr] = match;
|
99
|
-
const startNum = parseInt(startStr);
|
100
|
-
for (let i = 0; i < count; i++) {
|
101
|
-
const num = startNum + i;
|
102
|
-
const paddedNum = num.toString().padStart(startStr.length, '0');
|
103
|
-
names.push(`${prefix}${paddedNum}`);
|
104
|
-
}
|
105
|
-
return names;
|
106
|
-
}
|
107
|
-
else {
|
108
|
-
// If no number pattern, append numbers
|
109
|
-
for (let i = 1; i <= count; i++) {
|
110
|
-
const paddedNum = i.toString().padStart(2, '0');
|
111
|
-
names.push(`${nameInput}${paddedNum}`);
|
112
|
-
}
|
113
|
-
return names;
|
114
|
-
}
|
115
|
-
}
|
116
|
-
// Single VM
|
117
|
-
return [nameInput];
|
118
|
-
}
|
119
|
-
// Helper function to get available cluster nodes
|
120
|
-
async function getClusterNodes() {
|
121
|
-
try {
|
122
|
-
const clusters = await api.get("/clusters");
|
123
|
-
const prodCluster = clusters.data.clusters.find((cluster) => cluster.name === "prod01");
|
124
|
-
if (prodCluster && prodCluster.servers) {
|
125
|
-
return prodCluster.servers.map((server) => server.id);
|
126
|
-
}
|
127
|
-
}
|
128
|
-
catch (error) {
|
129
|
-
console.error('Error getting cluster nodes:', error);
|
130
|
-
}
|
131
|
-
// Fallback to known nodes if API fails
|
132
|
-
return [1, 2, 3];
|
133
|
-
}
|
134
|
-
async function resolveInput({ group, cloud, template, size }) {
|
135
|
-
const [groups, zones, instanceTypes, servicePlans] = await Promise.all([
|
136
|
-
api.get("/groups"),
|
137
|
-
api.get("/zones"),
|
138
|
-
api.get("/instance-types"),
|
139
|
-
api.get("/service-plans")
|
140
|
-
]);
|
141
|
-
// Find group (case-insensitive)
|
142
|
-
const foundGroup = groups.data.groups.find((g) => g.name.toLowerCase() === group.toLowerCase());
|
143
|
-
// Find zone/cloud (case-insensitive, handle both terms)
|
144
|
-
const foundZone = zones.data.zones.find((z) => z.name.toLowerCase() === cloud.toLowerCase());
|
145
|
-
// In HPE VM Essentials, all VMs use "HPE VM" instance type
|
146
|
-
// The actual OS is determined by the image selection
|
147
|
-
const instanceType = instanceTypes.data.instanceTypes.find((t) => t.name === 'HPE VM');
|
148
|
-
// Find appropriate image based on template using osType properties
|
149
|
-
let selectedImage;
|
150
|
-
const templateLower = template.toLowerCase();
|
151
|
-
// Get available images to find the right one
|
152
|
-
const images = await api.get("/virtual-images");
|
153
|
-
if (templateLower.includes('ubuntu')) {
|
154
|
-
// Find Ubuntu images using osType.category and prefer latest version
|
155
|
-
const ubuntuImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'ubuntu').sort((a, b) => {
|
156
|
-
// Sort by version descending (24.04 > 22.04 > 20.04)
|
157
|
-
const aVersion = parseFloat(a.osType.osVersion || '0');
|
158
|
-
const bVersion = parseFloat(b.osType.osVersion || '0');
|
159
|
-
return bVersion - aVersion;
|
160
|
-
});
|
161
|
-
selectedImage = ubuntuImages[0];
|
162
|
-
}
|
163
|
-
else if (templateLower.includes('rocky')) {
|
164
|
-
// Find Rocky Linux images and prefer latest version (Rocky 9 > Rocky 8)
|
165
|
-
const rockyImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'rocky').sort((a, b) => {
|
166
|
-
const aVersion = parseInt(a.osType.osVersion || '0');
|
167
|
-
const bVersion = parseInt(b.osType.osVersion || '0');
|
168
|
-
return bVersion - aVersion; // Latest first
|
169
|
-
});
|
170
|
-
selectedImage = rockyImages[0];
|
171
|
-
}
|
172
|
-
else if (templateLower.includes('centos')) {
|
173
|
-
// Find CentOS images and prefer latest version (CentOS 9 > CentOS 8)
|
174
|
-
const centosImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'centos').sort((a, b) => {
|
175
|
-
const aVersion = parseInt(a.osType.osVersion || '0');
|
176
|
-
const bVersion = parseInt(b.osType.osVersion || '0');
|
177
|
-
return bVersion - aVersion; // Latest first
|
178
|
-
});
|
179
|
-
selectedImage = centosImages[0];
|
180
|
-
}
|
181
|
-
else if (templateLower.includes('debian')) {
|
182
|
-
// Find Debian images and prefer latest version (Debian 12 > Debian 11)
|
183
|
-
const debianImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'debian').sort((a, b) => {
|
184
|
-
const aVersion = parseInt(a.osType.osVersion || '0');
|
185
|
-
const bVersion = parseInt(b.osType.osVersion || '0');
|
186
|
-
return bVersion - aVersion; // Latest first
|
187
|
-
});
|
188
|
-
selectedImage = debianImages[0];
|
189
|
-
}
|
190
|
-
else if (templateLower.includes('alma')) {
|
191
|
-
// Find AlmaLinux images and prefer latest version (AlmaLinux 9 > AlmaLinux 8)
|
192
|
-
const almaImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'almalinux').sort((a, b) => {
|
193
|
-
const aVersion = parseInt(a.osType.osVersion || '0');
|
194
|
-
const bVersion = parseInt(b.osType.osVersion || '0');
|
195
|
-
return bVersion - aVersion; // Latest first
|
196
|
-
});
|
197
|
-
selectedImage = almaImages[0];
|
198
|
-
}
|
199
|
-
else if (templateLower.includes('rhel') || templateLower.includes('red hat')) {
|
200
|
-
// Find RHEL images and prefer latest version
|
201
|
-
const rhelImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'rhel').sort((a, b) => {
|
202
|
-
const aVersion = parseInt(a.osType.osVersion || '0');
|
203
|
-
const bVersion = parseInt(b.osType.osVersion || '0');
|
204
|
-
return bVersion - aVersion; // Latest first
|
205
|
-
});
|
206
|
-
selectedImage = rhelImages[0];
|
207
|
-
}
|
208
|
-
// Default to latest Ubuntu if no specific match
|
209
|
-
if (!selectedImage) {
|
210
|
-
const ubuntuImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'ubuntu').sort((a, b) => {
|
211
|
-
const aVersion = parseFloat(a.osType.osVersion || '0');
|
212
|
-
const bVersion = parseFloat(b.osType.osVersion || '0');
|
213
|
-
return bVersion - aVersion;
|
214
|
-
});
|
215
|
-
selectedImage = ubuntuImages[0] || images.data.virtualImages[0];
|
216
|
-
}
|
217
|
-
// Find service plan based on size (flexible matching)
|
218
|
-
let plan;
|
219
|
-
const sizeLower = size.toLowerCase();
|
220
|
-
if (sizeLower.includes('8gb') || sizeLower.includes('8 gb') || sizeLower.includes('medium')) {
|
221
|
-
plan = servicePlans.data.servicePlans.find((p) => p.name.includes('2 CPU, 8GB Memory'));
|
222
|
-
}
|
223
|
-
else if (sizeLower.includes('4gb') || sizeLower.includes('4 gb') || sizeLower.includes('small')) {
|
224
|
-
plan = servicePlans.data.servicePlans.find((p) => p.name.includes('1 CPU, 4GB Memory'));
|
225
|
-
}
|
226
|
-
// Fallback to any available plan
|
227
|
-
if (!plan) {
|
228
|
-
plan = servicePlans.data.servicePlans.find((p) => p.name.includes('CPU') && p.name.includes('Memory'));
|
229
|
-
}
|
26
|
+
// Register all tools using the modular tool definitions
|
27
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
230
28
|
return {
|
231
|
-
|
232
|
-
cloudId: foundZone?.id || zones.data.zones[0]?.id,
|
233
|
-
instanceTypeId: instanceType?.id,
|
234
|
-
servicePlanId: plan?.id,
|
235
|
-
imageId: selectedImage?.id,
|
236
|
-
// Return resolved names for better error reporting
|
237
|
-
resolvedGroup: foundGroup?.name || groups.data.groups[0]?.name,
|
238
|
-
resolvedCloud: foundZone?.name || zones.data.zones[0]?.name,
|
239
|
-
resolvedInstanceType: instanceType?.name,
|
240
|
-
resolvedPlan: plan?.name,
|
241
|
-
resolvedImage: selectedImage?.osType?.name || selectedImage?.name,
|
242
|
-
// Available options for error messages
|
243
|
-
availableGroups: groups.data.groups.map((g) => g.name),
|
244
|
-
availableZones: zones.data.zones.map((z) => z.name)
|
245
|
-
};
|
246
|
-
}
|
247
|
-
function parseVMIntent(naturalLanguageInput) {
|
248
|
-
const input = naturalLanguageInput.toLowerCase().trim();
|
249
|
-
const result = {
|
250
|
-
action: 'unknown',
|
251
|
-
confidence: 0,
|
252
|
-
entities: {},
|
253
|
-
originalText: naturalLanguageInput
|
29
|
+
tools: index_js_2.allTools,
|
254
30
|
};
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
destroy: [
|
273
|
-
/\b(delete|remove|destroy|terminate|stop|kill)\b/,
|
274
|
-
/\btear\s+down/,
|
275
|
-
/\bshut\s+down/
|
276
|
-
],
|
277
|
-
discover: [
|
278
|
-
/\b(available|options|resources|templates|plans)\b/,
|
279
|
-
/\bshow\s+me\s+(all|available)/,
|
280
|
-
/\bwhat\s+(are|is)\s+(available|possible)/,
|
281
|
-
/\bwhat\s+can\s+i\s+(create|do)/
|
282
|
-
]
|
283
|
-
};
|
284
|
-
// Calculate confidence for each intent
|
285
|
-
let maxConfidence = 0;
|
286
|
-
let detectedAction = 'unknown';
|
287
|
-
for (const [action, patterns] of Object.entries(intentPatterns)) {
|
288
|
-
let matches = 0;
|
289
|
-
for (const pattern of patterns) {
|
290
|
-
if (pattern.test(input)) {
|
291
|
-
matches++;
|
292
|
-
}
|
293
|
-
}
|
294
|
-
const confidence = patterns.length > 0 ? matches / patterns.length : 0;
|
295
|
-
if (confidence > maxConfidence) {
|
296
|
-
maxConfidence = confidence;
|
297
|
-
detectedAction = action;
|
298
|
-
}
|
299
|
-
}
|
300
|
-
result.action = detectedAction;
|
301
|
-
result.confidence = maxConfidence;
|
302
|
-
// Entity Extraction for VM creation
|
303
|
-
if (result.action === 'create') {
|
304
|
-
// Extract VM names and count
|
305
|
-
const namePatterns = [
|
306
|
-
/\b(?:vm|server|machine)s?\s+(?:named\s+)?["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?/,
|
307
|
-
/\b(?:named\s+)?["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?\s+(?:vm|server|machine)/,
|
308
|
-
/\bcreate\s+(?:(?:vm|server|machine)s?\s+)?["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?/,
|
309
|
-
/\b(?:vm|server|machine)\s+named\s+["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?/
|
310
|
-
];
|
311
|
-
for (const pattern of namePatterns) {
|
312
|
-
const match = input.match(pattern);
|
313
|
-
if (match) {
|
314
|
-
const nameInput = match[1] || match[2];
|
315
|
-
result.entities.vmNames = parseVMNames(nameInput);
|
316
|
-
break;
|
317
|
-
}
|
318
|
-
}
|
319
|
-
// Extract count
|
320
|
-
const countMatch = input.match(/\b(\d+)\s+(?:vm|server|machine)/);
|
321
|
-
if (countMatch) {
|
322
|
-
result.entities.count = parseInt(countMatch[1]);
|
323
|
-
}
|
324
|
-
// Extract OS template
|
325
|
-
const osPatterns = [
|
326
|
-
/\b(ubuntu|rocky|centos|debian|rhel|red\s*hat|alma|almalinux)(?:\s+(\d+(?:\.\d+)?))?/,
|
327
|
-
/\busing\s+([a-zA-Z0-9\-_]+)\s+(?:template|os|operating)/,
|
328
|
-
/\b(?:template|os|operating)\s+([a-zA-Z0-9\-_]+)/
|
329
|
-
];
|
330
|
-
for (const pattern of osPatterns) {
|
331
|
-
const match = input.match(pattern);
|
332
|
-
if (match) {
|
333
|
-
result.entities.osTemplate = match[0];
|
334
|
-
break;
|
335
|
-
}
|
336
|
-
}
|
337
|
-
// Extract size/memory
|
338
|
-
const sizePatterns = [
|
339
|
-
/\b(small|medium|large|tiny)\b/,
|
340
|
-
/\b(\d+)\s*gb\s+(?:memory|ram)\b/,
|
341
|
-
/\b(\d+)\s*cpu[s]?\s*,?\s*(\d+)\s*gb/,
|
342
|
-
/\bwith\s+(\d+(?:gb|cpu))\b/,
|
343
|
-
/\b(\d+gb)\b/
|
344
|
-
];
|
345
|
-
for (const pattern of sizePatterns) {
|
346
|
-
const match = input.match(pattern);
|
347
|
-
if (match) {
|
348
|
-
result.entities.size = match[0];
|
349
|
-
break;
|
350
|
-
}
|
351
|
-
}
|
352
|
-
// Extract group/environment
|
353
|
-
const groupPatterns = [
|
354
|
-
/\bin\s+(?:the\s+)?([a-zA-Z0-9\-_]+)\s+(?:group|environment|site)/,
|
355
|
-
/\b(?:group|environment|site)\s+([a-zA-Z0-9\-_]+)/,
|
356
|
-
/\bfor\s+([a-zA-Z0-9\-_]+)\s+(?:environment|testing|production)/
|
357
|
-
];
|
358
|
-
for (const pattern of groupPatterns) {
|
359
|
-
const match = input.match(pattern);
|
360
|
-
if (match) {
|
361
|
-
result.entities.group = match[1];
|
362
|
-
break;
|
363
|
-
}
|
364
|
-
}
|
365
|
-
// Extract zone/cloud
|
366
|
-
const zonePatterns = [
|
367
|
-
/\bon\s+([a-zA-Z0-9\-_]+)\s+(?:zone|cloud)/,
|
368
|
-
/\b(?:zone|cloud)\s+([a-zA-Z0-9\-_]+)/,
|
369
|
-
/\bin\s+([a-zA-Z0-9\-_]+)\s+(?:zone|cloud)/,
|
370
|
-
/\b(?:deploy|launch|create)\s+(?:in\s+)?([a-zA-Z0-9\-_]+)\s+zone/
|
371
|
-
];
|
372
|
-
for (const pattern of zonePatterns) {
|
373
|
-
const match = input.match(pattern);
|
374
|
-
if (match) {
|
375
|
-
result.entities.zone = match[1];
|
376
|
-
break;
|
377
|
-
}
|
378
|
-
}
|
379
|
-
// Extract distribution strategy
|
380
|
-
if (input.includes('spread') || input.includes('distribute') || input.includes('across')) {
|
381
|
-
result.entities.distribution = 'spread';
|
382
|
-
}
|
383
|
-
else if (input.includes('each node') || input.includes('all nodes')) {
|
384
|
-
result.entities.distribution = 'spread';
|
385
|
-
}
|
386
|
-
else if (input.includes('node') && input.match(/node[s]?\s*[\d,\s]+/)) {
|
387
|
-
const nodeMatch = input.match(/node[s]?\s*([\d,\s]+)/);
|
388
|
-
if (nodeMatch) {
|
389
|
-
result.entities.distribution = nodeMatch[1].replace(/\s+/g, '');
|
390
|
-
}
|
391
|
-
}
|
392
|
-
// Boost confidence if we extracted entities
|
393
|
-
const entityCount = Object.keys(result.entities).length;
|
394
|
-
if (entityCount > 0) {
|
395
|
-
result.confidence = Math.min(0.95, result.confidence + (entityCount * 0.1));
|
396
|
-
}
|
397
|
-
}
|
398
|
-
return result;
|
399
|
-
}
|
400
|
-
// Unified resource discovery function
|
401
|
-
async function getResources(type, intent, role) {
|
402
|
-
try {
|
403
|
-
// Fetch all resource types in parallel for comprehensive discovery
|
404
|
-
const [groups, zones, instanceTypes, servicePlans, virtualImages, clusters] = await Promise.all([
|
405
|
-
api.get("/groups"),
|
406
|
-
api.get("/zones"),
|
407
|
-
api.get("/instance-types"),
|
408
|
-
api.get("/service-plans"),
|
409
|
-
api.get("/virtual-images"),
|
410
|
-
api.get("/clusters")
|
411
|
-
]);
|
412
|
-
const allResources = {
|
413
|
-
groups: groups.data.groups || [],
|
414
|
-
zones: zones.data.zones || [],
|
415
|
-
instanceTypes: instanceTypes.data.instanceTypes || [],
|
416
|
-
servicePlans: servicePlans.data.servicePlans || [],
|
417
|
-
virtualImages: virtualImages.data.virtualImages || [],
|
418
|
-
clusters: clusters.data.clusters || []
|
419
|
-
};
|
420
|
-
// Apply intelligent filtering based on type
|
421
|
-
if (type) {
|
422
|
-
const typeLower = type.toLowerCase();
|
423
|
-
if (typeLower.includes('compute') || typeLower.includes('vm')) {
|
424
|
-
return {
|
425
|
-
groups: allResources.groups,
|
426
|
-
zones: allResources.zones,
|
427
|
-
servicePlans: allResources.servicePlans,
|
428
|
-
virtualImages: allResources.virtualImages,
|
429
|
-
clusters: allResources.clusters,
|
430
|
-
_filtered_for: 'compute/vm resources'
|
431
|
-
};
|
432
|
-
}
|
433
|
-
else if (typeLower.includes('network')) {
|
434
|
-
return {
|
435
|
-
zones: allResources.zones,
|
436
|
-
clusters: allResources.clusters,
|
437
|
-
_filtered_for: 'network resources'
|
438
|
-
};
|
439
|
-
}
|
440
|
-
else if (typeLower.includes('storage')) {
|
441
|
-
return {
|
442
|
-
servicePlans: allResources.servicePlans,
|
443
|
-
zones: allResources.zones,
|
444
|
-
_filtered_for: 'storage resources'
|
445
|
-
};
|
446
|
-
}
|
447
|
-
}
|
448
|
-
// Apply intent-based filtering
|
449
|
-
if (intent) {
|
450
|
-
const intentLower = intent.toLowerCase();
|
451
|
-
if (intentLower.includes('create') || intentLower.includes('provision')) {
|
452
|
-
// For creation intents, focus on templates and plans
|
453
|
-
return {
|
454
|
-
groups: allResources.groups,
|
455
|
-
zones: allResources.zones,
|
456
|
-
servicePlans: allResources.servicePlans,
|
457
|
-
virtualImages: allResources.virtualImages.map((img) => ({
|
458
|
-
id: img.id,
|
459
|
-
name: img.name,
|
460
|
-
osType: img.osType,
|
461
|
-
recommended: img.osType?.category === 'ubuntu' ? true : false
|
462
|
-
})),
|
463
|
-
_filtered_for: 'creation/provisioning intent'
|
464
|
-
};
|
465
|
-
}
|
466
|
-
else if (intentLower.includes('list') || intentLower.includes('show') || intentLower.includes('discover')) {
|
467
|
-
// For discovery intents, return comprehensive view
|
468
|
-
return {
|
469
|
-
summary: {
|
470
|
-
total_groups: allResources.groups.length,
|
471
|
-
total_zones: allResources.zones.length,
|
472
|
-
total_service_plans: allResources.servicePlans.length,
|
473
|
-
total_os_templates: allResources.virtualImages.length,
|
474
|
-
total_clusters: allResources.clusters.length
|
475
|
-
},
|
476
|
-
...allResources,
|
477
|
-
_filtered_for: 'discovery/listing intent'
|
478
|
-
};
|
479
|
-
}
|
480
|
-
}
|
481
|
-
// Default: return all resources with semantic organization
|
482
|
-
return {
|
483
|
-
compute: {
|
484
|
-
groups: allResources.groups,
|
485
|
-
servicePlans: allResources.servicePlans,
|
486
|
-
virtualImages: allResources.virtualImages
|
487
|
-
},
|
488
|
-
infrastructure: {
|
489
|
-
zones: allResources.zones,
|
490
|
-
clusters: allResources.clusters,
|
491
|
-
instanceTypes: allResources.instanceTypes
|
492
|
-
},
|
493
|
-
_organization: 'semantic grouping by function'
|
494
|
-
};
|
495
|
-
}
|
496
|
-
catch (error) {
|
497
|
-
return {
|
498
|
-
error: `Failed to fetch resources: ${error.response?.data?.message || error.message}`,
|
499
|
-
available_endpoints: ['/groups', '/zones', '/service-plans', '/virtual-images', '/clusters'],
|
500
|
-
fallback_suggestion: 'Try checking individual resource types with specific API endpoints'
|
501
|
-
};
|
502
|
-
}
|
31
|
+
});
|
32
|
+
// Handle tool calls using the modular tool handlers
|
33
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
34
|
+
const { name, arguments: args } = request.params;
|
35
|
+
// Look up the handler for this tool
|
36
|
+
const handler = index_js_2.toolHandlers[name];
|
37
|
+
if (!handler) {
|
38
|
+
throw new Error(`Unknown tool: ${name}`);
|
39
|
+
}
|
40
|
+
// Call the appropriate handler
|
41
|
+
return await handler(args);
|
42
|
+
});
|
43
|
+
// Start the server
|
44
|
+
async function main() {
|
45
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
46
|
+
await server.connect(transport);
|
47
|
+
console.error("VME MCP Server v1.5.0 running on stdio with modular architecture");
|
503
48
|
}
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
}, {
|
508
|
-
capabilities: {
|
509
|
-
tools: {
|
510
|
-
list: async () => ([
|
511
|
-
{
|
512
|
-
name: "get_resources",
|
513
|
-
description: "Discover and explore available VME infrastructure resources with intelligent filtering",
|
514
|
-
inputSchema: {
|
515
|
-
type: "object",
|
516
|
-
properties: {
|
517
|
-
type: {
|
518
|
-
type: "string",
|
519
|
-
description: "Resource type filter: 'compute', 'network', 'storage', 'vm', or leave empty for all"
|
520
|
-
},
|
521
|
-
intent: {
|
522
|
-
type: "string",
|
523
|
-
description: "Intent-based filtering: 'create', 'provision', 'list', 'discover', or natural language description"
|
524
|
-
},
|
525
|
-
role: {
|
526
|
-
type: "string",
|
527
|
-
description: "User role for permission-aware filtering (future enhancement)"
|
528
|
-
}
|
529
|
-
},
|
530
|
-
required: []
|
531
|
-
}
|
532
|
-
},
|
533
|
-
{
|
534
|
-
name: "parse_vm_intent",
|
535
|
-
description: "Parse natural language VM requests into structured parameters with confidence scoring",
|
536
|
-
inputSchema: {
|
537
|
-
type: "object",
|
538
|
-
properties: {
|
539
|
-
naturalLanguageInput: {
|
540
|
-
type: "string",
|
541
|
-
description: "Natural language description of VM provisioning request"
|
542
|
-
}
|
543
|
-
},
|
544
|
-
required: ["naturalLanguageInput"]
|
545
|
-
}
|
546
|
-
},
|
547
|
-
{
|
548
|
-
name: "export_training_data",
|
549
|
-
description: "Export AI training data for model improvement (requires ENABLE_AI_TRAINING_DATA=true)",
|
550
|
-
inputSchema: {
|
551
|
-
type: "object",
|
552
|
-
properties: {
|
553
|
-
format: {
|
554
|
-
type: "string",
|
555
|
-
description: "Export format: 'jsonl' or 'csv'",
|
556
|
-
enum: ["jsonl", "csv"]
|
557
|
-
},
|
558
|
-
days: {
|
559
|
-
type: "number",
|
560
|
-
description: "Number of days of data to export (default: 7)"
|
561
|
-
}
|
562
|
-
},
|
563
|
-
required: []
|
564
|
-
}
|
565
|
-
},
|
566
|
-
{
|
567
|
-
name: "provide_feedback",
|
568
|
-
description: "Provide feedback on intent parsing accuracy to improve future predictions",
|
569
|
-
inputSchema: {
|
570
|
-
type: "object",
|
571
|
-
properties: {
|
572
|
-
original_input: {
|
573
|
-
type: "string",
|
574
|
-
description: "Original natural language input"
|
575
|
-
},
|
576
|
-
parsed_result: {
|
577
|
-
type: "object",
|
578
|
-
description: "The parsed result that was incorrect"
|
579
|
-
},
|
580
|
-
correct_interpretation: {
|
581
|
-
type: "object",
|
582
|
-
description: "What the correct interpretation should have been"
|
583
|
-
},
|
584
|
-
feedback_notes: {
|
585
|
-
type: "string",
|
586
|
-
description: "Additional notes about what went wrong"
|
587
|
-
}
|
588
|
-
},
|
589
|
-
required: ["original_input", "feedback_notes"]
|
590
|
-
}
|
591
|
-
},
|
592
|
-
{
|
593
|
-
name: "create_vm",
|
594
|
-
description: "Provision a new virtual machine",
|
595
|
-
inputSchema: {
|
596
|
-
type: "object",
|
597
|
-
properties: {
|
598
|
-
name: {
|
599
|
-
type: "string",
|
600
|
-
description: "Name for VM(s). Supports patterns like 'web01' or 'web01->web03' for multiple VMs"
|
601
|
-
},
|
602
|
-
group: {
|
603
|
-
type: "string",
|
604
|
-
description: "Group/site where VM will be created"
|
605
|
-
},
|
606
|
-
cloud: {
|
607
|
-
type: "string",
|
608
|
-
description: "Cloud/zone where VM will be provisioned (also accepts 'zone')"
|
609
|
-
},
|
610
|
-
zone: {
|
611
|
-
type: "string",
|
612
|
-
description: "Zone/cloud where VM will be provisioned (alias for 'cloud')"
|
613
|
-
},
|
614
|
-
template: {
|
615
|
-
type: "string",
|
616
|
-
description: "VM template or operating system"
|
617
|
-
},
|
618
|
-
size: {
|
619
|
-
type: "string",
|
620
|
-
description: "VM size (small, medium, 4GB, 8GB, etc.)"
|
621
|
-
},
|
622
|
-
distribution: {
|
623
|
-
type: "string",
|
624
|
-
description: "VM distribution strategy: 'auto' (default), 'spread' (across all nodes), or 'node1,node2,node3' (specific nodes)"
|
625
|
-
},
|
626
|
-
count: {
|
627
|
-
type: "number",
|
628
|
-
description: "Number of VMs to create (alternative to name patterns)"
|
629
|
-
}
|
630
|
-
},
|
631
|
-
required: ["name", "group", "template", "size"]
|
632
|
-
}
|
633
|
-
}
|
634
|
-
]),
|
635
|
-
call: async (req) => {
|
636
|
-
if (req.tool === "get_resources") {
|
637
|
-
const { type, intent, role } = req.arguments;
|
638
|
-
const resources = await getResources(type, intent, role);
|
639
|
-
return {
|
640
|
-
toolResult: JSON.stringify(resources, null, 2),
|
641
|
-
isError: !!resources.error
|
642
|
-
};
|
643
|
-
}
|
644
|
-
if (req.tool === "parse_vm_intent") {
|
645
|
-
const startTime = Date.now();
|
646
|
-
const { naturalLanguageInput } = req.arguments;
|
647
|
-
const sessionId = generateSessionId();
|
648
|
-
const parsedIntent = parseVMIntent(naturalLanguageInput);
|
649
|
-
const parseTime = Date.now() - startTime;
|
650
|
-
// Log interaction for AI training (respects privacy settings)
|
651
|
-
logInteraction({
|
652
|
-
session_id: sessionId,
|
653
|
-
tool_name: 'parse_vm_intent',
|
654
|
-
user_input: { naturalLanguageInput },
|
655
|
-
parsed_output: parsedIntent,
|
656
|
-
success_metrics: {
|
657
|
-
operation_success: parsedIntent.action !== 'unknown',
|
658
|
-
confidence_score: parsedIntent.confidence,
|
659
|
-
entities_extracted: Object.keys(parsedIntent.entities).length
|
660
|
-
},
|
661
|
-
timing: {
|
662
|
-
parse_duration_ms: parseTime,
|
663
|
-
total_duration_ms: parseTime
|
664
|
-
}
|
665
|
-
});
|
666
|
-
return {
|
667
|
-
toolResult: JSON.stringify(parsedIntent, null, 2),
|
668
|
-
isError: parsedIntent.action === 'unknown' && parsedIntent.confidence < 0.3
|
669
|
-
};
|
670
|
-
}
|
671
|
-
if (req.tool === "export_training_data") {
|
672
|
-
if (!AI_TRAINING_ENABLED) {
|
673
|
-
return {
|
674
|
-
toolResult: JSON.stringify({
|
675
|
-
error: "Training data collection is disabled",
|
676
|
-
message: "Set ENABLE_AI_TRAINING_DATA=true in .env to enable data collection and export"
|
677
|
-
}, null, 2),
|
678
|
-
isError: true
|
679
|
-
};
|
680
|
-
}
|
681
|
-
const { format = "jsonl", days = 7 } = req.arguments;
|
682
|
-
try {
|
683
|
-
const logsDir = (0, path_1.join)(process.cwd(), 'ai-training-logs');
|
684
|
-
if (!(0, fs_1.existsSync)(logsDir)) {
|
685
|
-
return {
|
686
|
-
toolResult: JSON.stringify({
|
687
|
-
message: "No training data found",
|
688
|
-
data_count: 0
|
689
|
-
}, null, 2),
|
690
|
-
isError: false
|
691
|
-
};
|
692
|
-
}
|
693
|
-
// Collect data from last N days
|
694
|
-
const cutoffDate = new Date(Date.now() - (days * 24 * 60 * 60 * 1000));
|
695
|
-
const allData = [];
|
696
|
-
// Read log files and aggregate data
|
697
|
-
const { readdirSync } = require('fs');
|
698
|
-
const logFiles = readdirSync(logsDir).filter((file) => file.startsWith('interactions-') && file.endsWith('.jsonl'));
|
699
|
-
for (const file of logFiles) {
|
700
|
-
const fileDate = new Date(file.replace('interactions-', '').replace('.jsonl', ''));
|
701
|
-
if (fileDate >= cutoffDate) {
|
702
|
-
const content = (0, fs_1.readFileSync)((0, path_1.join)(logsDir, file), 'utf-8');
|
703
|
-
const lines = content.trim().split('\n').filter(line => line.trim());
|
704
|
-
for (const line of lines) {
|
705
|
-
try {
|
706
|
-
allData.push(JSON.parse(line));
|
707
|
-
}
|
708
|
-
catch (e) {
|
709
|
-
// Skip malformed lines
|
710
|
-
}
|
711
|
-
}
|
712
|
-
}
|
713
|
-
}
|
714
|
-
return {
|
715
|
-
toolResult: JSON.stringify({
|
716
|
-
message: `Exported ${allData.length} training data records from last ${days} days`,
|
717
|
-
format: format,
|
718
|
-
data_count: allData.length,
|
719
|
-
data: format === 'jsonl' ? allData : allData.map(item => ({
|
720
|
-
timestamp: item.timestamp,
|
721
|
-
tool: item.tool_name,
|
722
|
-
user_input: JSON.stringify(item.user_input),
|
723
|
-
parsed_output: JSON.stringify(item.parsed_output),
|
724
|
-
success: item.success_metrics?.operation_success,
|
725
|
-
confidence: item.success_metrics?.confidence_score
|
726
|
-
}))
|
727
|
-
}, null, 2),
|
728
|
-
isError: false
|
729
|
-
};
|
730
|
-
}
|
731
|
-
catch (error) {
|
732
|
-
return {
|
733
|
-
toolResult: JSON.stringify({
|
734
|
-
error: "Failed to export training data",
|
735
|
-
message: error.message
|
736
|
-
}, null, 2),
|
737
|
-
isError: true
|
738
|
-
};
|
739
|
-
}
|
740
|
-
}
|
741
|
-
if (req.tool === "provide_feedback") {
|
742
|
-
const { original_input, parsed_result, correct_interpretation, feedback_notes } = req.arguments;
|
743
|
-
const sessionId = generateSessionId();
|
744
|
-
// Log feedback for future model improvements
|
745
|
-
logInteraction({
|
746
|
-
session_id: sessionId,
|
747
|
-
tool_name: 'provide_feedback',
|
748
|
-
user_input: { original_input, feedback_notes },
|
749
|
-
parsed_output: { parsed_result, correct_interpretation },
|
750
|
-
user_feedback: feedback_notes,
|
751
|
-
success_metrics: {
|
752
|
-
operation_success: false, // This indicates the original parsing was wrong
|
753
|
-
user_satisfaction: 0 // User had to provide feedback = dissatisfied
|
754
|
-
}
|
755
|
-
});
|
756
|
-
return {
|
757
|
-
toolResult: JSON.stringify({
|
758
|
-
message: "Thank you for the feedback! This will help improve future intent recognition.",
|
759
|
-
feedback_recorded: true,
|
760
|
-
original_input: original_input,
|
761
|
-
feedback_notes: feedback_notes
|
762
|
-
}, null, 2),
|
763
|
-
isError: false
|
764
|
-
};
|
765
|
-
}
|
766
|
-
if (req.tool !== "create_vm")
|
767
|
-
return;
|
768
|
-
const { name, group, cloud, zone, template, size, distribution, count } = req.arguments;
|
769
|
-
// Allow both 'cloud' and 'zone' parameters interchangeably
|
770
|
-
const location = cloud || zone;
|
771
|
-
if (!location) {
|
772
|
-
return {
|
773
|
-
error: {
|
774
|
-
code: "missing_location",
|
775
|
-
message: "Either 'cloud' or 'zone' parameter is required"
|
776
|
-
}
|
777
|
-
};
|
778
|
-
}
|
779
|
-
// Parse VM names and determine distribution strategy
|
780
|
-
const vmNames = parseVMNames(name, count);
|
781
|
-
const nodes = await getClusterNodes();
|
782
|
-
// Determine node assignment strategy
|
783
|
-
let nodeAssignments = [];
|
784
|
-
if (distribution === 'spread' || (vmNames.length > 1 && !distribution)) {
|
785
|
-
// Distribute VMs across all available nodes
|
786
|
-
vmNames.forEach((vmName, index) => {
|
787
|
-
const nodeId = nodes[index % nodes.length];
|
788
|
-
nodeAssignments.push({ name: vmName, kvmHostId: nodeId });
|
789
|
-
});
|
790
|
-
}
|
791
|
-
else if (distribution && distribution !== 'auto') {
|
792
|
-
// Parse specific node assignments like "1,2,3" or "node1,node2,node3"
|
793
|
-
const specifiedNodes = distribution.split(',').map((n) => {
|
794
|
-
const trimmed = n.trim();
|
795
|
-
return trimmed.startsWith('node') ? parseInt(trimmed.replace('node', '')) : parseInt(trimmed);
|
796
|
-
}).filter((n) => !isNaN(n));
|
797
|
-
if (specifiedNodes.length > 0) {
|
798
|
-
vmNames.forEach((vmName, index) => {
|
799
|
-
const nodeId = specifiedNodes[index % specifiedNodes.length];
|
800
|
-
nodeAssignments.push({ name: vmName, kvmHostId: nodeId });
|
801
|
-
});
|
802
|
-
}
|
803
|
-
else {
|
804
|
-
nodeAssignments = vmNames.map(vmName => ({ name: vmName }));
|
805
|
-
}
|
806
|
-
}
|
807
|
-
else {
|
808
|
-
// Auto-placement (no kvmHostId specified)
|
809
|
-
nodeAssignments = vmNames.map(vmName => ({ name: vmName }));
|
810
|
-
}
|
811
|
-
const resolved = await resolveInput({ group, cloud: location, template, size });
|
812
|
-
const { groupId, cloudId, instanceTypeId, servicePlanId, imageId } = resolved;
|
813
|
-
if (!groupId || !cloudId || !instanceTypeId || !servicePlanId || !imageId) {
|
814
|
-
const errors = [];
|
815
|
-
if (!groupId)
|
816
|
-
errors.push(`Group '${group}' not found. Available: ${resolved.availableGroups.join(', ')}`);
|
817
|
-
if (!cloudId)
|
818
|
-
errors.push(`Zone/Cloud '${location}' not found. Available: ${resolved.availableZones.join(', ')}`);
|
819
|
-
if (!instanceTypeId)
|
820
|
-
errors.push(`Instance type could not be resolved`);
|
821
|
-
if (!servicePlanId)
|
822
|
-
errors.push(`Size '${size}' could not be resolved to service plan`);
|
823
|
-
if (!imageId)
|
824
|
-
errors.push(`Template '${template}' could not be resolved to OS image`);
|
825
|
-
return {
|
826
|
-
error: {
|
827
|
-
code: "resolution_failed",
|
828
|
-
message: `Failed to resolve parameters:\n${errors.join('\n')}`
|
829
|
-
}
|
830
|
-
};
|
831
|
-
}
|
832
|
-
// Create VMs sequentially
|
833
|
-
const results = [];
|
834
|
-
const errors = [];
|
835
|
-
for (const assignment of nodeAssignments) {
|
836
|
-
const vmConfig = {
|
837
|
-
resourcePoolId: 'pool-1',
|
838
|
-
poolProviderType: 'mvm',
|
839
|
-
imageId: imageId,
|
840
|
-
createUser: true
|
841
|
-
};
|
842
|
-
// Add kvmHostId only if explicitly specified
|
843
|
-
if (assignment.kvmHostId) {
|
844
|
-
vmConfig.kvmHostId = assignment.kvmHostId;
|
845
|
-
}
|
846
|
-
const payload = {
|
847
|
-
zoneId: cloudId,
|
848
|
-
instance: {
|
849
|
-
name: assignment.name,
|
850
|
-
cloud: 'tc-lab',
|
851
|
-
hostName: assignment.name,
|
852
|
-
type: 'mvm',
|
853
|
-
instanceType: {
|
854
|
-
code: 'mvm'
|
855
|
-
},
|
856
|
-
site: {
|
857
|
-
id: groupId
|
858
|
-
},
|
859
|
-
layout: {
|
860
|
-
id: 2, // Single HPE VM
|
861
|
-
code: 'mvm-1.0-single'
|
862
|
-
},
|
863
|
-
plan: {
|
864
|
-
id: servicePlanId
|
865
|
-
}
|
866
|
-
},
|
867
|
-
config: vmConfig,
|
868
|
-
volumes: [
|
869
|
-
{
|
870
|
-
id: -1,
|
871
|
-
rootVolume: true,
|
872
|
-
name: 'root',
|
873
|
-
size: 10,
|
874
|
-
storageType: 1,
|
875
|
-
datastoreId: 5
|
876
|
-
}
|
877
|
-
],
|
878
|
-
networkInterfaces: [
|
879
|
-
{
|
880
|
-
primaryInterface: true,
|
881
|
-
ipMode: 'dhcp',
|
882
|
-
network: {
|
883
|
-
id: 'network-2'
|
884
|
-
},
|
885
|
-
networkInterfaceTypeId: 10
|
886
|
-
}
|
887
|
-
],
|
888
|
-
layoutSize: 1
|
889
|
-
};
|
890
|
-
try {
|
891
|
-
const response = await api.post("/instances", payload);
|
892
|
-
const vm = response.data?.instance;
|
893
|
-
const nodeInfo = assignment.kvmHostId ? ` on node ${assignment.kvmHostId}` : ' (auto-placed)';
|
894
|
-
results.push(`✅ '${vm.name}' created (ID: ${vm.id})${nodeInfo}`);
|
895
|
-
}
|
896
|
-
catch (err) {
|
897
|
-
const nodeInfo = assignment.kvmHostId ? ` on node ${assignment.kvmHostId}` : '';
|
898
|
-
errors.push(`❌ '${assignment.name}' failed${nodeInfo}: ${err.response?.data?.message || err.message}`);
|
899
|
-
}
|
900
|
-
}
|
901
|
-
// Prepare response
|
902
|
-
const summary = [];
|
903
|
-
if (results.length > 0) {
|
904
|
-
summary.push(`Successfully created ${results.length} VM(s):`);
|
905
|
-
summary.push(...results);
|
906
|
-
}
|
907
|
-
if (errors.length > 0) {
|
908
|
-
summary.push(`\nFailed to create ${errors.length} VM(s):`);
|
909
|
-
summary.push(...errors);
|
910
|
-
}
|
911
|
-
summary.push(`\nResolved parameters:`);
|
912
|
-
summary.push(`- Group: ${resolved.resolvedGroup}`);
|
913
|
-
summary.push(`- Zone/Cloud: ${resolved.resolvedCloud}`);
|
914
|
-
summary.push(`- Template: ${resolved.resolvedImage}`);
|
915
|
-
summary.push(`- Plan: ${resolved.resolvedPlan}`);
|
916
|
-
if (distribution === 'spread' || (vmNames.length > 1 && !distribution)) {
|
917
|
-
summary.push(`- Distribution: Spread across nodes ${nodes.join(', ')}`);
|
918
|
-
}
|
919
|
-
else if (nodeAssignments.some(a => a.kvmHostId)) {
|
920
|
-
summary.push(`- Distribution: Specific node placement`);
|
921
|
-
}
|
922
|
-
else {
|
923
|
-
summary.push(`- Distribution: Auto-placement`);
|
924
|
-
}
|
925
|
-
return {
|
926
|
-
toolResult: summary.join('\n'),
|
927
|
-
isError: errors.length > 0 && results.length === 0
|
928
|
-
};
|
929
|
-
}
|
930
|
-
}
|
931
|
-
}
|
49
|
+
main().catch((error) => {
|
50
|
+
console.error("Failed to start server:", error);
|
51
|
+
process.exit(1);
|
932
52
|
});
|
933
|
-
const transport = new stdio_js_1.StdioServerTransport();
|
934
|
-
server.connect(transport);
|