vme-mcp-server 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +154 -0
  2. package/dist/server.js +933 -0
  3. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # VME MCP Server
2
+
3
+ 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 Claude Code.
4
+
5
+ ## Quick Start
6
+
7
+ ### Prerequisites
8
+
9
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed
10
+ - Node.js 18+ and npm
11
+ - Access to an HPE VM Essentials (VME) instance
12
+
13
+ ### 1. Clone and Setup
14
+
15
+ ```bash
16
+ git clone https://github.com/frippe75/vme-mcp-server.git
17
+ cd vme-mcp-server
18
+ npm install
19
+ ```
20
+
21
+ ### 2. Configure VME API Access
22
+
23
+ Create a `.env` file with your VME credentials:
24
+
25
+ ```bash
26
+ cp .env.example .env
27
+ ```
28
+
29
+ Edit `.env`:
30
+ ```env
31
+ VME_API_BASE_URL=https://your-vme-instance.com/api
32
+ VME_API_TOKEN=your-bearer-token-here
33
+ ```
34
+
35
+ ### 3. Add to Claude Code
36
+
37
+ **Option A: Using Claude Code CLI (Recommended)**
38
+
39
+ ```bash
40
+ claude mcp add vme-server \
41
+ -e VME_API_BASE_URL=https://your-vme-instance.com/api \
42
+ -e VME_API_TOKEN=your-bearer-token \
43
+ -- npx tsx src/server.ts
44
+ ```
45
+
46
+ **Option B: Manual Configuration**
47
+
48
+ Add to your Claude Code configuration file:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "vme-server": {
54
+ "command": "npx",
55
+ "args": ["tsx", "src/server.ts"],
56
+ "cwd": "/path/to/vme-mcp-server",
57
+ "env": {
58
+ "VME_API_BASE_URL": "https://your-vme-instance.com/api",
59
+ "VME_API_TOKEN": "your-bearer-token"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ### 4. Restart Claude Code
67
+
68
+ ```bash
69
+ claude --restart
70
+ ```
71
+
72
+ ### 5. Verify Installation
73
+
74
+ ```bash
75
+ claude mcp list
76
+ ```
77
+
78
+ You should see `vme-server` in the list of active servers.
79
+
80
+ ## Usage Examples
81
+
82
+ ### Basic VM Creation
83
+ ```
84
+ Create a VM named "web-server-01" in the production group using Ubuntu 22.04 with 4GB memory
85
+ ```
86
+
87
+ ### Multi-VM Creation
88
+ ```
89
+ Create VMs web01->web03 running on each node of the cluster. OS is not important. Small VMs
90
+ Provision web01 through web05 spread across all cluster nodes with Ubuntu and 4GB memory
91
+ ```
92
+
93
+ ### Advanced VM Provisioning
94
+ ```
95
+ Provision 3 small Ubuntu VMs for testing in the dev environment on tc-lab cloud
96
+ Create VMs db01->db03 on specific nodes 1,2,3 respectively with Rocky Linux
97
+ ```
98
+
99
+ ## Key Features
100
+
101
+ ### Natural Language Processing
102
+ - **Flexible terminology**: Use "zone" or "cloud", "small" or "4GB" interchangeably
103
+ - **Smart template matching**: "Ubuntu", "ubuntu", "Ubuntu 22.04" all work
104
+ - **Multi-VM patterns**: "web01->web03" creates web01, web02, web03
105
+
106
+ ### Intelligent Distribution
107
+ - **Auto-placement**: VME cluster load balancing (default)
108
+ - **Spread strategy**: Distribute VMs across all cluster nodes
109
+ - **Specific targeting**: Assign VMs to particular nodes
110
+
111
+ ### Resource Resolution
112
+ - Automatically finds the latest OS versions (Ubuntu 24.04, Rocky 9, etc.)
113
+ - Intelligent size matching (small → 4GB, medium → 8GB)
114
+ - Fallback handling for unknown resources
115
+
116
+ ## Testing
117
+
118
+ ```bash
119
+ # Verify API connectivity (optional)
120
+ npm run test:api
121
+
122
+ # Run comprehensive test suite
123
+ npm test
124
+ ```
125
+
126
+ ## Troubleshooting
127
+
128
+ ### Server Not Loading
129
+ ```bash
130
+ claude mcp get vme-server
131
+ claude --debug
132
+ ```
133
+
134
+ ### Authentication Issues
135
+ ```bash
136
+ npm run test:api
137
+ ```
138
+
139
+ ### VM Creation Problems
140
+ Check your VME permissions and resource availability.
141
+
142
+ ## Documentation
143
+
144
+ - **[CONTRIBUTING.md](./CONTRIBUTING.md)** - Development setup, testing, and contribution guidelines
145
+ - **[TESTING.md](./TESTING.md)** - Comprehensive test documentation and coverage
146
+ - **[CLAUDE.md](./CLAUDE.md)** - Architecture vision and AI-driven development roadmap
147
+
148
+ ## License
149
+
150
+ ISC License - see LICENSE file for details.
151
+
152
+ ---
153
+
154
+ **Need help?** Check the [troubleshooting guide](./CONTRIBUTING.md#troubleshooting) or open an issue.
package/dist/server.js ADDED
@@ -0,0 +1,933 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
7
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const dotenv_1 = __importDefault(require("dotenv"));
10
+ const fs_1 = require("fs");
11
+ const path_1 = require("path");
12
+ dotenv_1.default.config();
13
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
14
+ // AI Training Data Collection Configuration
15
+ const AI_TRAINING_ENABLED = process.env.ENABLE_AI_TRAINING_DATA === 'true';
16
+ const AI_TRAINING_FIELDS = (process.env.AI_TRAINING_DATA_FIELDS || 'user_input,parsed_output,success_metrics').split(',');
17
+ const AI_TRAINING_RETENTION_DAYS = parseInt(process.env.AI_TRAINING_DATA_RETENTION_DAYS || '30');
18
+ // Privacy-aware interaction logging
19
+ function logInteraction(interaction) {
20
+ if (!AI_TRAINING_ENABLED) {
21
+ return; // Respect user privacy choice
22
+ }
23
+ try {
24
+ // Ensure logs directory exists
25
+ const logsDir = (0, path_1.join)(process.cwd(), 'ai-training-logs');
26
+ if (!(0, fs_1.existsSync)(logsDir)) {
27
+ (0, fs_1.mkdirSync)(logsDir, { recursive: true });
28
+ }
29
+ // Filter fields based on user preferences
30
+ const filteredInteraction = {
31
+ id: interaction.id || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
32
+ timestamp: new Date().toISOString(),
33
+ session_id: interaction.session_id || 'unknown',
34
+ tool_name: interaction.tool_name || 'unknown'
35
+ };
36
+ // Only include fields user has opted into
37
+ if (AI_TRAINING_FIELDS.includes('user_input') && interaction.user_input) {
38
+ filteredInteraction.user_input = interaction.user_input;
39
+ }
40
+ if (AI_TRAINING_FIELDS.includes('parsed_output') && interaction.parsed_output) {
41
+ filteredInteraction.parsed_output = interaction.parsed_output;
42
+ }
43
+ if (AI_TRAINING_FIELDS.includes('api_calls') && interaction.api_calls) {
44
+ filteredInteraction.api_calls = interaction.api_calls;
45
+ }
46
+ if (AI_TRAINING_FIELDS.includes('success_metrics') && interaction.success_metrics) {
47
+ filteredInteraction.success_metrics = interaction.success_metrics;
48
+ }
49
+ if (AI_TRAINING_FIELDS.includes('timing') && interaction.timing) {
50
+ filteredInteraction.timing = interaction.timing;
51
+ }
52
+ // Append to daily log file (JSONL format)
53
+ const logFile = (0, path_1.join)(logsDir, `interactions-${new Date().toISOString().split('T')[0]}.jsonl`);
54
+ const logEntry = JSON.stringify(filteredInteraction) + '\n';
55
+ (0, fs_1.writeFileSync)(logFile, logEntry, { flag: 'a' });
56
+ }
57
+ catch (error) {
58
+ // Fail silently to not disrupt user experience
59
+ console.warn('AI training data logging failed:', error);
60
+ }
61
+ }
62
+ // Generate session ID for interaction tracking
63
+ function generateSessionId() {
64
+ return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
65
+ }
66
+ const api = axios_1.default.create({
67
+ baseURL: process.env.VME_API_BASE_URL,
68
+ headers: {
69
+ Authorization: `Bearer ${process.env.VME_API_TOKEN}`,
70
+ "Content-Type": "application/json"
71
+ }
72
+ });
73
+ // Helper function to parse VM name patterns
74
+ function parseVMNames(nameInput, count) {
75
+ // Handle range patterns like "web01->web03" or "web01 -> web03"
76
+ const rangeMatch = nameInput.match(/^(.+?)(\d+)\s*->\s*(.+?)(\d+)$/);
77
+ if (rangeMatch) {
78
+ const [, prefix1, start, prefix2, end] = rangeMatch;
79
+ if (prefix1.trim() === prefix2.trim()) {
80
+ const basePrefix = prefix1.trim();
81
+ const startNum = parseInt(start);
82
+ const endNum = parseInt(end);
83
+ const names = [];
84
+ for (let i = startNum; i <= endNum; i++) {
85
+ const paddedNum = i.toString().padStart(start.length, '0');
86
+ names.push(`${basePrefix}${paddedNum}`);
87
+ }
88
+ return names;
89
+ }
90
+ }
91
+ // Handle count-based generation
92
+ if (count && count > 1) {
93
+ const names = [];
94
+ // Extract base name and number pattern
95
+ const match = nameInput.match(/^(.+?)(\d+)$/);
96
+ if (match) {
97
+ const [, prefix, startStr] = match;
98
+ const startNum = parseInt(startStr);
99
+ for (let i = 0; i < count; i++) {
100
+ const num = startNum + i;
101
+ const paddedNum = num.toString().padStart(startStr.length, '0');
102
+ names.push(`${prefix}${paddedNum}`);
103
+ }
104
+ return names;
105
+ }
106
+ else {
107
+ // If no number pattern, append numbers
108
+ for (let i = 1; i <= count; i++) {
109
+ const paddedNum = i.toString().padStart(2, '0');
110
+ names.push(`${nameInput}${paddedNum}`);
111
+ }
112
+ return names;
113
+ }
114
+ }
115
+ // Single VM
116
+ return [nameInput];
117
+ }
118
+ // Helper function to get available cluster nodes
119
+ async function getClusterNodes() {
120
+ try {
121
+ const clusters = await api.get("/clusters");
122
+ const prodCluster = clusters.data.clusters.find((cluster) => cluster.name === "prod01");
123
+ if (prodCluster && prodCluster.servers) {
124
+ return prodCluster.servers.map((server) => server.id);
125
+ }
126
+ }
127
+ catch (error) {
128
+ console.error('Error getting cluster nodes:', error);
129
+ }
130
+ // Fallback to known nodes if API fails
131
+ return [1, 2, 3];
132
+ }
133
+ async function resolveInput({ group, cloud, template, size }) {
134
+ const [groups, zones, instanceTypes, servicePlans] = await Promise.all([
135
+ api.get("/groups"),
136
+ api.get("/zones"),
137
+ api.get("/instance-types"),
138
+ api.get("/service-plans")
139
+ ]);
140
+ // Find group (case-insensitive)
141
+ const foundGroup = groups.data.groups.find((g) => g.name.toLowerCase() === group.toLowerCase());
142
+ // Find zone/cloud (case-insensitive, handle both terms)
143
+ const foundZone = zones.data.zones.find((z) => z.name.toLowerCase() === cloud.toLowerCase());
144
+ // In HPE VM Essentials, all VMs use "HPE VM" instance type
145
+ // The actual OS is determined by the image selection
146
+ const instanceType = instanceTypes.data.instanceTypes.find((t) => t.name === 'HPE VM');
147
+ // Find appropriate image based on template using osType properties
148
+ let selectedImage;
149
+ const templateLower = template.toLowerCase();
150
+ // Get available images to find the right one
151
+ const images = await api.get("/virtual-images");
152
+ if (templateLower.includes('ubuntu')) {
153
+ // Find Ubuntu images using osType.category and prefer latest version
154
+ const ubuntuImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'ubuntu').sort((a, b) => {
155
+ // Sort by version descending (24.04 > 22.04 > 20.04)
156
+ const aVersion = parseFloat(a.osType.osVersion || '0');
157
+ const bVersion = parseFloat(b.osType.osVersion || '0');
158
+ return bVersion - aVersion;
159
+ });
160
+ selectedImage = ubuntuImages[0];
161
+ }
162
+ else if (templateLower.includes('rocky')) {
163
+ // Find Rocky Linux images and prefer latest version (Rocky 9 > Rocky 8)
164
+ const rockyImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'rocky').sort((a, b) => {
165
+ const aVersion = parseInt(a.osType.osVersion || '0');
166
+ const bVersion = parseInt(b.osType.osVersion || '0');
167
+ return bVersion - aVersion; // Latest first
168
+ });
169
+ selectedImage = rockyImages[0];
170
+ }
171
+ else if (templateLower.includes('centos')) {
172
+ // Find CentOS images and prefer latest version (CentOS 9 > CentOS 8)
173
+ const centosImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'centos').sort((a, b) => {
174
+ const aVersion = parseInt(a.osType.osVersion || '0');
175
+ const bVersion = parseInt(b.osType.osVersion || '0');
176
+ return bVersion - aVersion; // Latest first
177
+ });
178
+ selectedImage = centosImages[0];
179
+ }
180
+ else if (templateLower.includes('debian')) {
181
+ // Find Debian images and prefer latest version (Debian 12 > Debian 11)
182
+ const debianImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'debian').sort((a, b) => {
183
+ const aVersion = parseInt(a.osType.osVersion || '0');
184
+ const bVersion = parseInt(b.osType.osVersion || '0');
185
+ return bVersion - aVersion; // Latest first
186
+ });
187
+ selectedImage = debianImages[0];
188
+ }
189
+ else if (templateLower.includes('alma')) {
190
+ // Find AlmaLinux images and prefer latest version (AlmaLinux 9 > AlmaLinux 8)
191
+ const almaImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'almalinux').sort((a, b) => {
192
+ const aVersion = parseInt(a.osType.osVersion || '0');
193
+ const bVersion = parseInt(b.osType.osVersion || '0');
194
+ return bVersion - aVersion; // Latest first
195
+ });
196
+ selectedImage = almaImages[0];
197
+ }
198
+ else if (templateLower.includes('rhel') || templateLower.includes('red hat')) {
199
+ // Find RHEL images and prefer latest version
200
+ const rhelImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'rhel').sort((a, b) => {
201
+ const aVersion = parseInt(a.osType.osVersion || '0');
202
+ const bVersion = parseInt(b.osType.osVersion || '0');
203
+ return bVersion - aVersion; // Latest first
204
+ });
205
+ selectedImage = rhelImages[0];
206
+ }
207
+ // Default to latest Ubuntu if no specific match
208
+ if (!selectedImage) {
209
+ const ubuntuImages = images.data.virtualImages.filter((img) => img.osType && img.osType.category === 'ubuntu').sort((a, b) => {
210
+ const aVersion = parseFloat(a.osType.osVersion || '0');
211
+ const bVersion = parseFloat(b.osType.osVersion || '0');
212
+ return bVersion - aVersion;
213
+ });
214
+ selectedImage = ubuntuImages[0] || images.data.virtualImages[0];
215
+ }
216
+ // Find service plan based on size (flexible matching)
217
+ let plan;
218
+ const sizeLower = size.toLowerCase();
219
+ if (sizeLower.includes('8gb') || sizeLower.includes('8 gb') || sizeLower.includes('medium')) {
220
+ plan = servicePlans.data.servicePlans.find((p) => p.name.includes('2 CPU, 8GB Memory'));
221
+ }
222
+ else if (sizeLower.includes('4gb') || sizeLower.includes('4 gb') || sizeLower.includes('small')) {
223
+ plan = servicePlans.data.servicePlans.find((p) => p.name.includes('1 CPU, 4GB Memory'));
224
+ }
225
+ // Fallback to any available plan
226
+ if (!plan) {
227
+ plan = servicePlans.data.servicePlans.find((p) => p.name.includes('CPU') && p.name.includes('Memory'));
228
+ }
229
+ return {
230
+ groupId: foundGroup?.id || groups.data.groups[0]?.id,
231
+ cloudId: foundZone?.id || zones.data.zones[0]?.id,
232
+ instanceTypeId: instanceType?.id,
233
+ servicePlanId: plan?.id,
234
+ imageId: selectedImage?.id,
235
+ // Return resolved names for better error reporting
236
+ resolvedGroup: foundGroup?.name || groups.data.groups[0]?.name,
237
+ resolvedCloud: foundZone?.name || zones.data.zones[0]?.name,
238
+ resolvedInstanceType: instanceType?.name,
239
+ resolvedPlan: plan?.name,
240
+ resolvedImage: selectedImage?.osType?.name || selectedImage?.name,
241
+ // Available options for error messages
242
+ availableGroups: groups.data.groups.map((g) => g.name),
243
+ availableZones: zones.data.zones.map((z) => z.name)
244
+ };
245
+ }
246
+ function parseVMIntent(naturalLanguageInput) {
247
+ const input = naturalLanguageInput.toLowerCase().trim();
248
+ const result = {
249
+ action: 'unknown',
250
+ confidence: 0,
251
+ entities: {},
252
+ originalText: naturalLanguageInput
253
+ };
254
+ // Intent Classification with confidence scoring
255
+ const intentPatterns = {
256
+ create: [
257
+ /\b(create|provision|deploy|launch|start|build|make|setup)\b/,
258
+ /\bneed\s+(\d+\s+)?(vm|server|machine)/,
259
+ /\bcan\s+you\s+(create|provision|deploy)/
260
+ ],
261
+ modify: [
262
+ /\b(modify|change|update|edit|alter|resize|scale)\b/,
263
+ /\badd\s+(more\s+)?(cpu|memory|disk|storage)/,
264
+ /\bchange\s+the\s+(size|plan|configuration)/
265
+ ],
266
+ monitor: [
267
+ /\b(show|list|display|check|status|monitor|view)\b(?!.*\b(available|options|resources|templates|plans)\b)/,
268
+ /\bwhat\s+(vm|server|machine)/,
269
+ /\bhow\s+many\s+(vm|server|machine)/
270
+ ],
271
+ destroy: [
272
+ /\b(delete|remove|destroy|terminate|stop|kill)\b/,
273
+ /\btear\s+down/,
274
+ /\bshut\s+down/
275
+ ],
276
+ discover: [
277
+ /\b(available|options|resources|templates|plans)\b/,
278
+ /\bshow\s+me\s+(all|available)/,
279
+ /\bwhat\s+(are|is)\s+(available|possible)/,
280
+ /\bwhat\s+can\s+i\s+(create|do)/
281
+ ]
282
+ };
283
+ // Calculate confidence for each intent
284
+ let maxConfidence = 0;
285
+ let detectedAction = 'unknown';
286
+ for (const [action, patterns] of Object.entries(intentPatterns)) {
287
+ let matches = 0;
288
+ for (const pattern of patterns) {
289
+ if (pattern.test(input)) {
290
+ matches++;
291
+ }
292
+ }
293
+ const confidence = patterns.length > 0 ? matches / patterns.length : 0;
294
+ if (confidence > maxConfidence) {
295
+ maxConfidence = confidence;
296
+ detectedAction = action;
297
+ }
298
+ }
299
+ result.action = detectedAction;
300
+ result.confidence = maxConfidence;
301
+ // Entity Extraction for VM creation
302
+ if (result.action === 'create') {
303
+ // Extract VM names and count
304
+ const namePatterns = [
305
+ /\b(?:vm|server|machine)s?\s+(?:named\s+)?["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?/,
306
+ /\b(?:named\s+)?["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?\s+(?:vm|server|machine)/,
307
+ /\bcreate\s+(?:(?:vm|server|machine)s?\s+)?["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?/,
308
+ /\b(?:vm|server|machine)\s+named\s+["']?([a-zA-Z0-9\-_]+(?:\s*->\s*[a-zA-Z0-9\-_]+)?)["']?/
309
+ ];
310
+ for (const pattern of namePatterns) {
311
+ const match = input.match(pattern);
312
+ if (match) {
313
+ const nameInput = match[1] || match[2];
314
+ result.entities.vmNames = parseVMNames(nameInput);
315
+ break;
316
+ }
317
+ }
318
+ // Extract count
319
+ const countMatch = input.match(/\b(\d+)\s+(?:vm|server|machine)/);
320
+ if (countMatch) {
321
+ result.entities.count = parseInt(countMatch[1]);
322
+ }
323
+ // Extract OS template
324
+ const osPatterns = [
325
+ /\b(ubuntu|rocky|centos|debian|rhel|red\s*hat|alma|almalinux)(?:\s+(\d+(?:\.\d+)?))?/,
326
+ /\busing\s+([a-zA-Z0-9\-_]+)\s+(?:template|os|operating)/,
327
+ /\b(?:template|os|operating)\s+([a-zA-Z0-9\-_]+)/
328
+ ];
329
+ for (const pattern of osPatterns) {
330
+ const match = input.match(pattern);
331
+ if (match) {
332
+ result.entities.osTemplate = match[0];
333
+ break;
334
+ }
335
+ }
336
+ // Extract size/memory
337
+ const sizePatterns = [
338
+ /\b(small|medium|large|tiny)\b/,
339
+ /\b(\d+)\s*gb\s+(?:memory|ram)\b/,
340
+ /\b(\d+)\s*cpu[s]?\s*,?\s*(\d+)\s*gb/,
341
+ /\bwith\s+(\d+(?:gb|cpu))\b/,
342
+ /\b(\d+gb)\b/
343
+ ];
344
+ for (const pattern of sizePatterns) {
345
+ const match = input.match(pattern);
346
+ if (match) {
347
+ result.entities.size = match[0];
348
+ break;
349
+ }
350
+ }
351
+ // Extract group/environment
352
+ const groupPatterns = [
353
+ /\bin\s+(?:the\s+)?([a-zA-Z0-9\-_]+)\s+(?:group|environment|site)/,
354
+ /\b(?:group|environment|site)\s+([a-zA-Z0-9\-_]+)/,
355
+ /\bfor\s+([a-zA-Z0-9\-_]+)\s+(?:environment|testing|production)/
356
+ ];
357
+ for (const pattern of groupPatterns) {
358
+ const match = input.match(pattern);
359
+ if (match) {
360
+ result.entities.group = match[1];
361
+ break;
362
+ }
363
+ }
364
+ // Extract zone/cloud
365
+ const zonePatterns = [
366
+ /\bon\s+([a-zA-Z0-9\-_]+)\s+(?:zone|cloud)/,
367
+ /\b(?:zone|cloud)\s+([a-zA-Z0-9\-_]+)/,
368
+ /\bin\s+([a-zA-Z0-9\-_]+)\s+(?:zone|cloud)/,
369
+ /\b(?:deploy|launch|create)\s+(?:in\s+)?([a-zA-Z0-9\-_]+)\s+zone/
370
+ ];
371
+ for (const pattern of zonePatterns) {
372
+ const match = input.match(pattern);
373
+ if (match) {
374
+ result.entities.zone = match[1];
375
+ break;
376
+ }
377
+ }
378
+ // Extract distribution strategy
379
+ if (input.includes('spread') || input.includes('distribute') || input.includes('across')) {
380
+ result.entities.distribution = 'spread';
381
+ }
382
+ else if (input.includes('each node') || input.includes('all nodes')) {
383
+ result.entities.distribution = 'spread';
384
+ }
385
+ else if (input.includes('node') && input.match(/node[s]?\s*[\d,\s]+/)) {
386
+ const nodeMatch = input.match(/node[s]?\s*([\d,\s]+)/);
387
+ if (nodeMatch) {
388
+ result.entities.distribution = nodeMatch[1].replace(/\s+/g, '');
389
+ }
390
+ }
391
+ // Boost confidence if we extracted entities
392
+ const entityCount = Object.keys(result.entities).length;
393
+ if (entityCount > 0) {
394
+ result.confidence = Math.min(0.95, result.confidence + (entityCount * 0.1));
395
+ }
396
+ }
397
+ return result;
398
+ }
399
+ // Unified resource discovery function
400
+ async function getResources(type, intent, role) {
401
+ try {
402
+ // Fetch all resource types in parallel for comprehensive discovery
403
+ const [groups, zones, instanceTypes, servicePlans, virtualImages, clusters] = await Promise.all([
404
+ api.get("/groups"),
405
+ api.get("/zones"),
406
+ api.get("/instance-types"),
407
+ api.get("/service-plans"),
408
+ api.get("/virtual-images"),
409
+ api.get("/clusters")
410
+ ]);
411
+ const allResources = {
412
+ groups: groups.data.groups || [],
413
+ zones: zones.data.zones || [],
414
+ instanceTypes: instanceTypes.data.instanceTypes || [],
415
+ servicePlans: servicePlans.data.servicePlans || [],
416
+ virtualImages: virtualImages.data.virtualImages || [],
417
+ clusters: clusters.data.clusters || []
418
+ };
419
+ // Apply intelligent filtering based on type
420
+ if (type) {
421
+ const typeLower = type.toLowerCase();
422
+ if (typeLower.includes('compute') || typeLower.includes('vm')) {
423
+ return {
424
+ groups: allResources.groups,
425
+ zones: allResources.zones,
426
+ servicePlans: allResources.servicePlans,
427
+ virtualImages: allResources.virtualImages,
428
+ clusters: allResources.clusters,
429
+ _filtered_for: 'compute/vm resources'
430
+ };
431
+ }
432
+ else if (typeLower.includes('network')) {
433
+ return {
434
+ zones: allResources.zones,
435
+ clusters: allResources.clusters,
436
+ _filtered_for: 'network resources'
437
+ };
438
+ }
439
+ else if (typeLower.includes('storage')) {
440
+ return {
441
+ servicePlans: allResources.servicePlans,
442
+ zones: allResources.zones,
443
+ _filtered_for: 'storage resources'
444
+ };
445
+ }
446
+ }
447
+ // Apply intent-based filtering
448
+ if (intent) {
449
+ const intentLower = intent.toLowerCase();
450
+ if (intentLower.includes('create') || intentLower.includes('provision')) {
451
+ // For creation intents, focus on templates and plans
452
+ return {
453
+ groups: allResources.groups,
454
+ zones: allResources.zones,
455
+ servicePlans: allResources.servicePlans,
456
+ virtualImages: allResources.virtualImages.map((img) => ({
457
+ id: img.id,
458
+ name: img.name,
459
+ osType: img.osType,
460
+ recommended: img.osType?.category === 'ubuntu' ? true : false
461
+ })),
462
+ _filtered_for: 'creation/provisioning intent'
463
+ };
464
+ }
465
+ else if (intentLower.includes('list') || intentLower.includes('show') || intentLower.includes('discover')) {
466
+ // For discovery intents, return comprehensive view
467
+ return {
468
+ summary: {
469
+ total_groups: allResources.groups.length,
470
+ total_zones: allResources.zones.length,
471
+ total_service_plans: allResources.servicePlans.length,
472
+ total_os_templates: allResources.virtualImages.length,
473
+ total_clusters: allResources.clusters.length
474
+ },
475
+ ...allResources,
476
+ _filtered_for: 'discovery/listing intent'
477
+ };
478
+ }
479
+ }
480
+ // Default: return all resources with semantic organization
481
+ return {
482
+ compute: {
483
+ groups: allResources.groups,
484
+ servicePlans: allResources.servicePlans,
485
+ virtualImages: allResources.virtualImages
486
+ },
487
+ infrastructure: {
488
+ zones: allResources.zones,
489
+ clusters: allResources.clusters,
490
+ instanceTypes: allResources.instanceTypes
491
+ },
492
+ _organization: 'semantic grouping by function'
493
+ };
494
+ }
495
+ catch (error) {
496
+ return {
497
+ error: `Failed to fetch resources: ${error.response?.data?.message || error.message}`,
498
+ available_endpoints: ['/groups', '/zones', '/service-plans', '/virtual-images', '/clusters'],
499
+ fallback_suggestion: 'Try checking individual resource types with specific API endpoints'
500
+ };
501
+ }
502
+ }
503
+ const server = new index_js_1.Server({
504
+ name: "vm-create-server",
505
+ version: "1.0.0"
506
+ }, {
507
+ capabilities: {
508
+ tools: {
509
+ list: async () => ([
510
+ {
511
+ name: "get_resources",
512
+ description: "Discover and explore available VME infrastructure resources with intelligent filtering",
513
+ inputSchema: {
514
+ type: "object",
515
+ properties: {
516
+ type: {
517
+ type: "string",
518
+ description: "Resource type filter: 'compute', 'network', 'storage', 'vm', or leave empty for all"
519
+ },
520
+ intent: {
521
+ type: "string",
522
+ description: "Intent-based filtering: 'create', 'provision', 'list', 'discover', or natural language description"
523
+ },
524
+ role: {
525
+ type: "string",
526
+ description: "User role for permission-aware filtering (future enhancement)"
527
+ }
528
+ },
529
+ required: []
530
+ }
531
+ },
532
+ {
533
+ name: "parse_vm_intent",
534
+ description: "Parse natural language VM requests into structured parameters with confidence scoring",
535
+ inputSchema: {
536
+ type: "object",
537
+ properties: {
538
+ naturalLanguageInput: {
539
+ type: "string",
540
+ description: "Natural language description of VM provisioning request"
541
+ }
542
+ },
543
+ required: ["naturalLanguageInput"]
544
+ }
545
+ },
546
+ {
547
+ name: "export_training_data",
548
+ description: "Export AI training data for model improvement (requires ENABLE_AI_TRAINING_DATA=true)",
549
+ inputSchema: {
550
+ type: "object",
551
+ properties: {
552
+ format: {
553
+ type: "string",
554
+ description: "Export format: 'jsonl' or 'csv'",
555
+ enum: ["jsonl", "csv"]
556
+ },
557
+ days: {
558
+ type: "number",
559
+ description: "Number of days of data to export (default: 7)"
560
+ }
561
+ },
562
+ required: []
563
+ }
564
+ },
565
+ {
566
+ name: "provide_feedback",
567
+ description: "Provide feedback on intent parsing accuracy to improve future predictions",
568
+ inputSchema: {
569
+ type: "object",
570
+ properties: {
571
+ original_input: {
572
+ type: "string",
573
+ description: "Original natural language input"
574
+ },
575
+ parsed_result: {
576
+ type: "object",
577
+ description: "The parsed result that was incorrect"
578
+ },
579
+ correct_interpretation: {
580
+ type: "object",
581
+ description: "What the correct interpretation should have been"
582
+ },
583
+ feedback_notes: {
584
+ type: "string",
585
+ description: "Additional notes about what went wrong"
586
+ }
587
+ },
588
+ required: ["original_input", "feedback_notes"]
589
+ }
590
+ },
591
+ {
592
+ name: "create_vm",
593
+ description: "Provision a new virtual machine",
594
+ inputSchema: {
595
+ type: "object",
596
+ properties: {
597
+ name: {
598
+ type: "string",
599
+ description: "Name for VM(s). Supports patterns like 'web01' or 'web01->web03' for multiple VMs"
600
+ },
601
+ group: {
602
+ type: "string",
603
+ description: "Group/site where VM will be created"
604
+ },
605
+ cloud: {
606
+ type: "string",
607
+ description: "Cloud/zone where VM will be provisioned (also accepts 'zone')"
608
+ },
609
+ zone: {
610
+ type: "string",
611
+ description: "Zone/cloud where VM will be provisioned (alias for 'cloud')"
612
+ },
613
+ template: {
614
+ type: "string",
615
+ description: "VM template or operating system"
616
+ },
617
+ size: {
618
+ type: "string",
619
+ description: "VM size (small, medium, 4GB, 8GB, etc.)"
620
+ },
621
+ distribution: {
622
+ type: "string",
623
+ description: "VM distribution strategy: 'auto' (default), 'spread' (across all nodes), or 'node1,node2,node3' (specific nodes)"
624
+ },
625
+ count: {
626
+ type: "number",
627
+ description: "Number of VMs to create (alternative to name patterns)"
628
+ }
629
+ },
630
+ required: ["name", "group", "template", "size"]
631
+ }
632
+ }
633
+ ]),
634
+ call: async (req) => {
635
+ if (req.tool === "get_resources") {
636
+ const { type, intent, role } = req.arguments;
637
+ const resources = await getResources(type, intent, role);
638
+ return {
639
+ toolResult: JSON.stringify(resources, null, 2),
640
+ isError: !!resources.error
641
+ };
642
+ }
643
+ if (req.tool === "parse_vm_intent") {
644
+ const startTime = Date.now();
645
+ const { naturalLanguageInput } = req.arguments;
646
+ const sessionId = generateSessionId();
647
+ const parsedIntent = parseVMIntent(naturalLanguageInput);
648
+ const parseTime = Date.now() - startTime;
649
+ // Log interaction for AI training (respects privacy settings)
650
+ logInteraction({
651
+ session_id: sessionId,
652
+ tool_name: 'parse_vm_intent',
653
+ user_input: { naturalLanguageInput },
654
+ parsed_output: parsedIntent,
655
+ success_metrics: {
656
+ operation_success: parsedIntent.action !== 'unknown',
657
+ confidence_score: parsedIntent.confidence,
658
+ entities_extracted: Object.keys(parsedIntent.entities).length
659
+ },
660
+ timing: {
661
+ parse_duration_ms: parseTime,
662
+ total_duration_ms: parseTime
663
+ }
664
+ });
665
+ return {
666
+ toolResult: JSON.stringify(parsedIntent, null, 2),
667
+ isError: parsedIntent.action === 'unknown' && parsedIntent.confidence < 0.3
668
+ };
669
+ }
670
+ if (req.tool === "export_training_data") {
671
+ if (!AI_TRAINING_ENABLED) {
672
+ return {
673
+ toolResult: JSON.stringify({
674
+ error: "Training data collection is disabled",
675
+ message: "Set ENABLE_AI_TRAINING_DATA=true in .env to enable data collection and export"
676
+ }, null, 2),
677
+ isError: true
678
+ };
679
+ }
680
+ const { format = "jsonl", days = 7 } = req.arguments;
681
+ try {
682
+ const logsDir = (0, path_1.join)(process.cwd(), 'ai-training-logs');
683
+ if (!(0, fs_1.existsSync)(logsDir)) {
684
+ return {
685
+ toolResult: JSON.stringify({
686
+ message: "No training data found",
687
+ data_count: 0
688
+ }, null, 2),
689
+ isError: false
690
+ };
691
+ }
692
+ // Collect data from last N days
693
+ const cutoffDate = new Date(Date.now() - (days * 24 * 60 * 60 * 1000));
694
+ const allData = [];
695
+ // Read log files and aggregate data
696
+ const { readdirSync } = require('fs');
697
+ const logFiles = readdirSync(logsDir).filter((file) => file.startsWith('interactions-') && file.endsWith('.jsonl'));
698
+ for (const file of logFiles) {
699
+ const fileDate = new Date(file.replace('interactions-', '').replace('.jsonl', ''));
700
+ if (fileDate >= cutoffDate) {
701
+ const content = (0, fs_1.readFileSync)((0, path_1.join)(logsDir, file), 'utf-8');
702
+ const lines = content.trim().split('\n').filter(line => line.trim());
703
+ for (const line of lines) {
704
+ try {
705
+ allData.push(JSON.parse(line));
706
+ }
707
+ catch (e) {
708
+ // Skip malformed lines
709
+ }
710
+ }
711
+ }
712
+ }
713
+ return {
714
+ toolResult: JSON.stringify({
715
+ message: `Exported ${allData.length} training data records from last ${days} days`,
716
+ format: format,
717
+ data_count: allData.length,
718
+ data: format === 'jsonl' ? allData : allData.map(item => ({
719
+ timestamp: item.timestamp,
720
+ tool: item.tool_name,
721
+ user_input: JSON.stringify(item.user_input),
722
+ parsed_output: JSON.stringify(item.parsed_output),
723
+ success: item.success_metrics?.operation_success,
724
+ confidence: item.success_metrics?.confidence_score
725
+ }))
726
+ }, null, 2),
727
+ isError: false
728
+ };
729
+ }
730
+ catch (error) {
731
+ return {
732
+ toolResult: JSON.stringify({
733
+ error: "Failed to export training data",
734
+ message: error.message
735
+ }, null, 2),
736
+ isError: true
737
+ };
738
+ }
739
+ }
740
+ if (req.tool === "provide_feedback") {
741
+ const { original_input, parsed_result, correct_interpretation, feedback_notes } = req.arguments;
742
+ const sessionId = generateSessionId();
743
+ // Log feedback for future model improvements
744
+ logInteraction({
745
+ session_id: sessionId,
746
+ tool_name: 'provide_feedback',
747
+ user_input: { original_input, feedback_notes },
748
+ parsed_output: { parsed_result, correct_interpretation },
749
+ user_feedback: feedback_notes,
750
+ success_metrics: {
751
+ operation_success: false, // This indicates the original parsing was wrong
752
+ user_satisfaction: 0 // User had to provide feedback = dissatisfied
753
+ }
754
+ });
755
+ return {
756
+ toolResult: JSON.stringify({
757
+ message: "Thank you for the feedback! This will help improve future intent recognition.",
758
+ feedback_recorded: true,
759
+ original_input: original_input,
760
+ feedback_notes: feedback_notes
761
+ }, null, 2),
762
+ isError: false
763
+ };
764
+ }
765
+ if (req.tool !== "create_vm")
766
+ return;
767
+ const { name, group, cloud, zone, template, size, distribution, count } = req.arguments;
768
+ // Allow both 'cloud' and 'zone' parameters interchangeably
769
+ const location = cloud || zone;
770
+ if (!location) {
771
+ return {
772
+ error: {
773
+ code: "missing_location",
774
+ message: "Either 'cloud' or 'zone' parameter is required"
775
+ }
776
+ };
777
+ }
778
+ // Parse VM names and determine distribution strategy
779
+ const vmNames = parseVMNames(name, count);
780
+ const nodes = await getClusterNodes();
781
+ // Determine node assignment strategy
782
+ let nodeAssignments = [];
783
+ if (distribution === 'spread' || (vmNames.length > 1 && !distribution)) {
784
+ // Distribute VMs across all available nodes
785
+ vmNames.forEach((vmName, index) => {
786
+ const nodeId = nodes[index % nodes.length];
787
+ nodeAssignments.push({ name: vmName, kvmHostId: nodeId });
788
+ });
789
+ }
790
+ else if (distribution && distribution !== 'auto') {
791
+ // Parse specific node assignments like "1,2,3" or "node1,node2,node3"
792
+ const specifiedNodes = distribution.split(',').map((n) => {
793
+ const trimmed = n.trim();
794
+ return trimmed.startsWith('node') ? parseInt(trimmed.replace('node', '')) : parseInt(trimmed);
795
+ }).filter((n) => !isNaN(n));
796
+ if (specifiedNodes.length > 0) {
797
+ vmNames.forEach((vmName, index) => {
798
+ const nodeId = specifiedNodes[index % specifiedNodes.length];
799
+ nodeAssignments.push({ name: vmName, kvmHostId: nodeId });
800
+ });
801
+ }
802
+ else {
803
+ nodeAssignments = vmNames.map(vmName => ({ name: vmName }));
804
+ }
805
+ }
806
+ else {
807
+ // Auto-placement (no kvmHostId specified)
808
+ nodeAssignments = vmNames.map(vmName => ({ name: vmName }));
809
+ }
810
+ const resolved = await resolveInput({ group, cloud: location, template, size });
811
+ const { groupId, cloudId, instanceTypeId, servicePlanId, imageId } = resolved;
812
+ if (!groupId || !cloudId || !instanceTypeId || !servicePlanId || !imageId) {
813
+ const errors = [];
814
+ if (!groupId)
815
+ errors.push(`Group '${group}' not found. Available: ${resolved.availableGroups.join(', ')}`);
816
+ if (!cloudId)
817
+ errors.push(`Zone/Cloud '${location}' not found. Available: ${resolved.availableZones.join(', ')}`);
818
+ if (!instanceTypeId)
819
+ errors.push(`Instance type could not be resolved`);
820
+ if (!servicePlanId)
821
+ errors.push(`Size '${size}' could not be resolved to service plan`);
822
+ if (!imageId)
823
+ errors.push(`Template '${template}' could not be resolved to OS image`);
824
+ return {
825
+ error: {
826
+ code: "resolution_failed",
827
+ message: `Failed to resolve parameters:\n${errors.join('\n')}`
828
+ }
829
+ };
830
+ }
831
+ // Create VMs sequentially
832
+ const results = [];
833
+ const errors = [];
834
+ for (const assignment of nodeAssignments) {
835
+ const vmConfig = {
836
+ resourcePoolId: 'pool-1',
837
+ poolProviderType: 'mvm',
838
+ imageId: imageId,
839
+ createUser: true
840
+ };
841
+ // Add kvmHostId only if explicitly specified
842
+ if (assignment.kvmHostId) {
843
+ vmConfig.kvmHostId = assignment.kvmHostId;
844
+ }
845
+ const payload = {
846
+ zoneId: cloudId,
847
+ instance: {
848
+ name: assignment.name,
849
+ cloud: 'tc-lab',
850
+ hostName: assignment.name,
851
+ type: 'mvm',
852
+ instanceType: {
853
+ code: 'mvm'
854
+ },
855
+ site: {
856
+ id: groupId
857
+ },
858
+ layout: {
859
+ id: 2, // Single HPE VM
860
+ code: 'mvm-1.0-single'
861
+ },
862
+ plan: {
863
+ id: servicePlanId
864
+ }
865
+ },
866
+ config: vmConfig,
867
+ volumes: [
868
+ {
869
+ id: -1,
870
+ rootVolume: true,
871
+ name: 'root',
872
+ size: 10,
873
+ storageType: 1,
874
+ datastoreId: 5
875
+ }
876
+ ],
877
+ networkInterfaces: [
878
+ {
879
+ primaryInterface: true,
880
+ ipMode: 'dhcp',
881
+ network: {
882
+ id: 'network-2'
883
+ },
884
+ networkInterfaceTypeId: 10
885
+ }
886
+ ],
887
+ layoutSize: 1
888
+ };
889
+ try {
890
+ const response = await api.post("/instances", payload);
891
+ const vm = response.data?.instance;
892
+ const nodeInfo = assignment.kvmHostId ? ` on node ${assignment.kvmHostId}` : ' (auto-placed)';
893
+ results.push(`✅ '${vm.name}' created (ID: ${vm.id})${nodeInfo}`);
894
+ }
895
+ catch (err) {
896
+ const nodeInfo = assignment.kvmHostId ? ` on node ${assignment.kvmHostId}` : '';
897
+ errors.push(`❌ '${assignment.name}' failed${nodeInfo}: ${err.response?.data?.message || err.message}`);
898
+ }
899
+ }
900
+ // Prepare response
901
+ const summary = [];
902
+ if (results.length > 0) {
903
+ summary.push(`Successfully created ${results.length} VM(s):`);
904
+ summary.push(...results);
905
+ }
906
+ if (errors.length > 0) {
907
+ summary.push(`\nFailed to create ${errors.length} VM(s):`);
908
+ summary.push(...errors);
909
+ }
910
+ summary.push(`\nResolved parameters:`);
911
+ summary.push(`- Group: ${resolved.resolvedGroup}`);
912
+ summary.push(`- Zone/Cloud: ${resolved.resolvedCloud}`);
913
+ summary.push(`- Template: ${resolved.resolvedImage}`);
914
+ summary.push(`- Plan: ${resolved.resolvedPlan}`);
915
+ if (distribution === 'spread' || (vmNames.length > 1 && !distribution)) {
916
+ summary.push(`- Distribution: Spread across nodes ${nodes.join(', ')}`);
917
+ }
918
+ else if (nodeAssignments.some(a => a.kvmHostId)) {
919
+ summary.push(`- Distribution: Specific node placement`);
920
+ }
921
+ else {
922
+ summary.push(`- Distribution: Auto-placement`);
923
+ }
924
+ return {
925
+ toolResult: summary.join('\n'),
926
+ isError: errors.length > 0 && results.length === 0
927
+ };
928
+ }
929
+ }
930
+ }
931
+ });
932
+ const transport = new stdio_js_1.StdioServerTransport();
933
+ server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "vme-mcp-server",
3
+ "version": "0.1.2",
4
+ "description": "VMware vCenter MCP Server - Natural language infrastructure management for Claude",
5
+ "main": "dist/server.js",
6
+ "bin": {
7
+ "vme-mcp-server": "./dist/server.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/server.js",
12
+ "dev": "tsx watch src/server.ts",
13
+ "prepublishOnly": "npm run build",
14
+ "test": "mocha tests/**/*.test.js",
15
+ "test:unit": "mocha tests/unit/*.test.js",
16
+ "test:integration": "mocha tests/integration/*.test.js",
17
+ "test:e2e": "mocha tests/e2e/*.test.js",
18
+ "test:performance": "mocha tests/performance/*.test.js",
19
+ "test:coverage": "nyc mocha tests/**/*.test.js"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "vmware",
24
+ "vcenter",
25
+ "infrastructure",
26
+ "ai",
27
+ "claude",
28
+ "natural-language",
29
+ "server"
30
+ ],
31
+ "author": "",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/frippe75/vme-mcp-server.git"
36
+ },
37
+ "files": [
38
+ "dist/",
39
+ "README.md"
40
+ ],
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.12.1",
43
+ "axios": "^1.9.0",
44
+ "dotenv": "^16.5.0",
45
+ "tsx": "^4.19.4",
46
+ "typescript": "^5.3.3"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.15.29",
50
+ "mocha": "^10.2.0",
51
+ "nyc": "^15.1.0"
52
+ },
53
+ "description": ""
54
+ }