UNPKG

vme-mcp-server

Version:

An intelligent Model Context Protocol (MCP) server that transforms HPE VM Essentials (VME) infrastructure management into natural language conversations. Provision VMs, manage resources, and control your infrastructure through simple English commands with

274 lines (273 loc) 11.3 kB
import { parseVMNames, calculateNodeAssignments } from "../lib/vm-parsing.js"; import { api, getClusterNodes } from "../lib/api-utils.js"; import { nameResolver } from "../lib/name-resolver.js"; export const createVMTool = { name: "create_vm", description: "Provision a new virtual machine. ⚡ TIP: Run discover_capabilities first to see available groups, zones, templates, and sizes in your environment.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Name for VM(s). Supports patterns like 'web01' or 'web01->web03' for multiple VMs" }, group: { type: "string", description: "Group/site where VM will be created" }, cloud: { type: "string", description: "Cloud/zone where VM will be provisioned (also accepts 'zone')" }, zone: { type: "string", description: "Zone/cloud where VM will be provisioned (alias for 'cloud')" }, template: { type: "string", description: "VM template or operating system" }, size: { type: "string", description: "VM size (small, medium, 4GB, 8GB, etc.)" }, distribution: { type: "string", description: "VM distribution strategy: 'auto' (default), 'spread' (across all nodes), or 'node1,node2,node3' (specific nodes)" }, count: { type: "number", description: "Number of VMs to create (alternative to name patterns)" } }, required: ["name", "group", "template", "size"] } }; export async function handleCreateVM(args) { const { name, group, cloud, zone, template, size, distribution, count } = args; // Allow both 'cloud' and 'zone' parameters interchangeably const location = cloud || zone; if (!location) { return { content: [ { type: "text", text: JSON.stringify({ error: { code: "missing_location", message: "Either 'cloud' or 'zone' parameter is required" } }, null, 2) } ], isError: true }; } // Parse VM names and determine distribution strategy const vmNames = parseVMNames(name, count); const nodes = await getClusterNodes(); // Calculate node assignments const nodeAssignments = calculateNodeAssignments(vmNames, nodes, distribution); // Resolve any node names to IDs for (const assignment of nodeAssignments) { if (assignment.nodeNameToResolve) { // Query servers to find node with matching name try { const servers = await api.get("/servers"); if (servers.data.servers) { const foundServer = servers.data.servers.find((server) => server.name?.toLowerCase() === assignment.nodeNameToResolve?.toLowerCase()); if (foundServer) { assignment.kvmHostId = foundServer.id; delete assignment.nodeNameToResolve; // Clean up } else { // If no exact name match, log available servers for debugging const availableNames = servers.data.servers.map((s) => s.name).filter(Boolean); console.warn(`Node '${assignment.nodeNameToResolve}' not found. Available: ${availableNames.join(', ')}`); } } } catch (error) { console.error(`Failed to resolve node name '${assignment.nodeNameToResolve}':`, error); } } } // Use name resolver to get IDs from actual VME environment const groupId = await nameResolver.resolveNameToId('group', group); const cloudId = await nameResolver.resolveNameToId('zone', location); const imageId = await nameResolver.resolveNameToId('virtualImage', template); const servicePlanId = await nameResolver.resolveNameToId('servicePlan', size); const instanceTypeId = await nameResolver.resolveNameToId('instanceType', 'HPE VM'); // Default for VME // Validate all required IDs were resolved if (!groupId || !cloudId || !servicePlanId || !imageId) { const errors = []; if (!groupId) { const availableGroups = await nameResolver.getAvailableNames('group'); errors.push(`Group '${group}' not found. Available: ${availableGroups.join(', ')}`); } if (!cloudId) { const availableZones = await nameResolver.getAvailableNames('zone'); errors.push(`Zone/Cloud '${location}' not found. Available: ${availableZones.join(', ')}`); } if (!servicePlanId) { const availablePlans = await nameResolver.getAvailableNames('servicePlan'); errors.push(`Size '${size}' could not be resolved to service plan. Available: ${availablePlans.join(', ')}`); } if (!imageId) { const availableImages = await nameResolver.getAvailableNames('virtualImage'); errors.push(`Template '${template}' could not be resolved to OS image. Available: ${availableImages.join(', ')}`); } return { content: [ { type: "text", text: JSON.stringify({ error: { code: "resolution_failed", message: `Failed to resolve parameters:\n${errors.join('\n')}` } }, null, 2) } ], isError: true }; } // Get additional required IDs from VME environment let resourcePoolId = 'pool-1'; // Default fallback let datastoreId = 5; // Default fallback let networkId = 'network-2'; // Default fallback let layoutId = 2; // Default fallback try { // Try to get real resource pool, datastore, network IDs from VME const [resourcePools, datastores, networks, layouts] = await Promise.allSettled([ api.get('/resource-pools').catch(() => null), api.get('/datastores').catch(() => null), api.get('/networks').catch(() => null), api.get('/layouts').catch(() => null) ]); // Use first available resource pool if (resourcePools.status === 'fulfilled' && resourcePools.value?.data?.resourcePools?.[0]) { resourcePoolId = resourcePools.value.data.resourcePools[0].id; } // Use first available datastore if (datastores.status === 'fulfilled' && datastores.value?.data?.datastores?.[0]) { datastoreId = datastores.value.data.datastores[0].id; } // Use first available network if (networks.status === 'fulfilled' && networks.value?.data?.networks?.[0]) { networkId = networks.value.data.networks[0].id; } // Use first HPE VM layout if (layouts.status === 'fulfilled' && layouts.value?.data?.layouts) { const hpeLayout = layouts.value.data.layouts.find((l) => l.code?.includes('mvm') || l.name?.toLowerCase().includes('hpe')); if (hpeLayout) { layoutId = hpeLayout.id; } } } catch (error) { console.warn('Could not discover some VME resources, using defaults:', error.message); } // Create VMs sequentially const results = []; const errors = []; for (const assignment of nodeAssignments) { const vmConfig = { resourcePoolId: resourcePoolId, poolProviderType: 'mvm', imageId: imageId, createUser: true }; // Add kvmHostId only if explicitly specified if (assignment.kvmHostId) { vmConfig.kvmHostId = assignment.kvmHostId; } const payload = { zoneId: cloudId, instance: { name: assignment.name, cloud: await nameResolver.getAvailableNames('zone').then(zones => zones[0]) || 'tc-lab', hostName: assignment.name, type: 'mvm', instanceType: { code: 'mvm' }, site: { id: groupId }, layout: { id: layoutId, code: 'mvm-1.0-single' }, plan: { id: servicePlanId } }, config: vmConfig, volumes: [ { id: -1, rootVolume: true, name: 'root', size: 10, storageType: 1, datastoreId: datastoreId } ], networkInterfaces: [ { primaryInterface: true, ipMode: 'dhcp', network: { id: networkId }, networkInterfaceTypeId: 10 } ], layoutSize: 1 }; try { const response = await api.post("/instances", payload); const vm = response.data?.instance; const nodeInfo = assignment.kvmHostId ? ` on node ${assignment.kvmHostId}` : ' (auto-placed)'; results.push(`VM '${vm.name}' created (ID: ${vm.id})${nodeInfo}`); } catch (err) { const nodeInfo = assignment.kvmHostId ? ` on node ${assignment.kvmHostId}` : ''; errors.push(`VM '${assignment.name}' failed${nodeInfo}: ${err.response?.data?.message || err.message}`); } } // Prepare response const summary = []; if (results.length > 0) { summary.push(`Successfully created ${results.length} VM(s):`); summary.push(...results); } if (errors.length > 0) { summary.push(`\nFailed to create ${errors.length} VM(s):`); summary.push(...errors); } summary.push(`\nResolved parameters:`); summary.push(`- Group: ${group} (ID: ${groupId})`); summary.push(`- Zone/Cloud: ${location} (ID: ${cloudId})`); summary.push(`- Template: ${template} (ID: ${imageId})`); summary.push(`- Plan: ${size} (ID: ${servicePlanId})`); if (distribution === 'spread' || (vmNames.length > 1 && !distribution)) { summary.push(`- Distribution: Spread across nodes ${nodes.join(', ')}`); } else if (nodeAssignments.some(a => a.kvmHostId)) { summary.push(`- Distribution: Specific node placement`); } else { summary.push(`- Distribution: Auto-placement`); } return { content: [ { type: "text", text: summary.join('\n') } ], isError: errors.length > 0 && results.length === 0 }; }