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.
@@ -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
- const prodCluster = clusters.data.clusters.find((cluster) => cluster.name === "prod01");
21
- if (prodCluster && prodCluster.servers) {
22
- return prodCluster.servers.map((server) => server.id);
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;
@@ -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 "web01->web03" or "web01 -> web03"
7
- const rangeMatch = nameInput.match(/^(.+?)(\d+)\s*->\s*(.+?)(\d+)$/);
8
- if (rangeMatch) {
9
- const [, prefix1, start, prefix2, end] = rangeMatch;
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 && distribution !== 'auto') {
61
- // Parse specific node assignments like "1,2,3" or "node1,node2,node3"
62
- const specifiedNodes = distribution.split(',').map((n) => {
63
- const trimmed = n.trim();
64
- return trimmed.startsWith('node') ? parseInt(trimmed.replace('node', '')) : parseInt(trimmed);
65
- }).filter((n) => !isNaN(n));
66
- if (specifiedNodes.length > 0) {
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)
@@ -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 with intelligent filtering and caching",
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, hosts, networks) with natural language. Use discover_capabilities first to learn available OS types, groups, and statuses in your environment.",
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', 'any VMs?', 'Ubuntu hosts', 'VMs in development group'"
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('host') || q.includes('server') || q.includes('node')) {
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 formatResponse(parsedQuery, results, totalCount) {
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
- items: limitedResults.map(item => ({
157
- id: item.id,
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
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vme-mcp-server",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "",
5
5
  "main": "dist/server.js",
6
6
  "bin": {