vme-mcp-server 0.1.7 → 0.1.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/dist/lib/api-utils.js
CHANGED
@@ -13,19 +13,32 @@ exports.api = axios_1.default.create({
|
|
13
13
|
"Content-Type": "application/json"
|
14
14
|
}
|
15
15
|
});
|
16
|
-
// Helper function to get available cluster nodes
|
16
|
+
// Helper function to get available cluster nodes (hypervisor hosts, not VMs)
|
17
17
|
async function getClusterNodes() {
|
18
18
|
try {
|
19
|
+
// Get actual cluster hypervisor nodes from /clusters endpoint
|
19
20
|
const clusters = await exports.api.get("/clusters");
|
20
|
-
|
21
|
-
if (
|
22
|
-
|
21
|
+
let allNodes = [];
|
22
|
+
if (clusters.data.clusters) {
|
23
|
+
for (const cluster of clusters.data.clusters) {
|
24
|
+
if (cluster.servers && Array.isArray(cluster.servers)) {
|
25
|
+
const clusterNodes = cluster.servers.map((server) => server.id).filter((id) => id);
|
26
|
+
allNodes.push(...clusterNodes);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
// Remove duplicates and sort
|
31
|
+
const uniqueNodes = [...new Set(allNodes)].sort((a, b) => a - b);
|
32
|
+
if (uniqueNodes.length > 0) {
|
33
|
+
console.log(`Found ${uniqueNodes.length} cluster hypervisor nodes: ${uniqueNodes.join(', ')}`);
|
34
|
+
return uniqueNodes;
|
23
35
|
}
|
24
36
|
}
|
25
37
|
catch (error) {
|
26
38
|
console.error('Error getting cluster nodes:', error);
|
27
39
|
}
|
28
|
-
// Fallback to known nodes if API fails
|
40
|
+
// Fallback to known hypervisor nodes if API fails
|
41
|
+
console.warn('Using fallback hypervisor nodes [1, 2, 3] - could not detect cluster hosts');
|
29
42
|
return [1, 2, 3];
|
30
43
|
}
|
31
44
|
exports.getClusterNodes = getClusterNodes;
|
package/dist/lib/vm-parsing.js
CHANGED
@@ -3,10 +3,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.calculateNodeAssignments = exports.parseVMNames = void 0;
|
4
4
|
// Helper function to parse VM name patterns
|
5
5
|
function parseVMNames(nameInput, count) {
|
6
|
-
// Handle range patterns like "
|
7
|
-
const
|
8
|
-
if (
|
9
|
-
const [, prefix1,
|
6
|
+
// Handle letter range patterns like "ws-a->ws-x" or "ws-a -> ws-x"
|
7
|
+
const letterRangeMatch = nameInput.match(/^(.+?)([a-z])\s*->\s*(.+?)([a-z])$/i);
|
8
|
+
if (letterRangeMatch) {
|
9
|
+
const [, prefix1, startLetter, prefix2, endLetter] = letterRangeMatch;
|
10
|
+
if (prefix1.trim() === prefix2.trim()) {
|
11
|
+
const basePrefix = prefix1.trim();
|
12
|
+
const startCode = startLetter.toLowerCase().charCodeAt(0);
|
13
|
+
const endCode = endLetter.toLowerCase().charCodeAt(0);
|
14
|
+
const names = [];
|
15
|
+
for (let i = startCode; i <= endCode; i++) {
|
16
|
+
const letter = String.fromCharCode(i);
|
17
|
+
names.push(`${basePrefix}${letter}`);
|
18
|
+
}
|
19
|
+
return names;
|
20
|
+
}
|
21
|
+
}
|
22
|
+
// Handle numeric range patterns like "web01->web03" or "web01 -> web03"
|
23
|
+
const numericRangeMatch = nameInput.match(/^(.+?)(\d+)\s*->\s*(.+?)(\d+)$/);
|
24
|
+
if (numericRangeMatch) {
|
25
|
+
const [, prefix1, start, prefix2, end] = numericRangeMatch;
|
10
26
|
if (prefix1.trim() === prefix2.trim()) {
|
11
27
|
const basePrefix = prefix1.trim();
|
12
28
|
const startNum = parseInt(start);
|
@@ -57,21 +73,13 @@ function calculateNodeAssignments(vmNames, nodes, distribution) {
|
|
57
73
|
nodeAssignments.push({ name: vmName, kvmHostId: nodeId });
|
58
74
|
});
|
59
75
|
}
|
60
|
-
else if (distribution &&
|
61
|
-
//
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
vmNames.forEach((vmName, index) => {
|
68
|
-
const nodeId = specifiedNodes[index % specifiedNodes.length];
|
69
|
-
nodeAssignments.push({ name: vmName, kvmHostId: nodeId });
|
70
|
-
});
|
71
|
-
}
|
72
|
-
else {
|
73
|
-
nodeAssignments = vmNames.map(vmName => ({ name: vmName }));
|
74
|
-
}
|
76
|
+
else if (distribution && !['auto', 'spread'].includes(distribution)) {
|
77
|
+
// Distribution is a node/hypervisor name - signal that it needs resolution
|
78
|
+
// Return assignments with the node name for the calling function to resolve
|
79
|
+
nodeAssignments = vmNames.map(vmName => ({
|
80
|
+
name: vmName,
|
81
|
+
nodeNameToResolve: distribution
|
82
|
+
}));
|
75
83
|
}
|
76
84
|
else {
|
77
85
|
// Auto-placement (no kvmHostId specified)
|
package/dist/tools/create-vm.js
CHANGED
@@ -6,7 +6,7 @@ const api_utils_js_1 = require("../lib/api-utils.js");
|
|
6
6
|
const name_resolver_js_1 = require("../lib/name-resolver.js");
|
7
7
|
exports.createVMTool = {
|
8
8
|
name: "create_vm",
|
9
|
-
description: "Provision a new virtual machine",
|
9
|
+
description: "Provision a new virtual machine. ⚡ TIP: Run discover_capabilities first to see available groups, zones, templates, and sizes in your environment.",
|
10
10
|
inputSchema: {
|
11
11
|
type: "object",
|
12
12
|
properties: {
|
@@ -71,6 +71,30 @@ async function handleCreateVM(args) {
|
|
71
71
|
const nodes = await (0, api_utils_js_1.getClusterNodes)();
|
72
72
|
// Calculate node assignments
|
73
73
|
const nodeAssignments = (0, vm_parsing_js_1.calculateNodeAssignments)(vmNames, nodes, distribution);
|
74
|
+
// Resolve any node names to IDs
|
75
|
+
for (const assignment of nodeAssignments) {
|
76
|
+
if (assignment.nodeNameToResolve) {
|
77
|
+
// Query servers to find node with matching name
|
78
|
+
try {
|
79
|
+
const servers = await api_utils_js_1.api.get("/servers");
|
80
|
+
if (servers.data.servers) {
|
81
|
+
const foundServer = servers.data.servers.find((server) => server.name?.toLowerCase() === assignment.nodeNameToResolve?.toLowerCase());
|
82
|
+
if (foundServer) {
|
83
|
+
assignment.kvmHostId = foundServer.id;
|
84
|
+
delete assignment.nodeNameToResolve; // Clean up
|
85
|
+
}
|
86
|
+
else {
|
87
|
+
// If no exact name match, log available servers for debugging
|
88
|
+
const availableNames = servers.data.servers.map((s) => s.name).filter(Boolean);
|
89
|
+
console.warn(`Node '${assignment.nodeNameToResolve}' not found. Available: ${availableNames.join(', ')}`);
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
catch (error) {
|
94
|
+
console.error(`Failed to resolve node name '${assignment.nodeNameToResolve}':`, error);
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
74
98
|
// Use name resolver to get IDs from actual VME environment
|
75
99
|
const groupId = await name_resolver_js_1.nameResolver.resolveNameToId('group', group);
|
76
100
|
const cloudId = await name_resolver_js_1.nameResolver.resolveNameToId('zone', location);
|
@@ -4,7 +4,7 @@ exports.handleDiscoverCapabilities = exports.discoverCapabilitiesTool = void 0;
|
|
4
4
|
const capability_discovery_js_1 = require("../lib/capability-discovery.js");
|
5
5
|
exports.discoverCapabilitiesTool = {
|
6
6
|
name: "discover_capabilities",
|
7
|
-
description: "Discover VME infrastructure capabilities
|
7
|
+
description: "⚡ RECOMMENDED FIRST STEP: Discover VME infrastructure capabilities to learn available resources. Run this tool at least once per session to cache environment data for optimal performance with other tools.",
|
8
8
|
inputSchema: {
|
9
9
|
type: "object",
|
10
10
|
properties: {
|
@@ -4,7 +4,7 @@ exports.handleGetResources = exports.getResourcesTool = void 0;
|
|
4
4
|
const api_utils_js_1 = require("../lib/api-utils.js");
|
5
5
|
exports.getResourcesTool = {
|
6
6
|
name: "get_resources",
|
7
|
-
description: "Discover and explore available VME infrastructure resources with intelligent filtering",
|
7
|
+
description: "Discover and explore available VME infrastructure resources with intelligent filtering. ⚡ TIP: Use discover_capabilities for comprehensive environment discovery first.",
|
8
8
|
inputSchema: {
|
9
9
|
type: "object",
|
10
10
|
properties: {
|
@@ -4,18 +4,24 @@ exports.handleQueryResources = exports.queryResourcesTool = void 0;
|
|
4
4
|
const api_utils_js_1 = require("../lib/api-utils.js");
|
5
5
|
exports.queryResourcesTool = {
|
6
6
|
name: "query_resources",
|
7
|
-
description: "Query actual infrastructure resources (VMs,
|
7
|
+
description: "Query actual infrastructure resources (VMs, cluster nodes, networks) with natural language. Supports network placement queries. ⚡ TIP: Run discover_capabilities first to learn available OS types, groups, and statuses in your environment.",
|
8
8
|
inputSchema: {
|
9
9
|
type: "object",
|
10
10
|
properties: {
|
11
11
|
natural_query: {
|
12
12
|
type: "string",
|
13
|
-
description: "Natural language query like 'Windows VMs', 'stopped VMs in production', '
|
13
|
+
description: "Natural language query like 'Windows VMs', 'stopped VMs in production', 'DNS servers', 'VMs named ns01', 'VMs on management network', 'any VMs?'"
|
14
14
|
},
|
15
15
|
limit: {
|
16
16
|
type: "number",
|
17
17
|
description: "Maximum number of results to return (default: 10, max: 100)",
|
18
18
|
default: 10
|
19
|
+
},
|
20
|
+
detail_level: {
|
21
|
+
type: "number",
|
22
|
+
description: "Information detail level: 1=basic (name, status, IP), 2=standard (+ network, plan), 3=full (all available fields)",
|
23
|
+
enum: [1, 2, 3],
|
24
|
+
default: 2
|
19
25
|
}
|
20
26
|
},
|
21
27
|
required: ["natural_query"]
|
@@ -25,9 +31,19 @@ function parseResourceQuery(query, limit = 10) {
|
|
25
31
|
const q = query.toLowerCase().trim();
|
26
32
|
// Determine resource type
|
27
33
|
let resource = 'instances'; // default
|
28
|
-
if (q.includes('
|
34
|
+
if (q.includes('cluster') && (q.includes('node') || q.includes('host'))) {
|
35
|
+
resource = 'cluster_nodes';
|
36
|
+
}
|
37
|
+
else if (q.includes('hypervisor') || q.includes('vme01') || q.includes('vme02') || q.includes('vme03')) {
|
38
|
+
resource = 'cluster_nodes';
|
39
|
+
}
|
40
|
+
else if (q.includes('host') || q.includes('server')) {
|
29
41
|
resource = 'hosts';
|
30
42
|
}
|
43
|
+
else if (q.includes('node') && !q.includes('cluster')) {
|
44
|
+
// When someone just says "nodes" without "cluster", assume they want cluster nodes
|
45
|
+
resource = 'cluster_nodes';
|
46
|
+
}
|
31
47
|
else if (q.includes('network')) {
|
32
48
|
resource = 'networks';
|
33
49
|
}
|
@@ -77,6 +93,19 @@ function parseResourceQuery(query, limit = 10) {
|
|
77
93
|
if (nameMatch) {
|
78
94
|
filters.name = nameMatch[1];
|
79
95
|
}
|
96
|
+
// Network patterns
|
97
|
+
if (q.includes('network') || q.includes('vlan')) {
|
98
|
+
const networkMatch = q.match(/on\s+["']?([a-zA-Z0-9\-_\s]+)["']?/);
|
99
|
+
if (networkMatch) {
|
100
|
+
filters.network = networkMatch[1].trim();
|
101
|
+
}
|
102
|
+
else if (q.includes('management')) {
|
103
|
+
filters.network = 'management';
|
104
|
+
}
|
105
|
+
else if (q.includes('compute')) {
|
106
|
+
filters.network = 'compute';
|
107
|
+
}
|
108
|
+
}
|
80
109
|
return { resource, action, filters, limit };
|
81
110
|
}
|
82
111
|
function filterResults(data, filters) {
|
@@ -110,10 +139,103 @@ function filterResults(data, filters) {
|
|
110
139
|
if (!itemName.includes(filters.name.toLowerCase()))
|
111
140
|
return false;
|
112
141
|
}
|
142
|
+
// Network filtering
|
143
|
+
if (filters.network) {
|
144
|
+
const networkName = item.interfaces?.[0]?.network?.name?.toLowerCase() || '';
|
145
|
+
const networkPool = item.interfaces?.[0]?.network?.pool?.name?.toLowerCase() || '';
|
146
|
+
const filterNetwork = filters.network.toLowerCase();
|
147
|
+
if (!networkName.includes(filterNetwork) && !networkPool.includes(filterNetwork)) {
|
148
|
+
return false;
|
149
|
+
}
|
150
|
+
}
|
113
151
|
return true;
|
114
152
|
});
|
115
153
|
}
|
116
|
-
function
|
154
|
+
function mapItemByDetailLevel(item, resource, detailLevel) {
|
155
|
+
// Level 1: Basic information only
|
156
|
+
const basicInfo = {
|
157
|
+
id: item.id,
|
158
|
+
name: item.name,
|
159
|
+
status: item.status
|
160
|
+
};
|
161
|
+
if (detailLevel === 1) {
|
162
|
+
return {
|
163
|
+
...basicInfo,
|
164
|
+
...(resource === 'instances' && {
|
165
|
+
ipAddress: item.connectionInfo?.[0]?.ip || item.externalIp || item.internalIp
|
166
|
+
})
|
167
|
+
};
|
168
|
+
}
|
169
|
+
// Level 2: Standard information (default)
|
170
|
+
const standardInfo = {
|
171
|
+
...basicInfo,
|
172
|
+
osType: item.osType,
|
173
|
+
group: item.group?.name || item.site?.name || 'unknown',
|
174
|
+
...(resource === 'instances' && {
|
175
|
+
plan: item.plan?.name,
|
176
|
+
powerState: item.powerState,
|
177
|
+
ipAddress: item.connectionInfo?.[0]?.ip || item.externalIp || item.internalIp,
|
178
|
+
networkName: item.interfaces?.[0]?.network?.name,
|
179
|
+
networkPool: item.interfaces?.[0]?.network?.pool?.name
|
180
|
+
})
|
181
|
+
};
|
182
|
+
if (detailLevel === 2) {
|
183
|
+
return standardInfo;
|
184
|
+
}
|
185
|
+
// Level 3: Full information
|
186
|
+
return {
|
187
|
+
...standardInfo,
|
188
|
+
...(resource === 'instances' && {
|
189
|
+
networkId: item.interfaces?.[0]?.network?.id,
|
190
|
+
interfaceCount: item.interfaces?.length || 0,
|
191
|
+
allInterfaces: item.interfaces?.map((iface) => ({
|
192
|
+
id: iface.id,
|
193
|
+
networkName: iface.network?.name,
|
194
|
+
ipAddress: iface.ipAddress,
|
195
|
+
ipMode: iface.ipMode
|
196
|
+
})) || [],
|
197
|
+
firewallEnabled: item.firewallEnabled,
|
198
|
+
networkLevel: item.networkLevel,
|
199
|
+
externalId: item.externalId,
|
200
|
+
hostname: item.hostname,
|
201
|
+
displayName: item.displayName,
|
202
|
+
maxMemory: item.maxMemory,
|
203
|
+
maxCores: item.maxCores,
|
204
|
+
dateCreated: item.dateCreated,
|
205
|
+
lastUpdated: item.lastUpdated
|
206
|
+
}),
|
207
|
+
...(resource === 'hosts' && {
|
208
|
+
serverType: item.serverType?.name,
|
209
|
+
zone: item.zone?.name,
|
210
|
+
powerState: item.powerState,
|
211
|
+
agentInstalled: item.agentInstalled,
|
212
|
+
maxMemory: item.maxMemory,
|
213
|
+
maxCores: item.maxCores,
|
214
|
+
freeMemory: item.freeMemory,
|
215
|
+
usedMemory: item.usedMemory
|
216
|
+
}),
|
217
|
+
...(resource === 'networks' && {
|
218
|
+
type: item.type?.name,
|
219
|
+
zone: item.zone?.name,
|
220
|
+
pool: item.pool?.name,
|
221
|
+
dhcpServer: item.dhcpServer,
|
222
|
+
dnsPrimary: item.dnsPrimary,
|
223
|
+
dnsSecondary: item.dnsSecondary,
|
224
|
+
gateway: item.gateway,
|
225
|
+
netmask: item.netmask,
|
226
|
+
cidr: item.cidr
|
227
|
+
}),
|
228
|
+
...(resource === 'cluster_nodes' && {
|
229
|
+
hostname: item.hostname,
|
230
|
+
serverType: item.serverType?.name || item.serverType,
|
231
|
+
powerState: item.powerState,
|
232
|
+
cpuCount: item.maxCpu,
|
233
|
+
memoryGB: item.maxMemory ? Math.floor(item.maxMemory / (1024 * 1024 * 1024)) : undefined,
|
234
|
+
vmCount: item.instances?.length || 0
|
235
|
+
})
|
236
|
+
};
|
237
|
+
}
|
238
|
+
function formatResponse(parsedQuery, results, totalCount, detailLevel = 2) {
|
117
239
|
const { action, resource, filters } = parsedQuery;
|
118
240
|
if (action === 'count') {
|
119
241
|
return {
|
@@ -153,24 +275,14 @@ function formatResponse(parsedQuery, results, totalCount) {
|
|
153
275
|
filters_applied: filters,
|
154
276
|
total_count: results.length,
|
155
277
|
returned_count: limitedResults.length,
|
156
|
-
|
157
|
-
|
158
|
-
name: item.name,
|
159
|
-
status: item.status,
|
160
|
-
osType: item.osType,
|
161
|
-
group: item.group?.name || item.site?.name || 'unknown',
|
162
|
-
...(resource === 'instances' && {
|
163
|
-
plan: item.plan?.name,
|
164
|
-
powerState: item.powerState,
|
165
|
-
ipAddress: item.externalIp || item.internalIp
|
166
|
-
})
|
167
|
-
})),
|
278
|
+
detail_level: detailLevel,
|
279
|
+
items: limitedResults.map(item => mapItemByDetailLevel(item, resource, detailLevel)),
|
168
280
|
query_summary: `Showing ${limitedResults.length} of ${results.length} ${resource} matching criteria`
|
169
281
|
};
|
170
282
|
}
|
171
283
|
async function handleQueryResources(args) {
|
172
284
|
try {
|
173
|
-
const { natural_query, limit = 10 } = args;
|
285
|
+
const { natural_query, limit = 10, detail_level = 2 } = args;
|
174
286
|
if (!natural_query || typeof natural_query !== 'string') {
|
175
287
|
return {
|
176
288
|
content: [
|
@@ -191,7 +303,8 @@ async function handleQueryResources(args) {
|
|
191
303
|
const endpointMap = {
|
192
304
|
'instances': '/instances',
|
193
305
|
'hosts': '/servers',
|
194
|
-
'networks': '/networks'
|
306
|
+
'networks': '/networks',
|
307
|
+
'cluster_nodes': '/clusters'
|
195
308
|
};
|
196
309
|
const endpoint = endpointMap[parsedQuery.resource];
|
197
310
|
// Query the VME API
|
@@ -207,6 +320,16 @@ async function handleQueryResources(args) {
|
|
207
320
|
else if (parsedQuery.resource === 'networks' && response.data.networks) {
|
208
321
|
rawData = response.data.networks;
|
209
322
|
}
|
323
|
+
else if (parsedQuery.resource === 'cluster_nodes' && response.data.clusters) {
|
324
|
+
// Extract hypervisor nodes from clusters
|
325
|
+
const allNodes = [];
|
326
|
+
for (const cluster of response.data.clusters) {
|
327
|
+
if (cluster.servers && Array.isArray(cluster.servers)) {
|
328
|
+
allNodes.push(...cluster.servers);
|
329
|
+
}
|
330
|
+
}
|
331
|
+
rawData = allNodes;
|
332
|
+
}
|
210
333
|
else {
|
211
334
|
// Fallback: try to find array in response
|
212
335
|
const possibleArrays = Object.values(response.data).filter(Array.isArray);
|
@@ -217,7 +340,7 @@ async function handleQueryResources(args) {
|
|
217
340
|
// Apply filters
|
218
341
|
const filteredResults = filterResults(rawData, parsedQuery.filters);
|
219
342
|
// Format response based on action
|
220
|
-
const formattedResponse = formatResponse(parsedQuery, filteredResults, rawData.length);
|
343
|
+
const formattedResponse = formatResponse(parsedQuery, filteredResults, rawData.length, detail_level);
|
221
344
|
return {
|
222
345
|
content: [
|
223
346
|
{
|