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.
@@ -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.6",
4
- "description": "VMware vCenter MCP Server - Natural language infrastructure management for Claude",
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
  }