vme-mcp-server 0.1.6 → 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 +18 -5
- package/dist/lib/capability-cache.js +131 -0
- package/dist/lib/capability-discovery.js +240 -0
- package/dist/lib/name-resolver.js +174 -0
- package/dist/lib/vm-parsing.js +27 -19
- package/dist/tools/check-capability.js +84 -0
- package/dist/tools/create-vm.js +95 -23
- package/dist/tools/discover-capabilities.js +79 -0
- package/dist/tools/get-cache-status.js +123 -0
- package/dist/tools/get-resources.js +1 -1
- package/dist/tools/index.js +37 -10
- package/dist/tools/query-resources.js +371 -0
- package/package.json +4 -4
@@ -0,0 +1,371 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.handleQueryResources = exports.queryResourcesTool = void 0;
|
4
|
+
const api_utils_js_1 = require("../lib/api-utils.js");
|
5
|
+
exports.queryResourcesTool = {
|
6
|
+
name: "query_resources",
|
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
|
+
inputSchema: {
|
9
|
+
type: "object",
|
10
|
+
properties: {
|
11
|
+
natural_query: {
|
12
|
+
type: "string",
|
13
|
+
description: "Natural language query like 'Windows VMs', 'stopped VMs in production', 'DNS servers', 'VMs named ns01', 'VMs on management network', 'any VMs?'"
|
14
|
+
},
|
15
|
+
limit: {
|
16
|
+
type: "number",
|
17
|
+
description: "Maximum number of results to return (default: 10, max: 100)",
|
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
|
25
|
+
}
|
26
|
+
},
|
27
|
+
required: ["natural_query"]
|
28
|
+
}
|
29
|
+
};
|
30
|
+
function parseResourceQuery(query, limit = 10) {
|
31
|
+
const q = query.toLowerCase().trim();
|
32
|
+
// Determine resource type
|
33
|
+
let resource = 'instances'; // default
|
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')) {
|
41
|
+
resource = 'hosts';
|
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
|
+
}
|
47
|
+
else if (q.includes('network')) {
|
48
|
+
resource = 'networks';
|
49
|
+
}
|
50
|
+
// Determine action
|
51
|
+
let action = 'list';
|
52
|
+
if (q.includes('how many') || q.includes('count')) {
|
53
|
+
action = 'count';
|
54
|
+
}
|
55
|
+
else if (q.includes('any ') || q.includes('summary')) {
|
56
|
+
action = 'summary';
|
57
|
+
}
|
58
|
+
// Extract filters
|
59
|
+
const filters = {};
|
60
|
+
// OS Type patterns
|
61
|
+
if (q.includes('windows'))
|
62
|
+
filters.osType = 'windows';
|
63
|
+
if (q.includes('linux'))
|
64
|
+
filters.osType = 'linux';
|
65
|
+
if (q.includes('ubuntu'))
|
66
|
+
filters.osType = 'ubuntu';
|
67
|
+
if (q.includes('centos'))
|
68
|
+
filters.osType = 'centos';
|
69
|
+
if (q.includes('rocky'))
|
70
|
+
filters.osType = 'rocky';
|
71
|
+
if (q.includes('debian'))
|
72
|
+
filters.osType = 'debian';
|
73
|
+
if (q.includes('rhel'))
|
74
|
+
filters.osType = 'rhel';
|
75
|
+
// Status patterns
|
76
|
+
if (q.includes('running'))
|
77
|
+
filters.status = 'running';
|
78
|
+
if (q.includes('stopped'))
|
79
|
+
filters.status = 'stopped';
|
80
|
+
if (q.includes('failed') || q.includes('error'))
|
81
|
+
filters.status = 'failed';
|
82
|
+
if (q.includes('provisioning'))
|
83
|
+
filters.status = 'provisioning';
|
84
|
+
// Group patterns
|
85
|
+
if (q.includes('production') || q.includes('prod'))
|
86
|
+
filters.group = 'production';
|
87
|
+
if (q.includes('development') || q.includes('dev'))
|
88
|
+
filters.group = 'development';
|
89
|
+
if (q.includes('test') || q.includes('testing'))
|
90
|
+
filters.group = 'test';
|
91
|
+
// Name patterns (exact matches)
|
92
|
+
const nameMatch = q.match(/named?\s+["']?([a-zA-Z0-9\-_]+)["']?/);
|
93
|
+
if (nameMatch) {
|
94
|
+
filters.name = nameMatch[1];
|
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
|
+
}
|
109
|
+
return { resource, action, filters, limit };
|
110
|
+
}
|
111
|
+
function filterResults(data, filters) {
|
112
|
+
return data.filter(item => {
|
113
|
+
// OS Type filtering
|
114
|
+
if (filters.osType) {
|
115
|
+
const itemOS = item.osType?.toLowerCase() || '';
|
116
|
+
const filterOS = filters.osType.toLowerCase();
|
117
|
+
if (filterOS === 'windows' && !itemOS.includes('windows'))
|
118
|
+
return false;
|
119
|
+
if (filterOS === 'linux' && !itemOS.includes('linux') && !itemOS.includes('ubuntu') && !itemOS.includes('centos') && !itemOS.includes('rocky') && !itemOS.includes('debian') && !itemOS.includes('rhel'))
|
120
|
+
return false;
|
121
|
+
if (filterOS !== 'windows' && filterOS !== 'linux' && !itemOS.includes(filterOS))
|
122
|
+
return false;
|
123
|
+
}
|
124
|
+
// Status filtering
|
125
|
+
if (filters.status) {
|
126
|
+
const itemStatus = item.status?.toLowerCase() || '';
|
127
|
+
if (!itemStatus.includes(filters.status.toLowerCase()))
|
128
|
+
return false;
|
129
|
+
}
|
130
|
+
// Group filtering
|
131
|
+
if (filters.group) {
|
132
|
+
const itemGroup = item.group?.name?.toLowerCase() || item.site?.name?.toLowerCase() || '';
|
133
|
+
if (!itemGroup.includes(filters.group.toLowerCase()))
|
134
|
+
return false;
|
135
|
+
}
|
136
|
+
// Name filtering
|
137
|
+
if (filters.name) {
|
138
|
+
const itemName = item.name?.toLowerCase() || '';
|
139
|
+
if (!itemName.includes(filters.name.toLowerCase()))
|
140
|
+
return false;
|
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
|
+
}
|
151
|
+
return true;
|
152
|
+
});
|
153
|
+
}
|
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) {
|
239
|
+
const { action, resource, filters } = parsedQuery;
|
240
|
+
if (action === 'count') {
|
241
|
+
return {
|
242
|
+
action: 'count',
|
243
|
+
resource: resource,
|
244
|
+
filters_applied: filters,
|
245
|
+
total_count: results.length,
|
246
|
+
query_summary: `Found ${results.length} ${resource} matching criteria`
|
247
|
+
};
|
248
|
+
}
|
249
|
+
if (action === 'summary') {
|
250
|
+
const statusCounts = {};
|
251
|
+
const osCounts = {};
|
252
|
+
results.forEach(item => {
|
253
|
+
const status = item.status || 'unknown';
|
254
|
+
const os = item.osType || 'unknown';
|
255
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
256
|
+
osCounts[os] = (osCounts[os] || 0) + 1;
|
257
|
+
});
|
258
|
+
return {
|
259
|
+
action: 'summary',
|
260
|
+
resource: resource,
|
261
|
+
filters_applied: filters,
|
262
|
+
total_count: results.length,
|
263
|
+
summary: {
|
264
|
+
by_status: statusCounts,
|
265
|
+
by_os_type: osCounts
|
266
|
+
},
|
267
|
+
query_summary: `Found ${results.length} ${resource} matching criteria`
|
268
|
+
};
|
269
|
+
}
|
270
|
+
// Default: list action
|
271
|
+
const limitedResults = results.slice(0, parsedQuery.limit);
|
272
|
+
return {
|
273
|
+
action: 'list',
|
274
|
+
resource: resource,
|
275
|
+
filters_applied: filters,
|
276
|
+
total_count: results.length,
|
277
|
+
returned_count: limitedResults.length,
|
278
|
+
detail_level: detailLevel,
|
279
|
+
items: limitedResults.map(item => mapItemByDetailLevel(item, resource, detailLevel)),
|
280
|
+
query_summary: `Showing ${limitedResults.length} of ${results.length} ${resource} matching criteria`
|
281
|
+
};
|
282
|
+
}
|
283
|
+
async function handleQueryResources(args) {
|
284
|
+
try {
|
285
|
+
const { natural_query, limit = 10, detail_level = 2 } = args;
|
286
|
+
if (!natural_query || typeof natural_query !== 'string') {
|
287
|
+
return {
|
288
|
+
content: [
|
289
|
+
{
|
290
|
+
type: "text",
|
291
|
+
text: JSON.stringify({
|
292
|
+
error: "Invalid input",
|
293
|
+
message: "natural_query parameter is required and must be a string",
|
294
|
+
examples: ["any VMs?", "Windows VMs", "stopped VMs in production", "Ubuntu hosts"]
|
295
|
+
}, null, 2)
|
296
|
+
}
|
297
|
+
],
|
298
|
+
isError: true
|
299
|
+
};
|
300
|
+
}
|
301
|
+
const parsedQuery = parseResourceQuery(natural_query, Math.min(limit, 100));
|
302
|
+
// Map resource type to API endpoint
|
303
|
+
const endpointMap = {
|
304
|
+
'instances': '/instances',
|
305
|
+
'hosts': '/servers',
|
306
|
+
'networks': '/networks',
|
307
|
+
'cluster_nodes': '/clusters'
|
308
|
+
};
|
309
|
+
const endpoint = endpointMap[parsedQuery.resource];
|
310
|
+
// Query the VME API
|
311
|
+
const response = await api_utils_js_1.api.get(endpoint);
|
312
|
+
// Extract data array from response
|
313
|
+
let rawData = [];
|
314
|
+
if (parsedQuery.resource === 'instances' && response.data.instances) {
|
315
|
+
rawData = response.data.instances;
|
316
|
+
}
|
317
|
+
else if (parsedQuery.resource === 'hosts' && response.data.servers) {
|
318
|
+
rawData = response.data.servers;
|
319
|
+
}
|
320
|
+
else if (parsedQuery.resource === 'networks' && response.data.networks) {
|
321
|
+
rawData = response.data.networks;
|
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
|
+
}
|
333
|
+
else {
|
334
|
+
// Fallback: try to find array in response
|
335
|
+
const possibleArrays = Object.values(response.data).filter(Array.isArray);
|
336
|
+
if (possibleArrays.length > 0) {
|
337
|
+
rawData = possibleArrays[0];
|
338
|
+
}
|
339
|
+
}
|
340
|
+
// Apply filters
|
341
|
+
const filteredResults = filterResults(rawData, parsedQuery.filters);
|
342
|
+
// Format response based on action
|
343
|
+
const formattedResponse = formatResponse(parsedQuery, filteredResults, rawData.length, detail_level);
|
344
|
+
return {
|
345
|
+
content: [
|
346
|
+
{
|
347
|
+
type: "text",
|
348
|
+
text: JSON.stringify(formattedResponse, null, 2)
|
349
|
+
}
|
350
|
+
],
|
351
|
+
isError: false
|
352
|
+
};
|
353
|
+
}
|
354
|
+
catch (error) {
|
355
|
+
return {
|
356
|
+
content: [
|
357
|
+
{
|
358
|
+
type: "text",
|
359
|
+
text: JSON.stringify({
|
360
|
+
error: "Resource query failed",
|
361
|
+
message: error.message,
|
362
|
+
natural_query: args?.natural_query || "unknown",
|
363
|
+
suggestion: "Check VME API connectivity or try discover_capabilities first to see available resources"
|
364
|
+
}, null, 2)
|
365
|
+
}
|
366
|
+
],
|
367
|
+
isError: true
|
368
|
+
};
|
369
|
+
}
|
370
|
+
}
|
371
|
+
exports.handleQueryResources = handleQueryResources;
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "vme-mcp-server",
|
3
|
-
"version": "0.1.
|
4
|
-
"description": "
|
3
|
+
"version": "0.1.8",
|
4
|
+
"description": "",
|
5
5
|
"main": "dist/server.js",
|
6
6
|
"bin": {
|
7
7
|
"vme-mcp-server": "./dist/server.js"
|
@@ -47,8 +47,8 @@
|
|
47
47
|
},
|
48
48
|
"devDependencies": {
|
49
49
|
"@types/node": "^22.15.29",
|
50
|
+
"chai": "^5.2.0",
|
50
51
|
"mocha": "^10.2.0",
|
51
52
|
"nyc": "^15.1.0"
|
52
|
-
}
|
53
|
-
"description": ""
|
53
|
+
}
|
54
54
|
}
|