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
JavaScript
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
};
}