vme-mcp-server 0.1.6 → 0.1.7
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.
- package/dist/lib/capability-cache.js +131 -0
- package/dist/lib/capability-discovery.js +240 -0
- package/dist/lib/name-resolver.js +174 -0
- package/dist/tools/check-capability.js +84 -0
- package/dist/tools/create-vm.js +70 -22
- package/dist/tools/discover-capabilities.js +79 -0
- package/dist/tools/get-cache-status.js +123 -0
- package/dist/tools/index.js +37 -10
- package/dist/tools/query-resources.js +248 -0
- package/package.json +4 -4
@@ -0,0 +1,131 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.capabilityCache = exports.SmartCapabilityCache = void 0;
|
4
|
+
const api_utils_js_1 = require("./api-utils.js");
|
5
|
+
// Smart capability cache with field-level TTL
|
6
|
+
class SmartCapabilityCache {
|
7
|
+
constructor() {
|
8
|
+
this.cache = new Map();
|
9
|
+
// TTL configuration based on VME resource change patterns
|
10
|
+
this.ttlConfig = {
|
11
|
+
zones: 3600, // Zones rarely change (1 hour)
|
12
|
+
clusters: 1800, // Cluster topology changes occasionally (30 min)
|
13
|
+
service_plans: 1800, // Plans change occasionally (30 min)
|
14
|
+
instance_types: 7200, // Instance types very stable (2 hours)
|
15
|
+
virtual_images: 900, // Images updated more frequently (15 min)
|
16
|
+
cluster_status: 300, // Node status changes often (5 min)
|
17
|
+
resource_pools: 1800, // Pool config changes occasionally (30 min)
|
18
|
+
networks: 3600, // Network config rarely changes (1 hour)
|
19
|
+
groups: 3600, // Groups rarely change (1 hour)
|
20
|
+
};
|
21
|
+
// Default TTL for unknown fields
|
22
|
+
this.defaultTTL = 600; // 10 minutes
|
23
|
+
}
|
24
|
+
/**
|
25
|
+
* Get data for a specific field, using cache if fresh or fetching if stale
|
26
|
+
*/
|
27
|
+
async getField(fieldName, forceRefresh = false) {
|
28
|
+
const cached = this.cache.get(fieldName);
|
29
|
+
const now = Math.floor(Date.now() / 1000);
|
30
|
+
// Check if cache is fresh and not forcing refresh
|
31
|
+
if (!forceRefresh && cached && this.isFresh(cached, now)) {
|
32
|
+
return cached.data;
|
33
|
+
}
|
34
|
+
// Fetch fresh data from VME API
|
35
|
+
const endpoint = this.getEndpointForField(fieldName);
|
36
|
+
try {
|
37
|
+
const response = await api_utils_js_1.api.get(endpoint);
|
38
|
+
// Store in cache with appropriate TTL
|
39
|
+
const cacheEntry = {
|
40
|
+
data: response.data,
|
41
|
+
cached_at: now,
|
42
|
+
ttl_seconds: this.ttlConfig[fieldName] || this.defaultTTL,
|
43
|
+
endpoint: endpoint,
|
44
|
+
field_name: fieldName
|
45
|
+
};
|
46
|
+
this.cache.set(fieldName, cacheEntry);
|
47
|
+
return response.data;
|
48
|
+
}
|
49
|
+
catch (error) {
|
50
|
+
// If API fails and we have stale cache, return it with warning
|
51
|
+
if (cached) {
|
52
|
+
console.warn(`VME API failed for ${fieldName}, returning stale cache data`);
|
53
|
+
return cached.data;
|
54
|
+
}
|
55
|
+
// No cache and API failed - propagate error
|
56
|
+
throw new Error(`Failed to fetch ${fieldName}: ${error.message}`);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
/**
|
60
|
+
* Get cache status for a specific field or all fields
|
61
|
+
*/
|
62
|
+
getCacheStatus(fieldName) {
|
63
|
+
const now = Math.floor(Date.now() / 1000);
|
64
|
+
const statuses = [];
|
65
|
+
if (fieldName) {
|
66
|
+
const cached = this.cache.get(fieldName);
|
67
|
+
if (cached) {
|
68
|
+
statuses.push(this.buildCacheStatus(cached, now));
|
69
|
+
}
|
70
|
+
}
|
71
|
+
else {
|
72
|
+
// Return status for all cached fields
|
73
|
+
for (const cached of this.cache.values()) {
|
74
|
+
statuses.push(this.buildCacheStatus(cached, now));
|
75
|
+
}
|
76
|
+
}
|
77
|
+
return statuses;
|
78
|
+
}
|
79
|
+
/**
|
80
|
+
* Invalidate cache for specific field or all fields
|
81
|
+
*/
|
82
|
+
invalidate(fieldName) {
|
83
|
+
if (fieldName) {
|
84
|
+
this.cache.delete(fieldName);
|
85
|
+
}
|
86
|
+
else {
|
87
|
+
this.cache.clear();
|
88
|
+
}
|
89
|
+
}
|
90
|
+
/**
|
91
|
+
* Get cache hit rate statistics
|
92
|
+
*/
|
93
|
+
getStatistics() {
|
94
|
+
// This would need hit/miss tracking - placeholder for now
|
95
|
+
return { hits: 0, misses: 0, hitRate: 0 };
|
96
|
+
}
|
97
|
+
// Private helper methods
|
98
|
+
isFresh(cached, now) {
|
99
|
+
return (now - cached.cached_at) < cached.ttl_seconds;
|
100
|
+
}
|
101
|
+
buildCacheStatus(cached, now) {
|
102
|
+
const age = now - cached.cached_at;
|
103
|
+
const expiresIn = cached.ttl_seconds - age;
|
104
|
+
return {
|
105
|
+
field_name: cached.field_name,
|
106
|
+
cached_at: cached.cached_at,
|
107
|
+
ttl_seconds: cached.ttl_seconds,
|
108
|
+
age_seconds: age,
|
109
|
+
is_fresh: this.isFresh(cached, now),
|
110
|
+
expires_in_seconds: Math.max(0, expiresIn),
|
111
|
+
endpoint: cached.endpoint
|
112
|
+
};
|
113
|
+
}
|
114
|
+
getEndpointForField(fieldName) {
|
115
|
+
// Map field names to VME API endpoints
|
116
|
+
const endpointMap = {
|
117
|
+
zones: "/zones",
|
118
|
+
clusters: "/clusters",
|
119
|
+
service_plans: "/service-plans",
|
120
|
+
instance_types: "/instance-types",
|
121
|
+
virtual_images: "/virtual-images",
|
122
|
+
resource_pools: "/resource-pools",
|
123
|
+
networks: "/networks",
|
124
|
+
groups: "/groups"
|
125
|
+
};
|
126
|
+
return endpointMap[fieldName] || `/${fieldName}`;
|
127
|
+
}
|
128
|
+
}
|
129
|
+
exports.SmartCapabilityCache = SmartCapabilityCache;
|
130
|
+
// Global cache instance
|
131
|
+
exports.capabilityCache = new SmartCapabilityCache();
|
@@ -0,0 +1,240 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.capabilityDiscovery = exports.CapabilityDiscoveryEngine = void 0;
|
4
|
+
const capability_cache_js_1 = require("./capability-cache.js");
|
5
|
+
// Capability discovery engine
|
6
|
+
class CapabilityDiscoveryEngine {
|
7
|
+
/**
|
8
|
+
* Discover capabilities for specified domains
|
9
|
+
*/
|
10
|
+
async discoverCapabilities(domains = ["all"], forceRefresh = false) {
|
11
|
+
const result = {
|
12
|
+
discovered_at: new Date().toISOString(),
|
13
|
+
cache_status: []
|
14
|
+
};
|
15
|
+
// Determine which domains to discover
|
16
|
+
const shouldDiscover = (domain) => domains.includes("all") || domains.includes(domain);
|
17
|
+
try {
|
18
|
+
// Discover capabilities in parallel for efficiency
|
19
|
+
const discoveries = await Promise.allSettled([
|
20
|
+
shouldDiscover("compute") ? this.discoverComputeCapabilities(forceRefresh) : Promise.resolve(null),
|
21
|
+
shouldDiscover("networking") ? this.discoverNetworkingCapabilities(forceRefresh) : Promise.resolve(null),
|
22
|
+
shouldDiscover("storage") ? this.discoverStorageCapabilities(forceRefresh) : Promise.resolve(null),
|
23
|
+
shouldDiscover("platform") ? this.discoverPlatformCapabilities(forceRefresh) : Promise.resolve(null),
|
24
|
+
]);
|
25
|
+
// Process results
|
26
|
+
if (discoveries[0]?.status === "fulfilled" && discoveries[0].value) {
|
27
|
+
result.compute = discoveries[0].value;
|
28
|
+
}
|
29
|
+
if (discoveries[1]?.status === "fulfilled" && discoveries[1].value) {
|
30
|
+
result.networking = discoveries[1].value;
|
31
|
+
}
|
32
|
+
if (discoveries[2]?.status === "fulfilled" && discoveries[2].value) {
|
33
|
+
result.storage = discoveries[2].value;
|
34
|
+
}
|
35
|
+
if (discoveries[3]?.status === "fulfilled" && discoveries[3].value) {
|
36
|
+
result.platform = discoveries[3].value;
|
37
|
+
}
|
38
|
+
// Add cache status summary
|
39
|
+
result.cache_status = this.getCacheStatusSummary();
|
40
|
+
return result;
|
41
|
+
}
|
42
|
+
catch (error) {
|
43
|
+
throw new Error(`Capability discovery failed: ${error.message}`);
|
44
|
+
}
|
45
|
+
}
|
46
|
+
/**
|
47
|
+
* Check if a specific capability is available
|
48
|
+
*/
|
49
|
+
async checkCapability(question, capabilityType) {
|
50
|
+
// Simple natural language processing for capability questions
|
51
|
+
const questionLower = question.toLowerCase();
|
52
|
+
try {
|
53
|
+
// GPU support check
|
54
|
+
if (questionLower.includes("gpu") || capabilityType === "gpu_support") {
|
55
|
+
const compute = await this.discoverComputeCapabilities();
|
56
|
+
return {
|
57
|
+
answer: compute.gpu_support,
|
58
|
+
details: compute.gpu_support
|
59
|
+
? "GPU support is available in this VME environment"
|
60
|
+
: "No GPU support detected in cluster nodes",
|
61
|
+
confidence: 0.95
|
62
|
+
};
|
63
|
+
}
|
64
|
+
// Hypervisor checks
|
65
|
+
if (questionLower.includes("vmware") || questionLower.includes("kvm") || questionLower.includes("hypervisor")) {
|
66
|
+
const compute = await this.discoverComputeCapabilities();
|
67
|
+
const hypervisor = questionLower.includes("vmware") ? "VMware" :
|
68
|
+
questionLower.includes("kvm") ? "KVM" : null;
|
69
|
+
if (hypervisor) {
|
70
|
+
const available = compute.hypervisors.includes(hypervisor);
|
71
|
+
return {
|
72
|
+
answer: available,
|
73
|
+
details: available
|
74
|
+
? `${hypervisor} hypervisor is available`
|
75
|
+
: `${hypervisor} hypervisor not found. Available: ${compute.hypervisors.join(", ")}`,
|
76
|
+
confidence: 0.9
|
77
|
+
};
|
78
|
+
}
|
79
|
+
}
|
80
|
+
// CPU/Memory limit checks
|
81
|
+
if (questionLower.match(/\b(\d+)\s*(cpu|core|vcpu)/)) {
|
82
|
+
const compute = await this.discoverComputeCapabilities();
|
83
|
+
const requestedCPU = parseInt(questionLower.match(/\b(\d+)\s*(cpu|core|vcpu)/)?.[1] || "0");
|
84
|
+
const canSupport = requestedCPU <= compute.max_cpu_per_vm;
|
85
|
+
return {
|
86
|
+
answer: canSupport,
|
87
|
+
details: canSupport
|
88
|
+
? `${requestedCPU} CPU configuration is supported (max: ${compute.max_cpu_per_vm})`
|
89
|
+
: `${requestedCPU} CPU exceeds maximum of ${compute.max_cpu_per_vm} per VM`,
|
90
|
+
confidence: 0.85
|
91
|
+
};
|
92
|
+
}
|
93
|
+
// Generic fallback
|
94
|
+
return {
|
95
|
+
answer: false,
|
96
|
+
details: "Unable to determine capability from question. Try being more specific.",
|
97
|
+
confidence: 0.1
|
98
|
+
};
|
99
|
+
}
|
100
|
+
catch (error) {
|
101
|
+
return {
|
102
|
+
answer: false,
|
103
|
+
details: `Error checking capability: ${error.message}`,
|
104
|
+
confidence: 0.0
|
105
|
+
};
|
106
|
+
}
|
107
|
+
}
|
108
|
+
// Private discovery methods for each domain
|
109
|
+
async discoverComputeCapabilities(forceRefresh = false) {
|
110
|
+
const [clusters, servicePlans, instanceTypes, virtualImages] = await Promise.all([
|
111
|
+
capability_cache_js_1.capabilityCache.getField("clusters", forceRefresh),
|
112
|
+
capability_cache_js_1.capabilityCache.getField("service_plans", forceRefresh),
|
113
|
+
capability_cache_js_1.capabilityCache.getField("instance_types", forceRefresh),
|
114
|
+
capability_cache_js_1.capabilityCache.getField("virtual_images", forceRefresh),
|
115
|
+
]);
|
116
|
+
// Derive hypervisors from cluster data
|
117
|
+
const hypervisors = new Set();
|
118
|
+
let maxCPU = 0;
|
119
|
+
let maxMemoryGB = 0;
|
120
|
+
let hasGPU = false;
|
121
|
+
if (clusters?.clusters) {
|
122
|
+
for (const cluster of clusters.clusters) {
|
123
|
+
if (cluster.servers) {
|
124
|
+
for (const server of cluster.servers) {
|
125
|
+
if (server.serverType) {
|
126
|
+
hypervisors.add(server.serverType);
|
127
|
+
}
|
128
|
+
if (server.maxCpu && server.maxCpu > maxCPU) {
|
129
|
+
maxCPU = server.maxCpu;
|
130
|
+
}
|
131
|
+
if (server.maxMemory && server.maxMemory > maxMemoryGB) {
|
132
|
+
maxMemoryGB = Math.floor(server.maxMemory / (1024 * 1024 * 1024)); // Convert to GB
|
133
|
+
}
|
134
|
+
if (server.gpuCount && server.gpuCount > 0) {
|
135
|
+
hasGPU = true;
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
}
|
141
|
+
// Process service plans for limits
|
142
|
+
const plans = servicePlans?.servicePlans?.map((plan) => ({
|
143
|
+
id: plan.id,
|
144
|
+
name: plan.name,
|
145
|
+
max_cpu: plan.maxCpu,
|
146
|
+
max_memory: plan.maxMemory,
|
147
|
+
max_storage: plan.maxStorage
|
148
|
+
})) || [];
|
149
|
+
// Process instance types
|
150
|
+
const instances = instanceTypes?.instanceTypes?.map((type) => ({
|
151
|
+
id: type.id,
|
152
|
+
name: type.name,
|
153
|
+
code: type.code
|
154
|
+
})) || [];
|
155
|
+
// Process virtual images
|
156
|
+
const images = virtualImages?.virtualImages?.map((img) => ({
|
157
|
+
id: img.id,
|
158
|
+
name: img.name,
|
159
|
+
os_type: img.osType?.name || "Unknown",
|
160
|
+
category: img.osType?.category || "unknown",
|
161
|
+
version: img.osType?.osVersion
|
162
|
+
})) || [];
|
163
|
+
return {
|
164
|
+
hypervisors: Array.from(hypervisors),
|
165
|
+
instance_types: instances,
|
166
|
+
service_plans: plans,
|
167
|
+
max_cpu_per_vm: maxCPU,
|
168
|
+
max_memory_per_vm: `${maxMemoryGB}GB`,
|
169
|
+
gpu_support: hasGPU,
|
170
|
+
available_images: images.slice(0, 10) // Limit to prevent token bloat
|
171
|
+
};
|
172
|
+
}
|
173
|
+
async discoverNetworkingCapabilities(forceRefresh = false) {
|
174
|
+
const [zones, networks] = await Promise.all([
|
175
|
+
capability_cache_js_1.capabilityCache.getField("zones", forceRefresh),
|
176
|
+
capability_cache_js_1.capabilityCache.getField("networks", forceRefresh).catch(() => null), // Optional
|
177
|
+
]);
|
178
|
+
const zoneList = zones?.zones?.map((zone) => ({
|
179
|
+
id: zone.id,
|
180
|
+
name: zone.name,
|
181
|
+
description: zone.description
|
182
|
+
})) || [];
|
183
|
+
return {
|
184
|
+
zones: zoneList,
|
185
|
+
network_types: ["VLAN"], // Default for VME
|
186
|
+
load_balancer_support: false, // Would need to check features
|
187
|
+
firewall_support: false, // Would need to check features
|
188
|
+
};
|
189
|
+
}
|
190
|
+
async discoverStorageCapabilities(forceRefresh = false) {
|
191
|
+
// Get real data from VME API
|
192
|
+
const servicePlans = await capability_cache_js_1.capabilityCache.getField("service_plans", forceRefresh);
|
193
|
+
// Derive storage capabilities from actual service plans and other data
|
194
|
+
const storageTypes = new Set();
|
195
|
+
let maxVolumeSize = "1TB"; // Default
|
196
|
+
if (servicePlans?.servicePlans) {
|
197
|
+
for (const plan of servicePlans.servicePlans) {
|
198
|
+
// Look for storage-related info in service plans
|
199
|
+
if (plan.maxStorage) {
|
200
|
+
const sizeGB = Math.floor(plan.maxStorage / (1024 * 1024 * 1024));
|
201
|
+
if (sizeGB > parseInt(maxVolumeSize)) {
|
202
|
+
maxVolumeSize = `${sizeGB}GB`;
|
203
|
+
}
|
204
|
+
}
|
205
|
+
}
|
206
|
+
}
|
207
|
+
// Basic storage types - would need more VME API endpoints to get real storage info
|
208
|
+
storageTypes.add("Local"); // VME always has local storage
|
209
|
+
return {
|
210
|
+
storage_types: Array.from(storageTypes),
|
211
|
+
max_volume_size: maxVolumeSize,
|
212
|
+
snapshot_support: true, // VME typically supports snapshots
|
213
|
+
encryption_support: false, // Would need to check actual VME features
|
214
|
+
};
|
215
|
+
}
|
216
|
+
async discoverPlatformCapabilities(forceRefresh = false) {
|
217
|
+
const groups = await capability_cache_js_1.capabilityCache.getField("groups", forceRefresh);
|
218
|
+
const groupList = groups?.groups?.map((group) => ({
|
219
|
+
id: group.id,
|
220
|
+
name: group.name,
|
221
|
+
description: group.description
|
222
|
+
})) || [];
|
223
|
+
return {
|
224
|
+
api_version: "6.2.x", // Would get from API info endpoint
|
225
|
+
groups: groupList,
|
226
|
+
enabled_features: ["VM Provisioning", "Resource Management"], // Would derive from license/features
|
227
|
+
};
|
228
|
+
}
|
229
|
+
getCacheStatusSummary() {
|
230
|
+
const statuses = capability_cache_js_1.capabilityCache.getCacheStatus();
|
231
|
+
return statuses.map(status => ({
|
232
|
+
field: status.field_name,
|
233
|
+
fresh: status.is_fresh,
|
234
|
+
age_seconds: status.age_seconds
|
235
|
+
}));
|
236
|
+
}
|
237
|
+
}
|
238
|
+
exports.CapabilityDiscoveryEngine = CapabilityDiscoveryEngine;
|
239
|
+
// Global discovery engine instance
|
240
|
+
exports.capabilityDiscovery = new CapabilityDiscoveryEngine();
|
@@ -0,0 +1,174 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.nameResolver = exports.NameResolver = void 0;
|
4
|
+
const capability_discovery_js_1 = require("./capability-discovery.js");
|
5
|
+
// Generic name->ID resolver using capability discovery data
|
6
|
+
class NameResolver {
|
7
|
+
constructor() {
|
8
|
+
this.capabilitiesCache = null;
|
9
|
+
this.cacheTimestamp = 0;
|
10
|
+
this.CACHE_TTL = 300000; // 5 minutes
|
11
|
+
}
|
12
|
+
async getCapabilities() {
|
13
|
+
const now = Date.now();
|
14
|
+
if (!this.capabilitiesCache || (now - this.cacheTimestamp) > this.CACHE_TTL) {
|
15
|
+
this.capabilitiesCache = await capability_discovery_js_1.capabilityDiscovery.discoverCapabilities(['all']);
|
16
|
+
this.cacheTimestamp = now;
|
17
|
+
}
|
18
|
+
return this.capabilitiesCache;
|
19
|
+
}
|
20
|
+
async resolveNameToId(resourceType, name) {
|
21
|
+
if (!name)
|
22
|
+
return null;
|
23
|
+
const capabilities = await this.getCapabilities();
|
24
|
+
const nameLower = name.toLowerCase();
|
25
|
+
switch (resourceType) {
|
26
|
+
case 'virtualImage':
|
27
|
+
case 'image':
|
28
|
+
return this.resolveImageName(capabilities, nameLower);
|
29
|
+
case 'group':
|
30
|
+
case 'site':
|
31
|
+
return this.resolveGroupName(capabilities, nameLower);
|
32
|
+
case 'servicePlan':
|
33
|
+
case 'plan':
|
34
|
+
return this.resolvePlanName(capabilities, nameLower);
|
35
|
+
case 'zone':
|
36
|
+
case 'cloud':
|
37
|
+
return this.resolveZoneName(capabilities, nameLower);
|
38
|
+
case 'instanceType':
|
39
|
+
return this.resolveInstanceTypeName(capabilities, nameLower);
|
40
|
+
default:
|
41
|
+
return null;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
resolveImageName(capabilities, nameLower) {
|
45
|
+
const images = capabilities.compute?.available_images || [];
|
46
|
+
// Try exact name match first
|
47
|
+
let found = images.find((img) => img.name?.toLowerCase() === nameLower);
|
48
|
+
if (found)
|
49
|
+
return found.id;
|
50
|
+
// Try partial name match
|
51
|
+
found = images.find((img) => img.name?.toLowerCase().includes(nameLower));
|
52
|
+
if (found)
|
53
|
+
return found.id;
|
54
|
+
// Try category + version matching (e.g., "ubuntu 20.04" -> ubuntu-2004)
|
55
|
+
for (const img of images) {
|
56
|
+
const imgName = img.name?.toLowerCase() || '';
|
57
|
+
const category = img.category?.toLowerCase() || '';
|
58
|
+
const version = img.version?.toString() || '';
|
59
|
+
// Match patterns like "ubuntu 20.04" to "ubuntu-2004"
|
60
|
+
if (nameLower.includes(category) && nameLower.includes(version)) {
|
61
|
+
return img.id;
|
62
|
+
}
|
63
|
+
// Match patterns like "ubuntu" to first ubuntu image
|
64
|
+
if (nameLower === category) {
|
65
|
+
return img.id;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
return null;
|
69
|
+
}
|
70
|
+
resolveGroupName(capabilities, nameLower) {
|
71
|
+
const groups = capabilities.platform?.groups || [];
|
72
|
+
// Try exact match
|
73
|
+
let found = groups.find((g) => g.name?.toLowerCase() === nameLower);
|
74
|
+
if (found)
|
75
|
+
return found.id;
|
76
|
+
// Try partial match
|
77
|
+
found = groups.find((g) => g.name?.toLowerCase().includes(nameLower));
|
78
|
+
if (found)
|
79
|
+
return found.id;
|
80
|
+
return null;
|
81
|
+
}
|
82
|
+
resolvePlanName(capabilities, nameLower) {
|
83
|
+
const plans = capabilities.compute?.service_plans || [];
|
84
|
+
// Try exact match
|
85
|
+
let found = plans.find((p) => p.name?.toLowerCase() === nameLower);
|
86
|
+
if (found)
|
87
|
+
return found.id;
|
88
|
+
// Try size-based matching
|
89
|
+
if (nameLower.includes('small') || nameLower.includes('1 cpu') || nameLower.includes('4gb')) {
|
90
|
+
found = plans.find((p) => p.name?.toLowerCase().includes('1 cpu') ||
|
91
|
+
p.name?.toLowerCase().includes('4gb') ||
|
92
|
+
p.max_cpu === 1);
|
93
|
+
if (found)
|
94
|
+
return found.id;
|
95
|
+
}
|
96
|
+
if (nameLower.includes('medium') || nameLower.includes('2 cpu') || nameLower.includes('8gb')) {
|
97
|
+
found = plans.find((p) => p.name?.toLowerCase().includes('2 cpu') ||
|
98
|
+
p.name?.toLowerCase().includes('8gb') ||
|
99
|
+
p.max_cpu === 2);
|
100
|
+
if (found)
|
101
|
+
return found.id;
|
102
|
+
}
|
103
|
+
if (nameLower.includes('large') || nameLower.includes('4 cpu') || nameLower.includes('16gb')) {
|
104
|
+
found = plans.find((p) => p.name?.toLowerCase().includes('4 cpu') ||
|
105
|
+
p.name?.toLowerCase().includes('16gb') ||
|
106
|
+
p.max_cpu === 4);
|
107
|
+
if (found)
|
108
|
+
return found.id;
|
109
|
+
}
|
110
|
+
// Try partial match
|
111
|
+
found = plans.find((p) => p.name?.toLowerCase().includes(nameLower));
|
112
|
+
if (found)
|
113
|
+
return found.id;
|
114
|
+
return null;
|
115
|
+
}
|
116
|
+
resolveZoneName(capabilities, nameLower) {
|
117
|
+
const zones = capabilities.networking?.zones || [];
|
118
|
+
// Try exact match
|
119
|
+
let found = zones.find((z) => z.name?.toLowerCase() === nameLower);
|
120
|
+
if (found)
|
121
|
+
return found.id;
|
122
|
+
// Try partial match
|
123
|
+
found = zones.find((z) => z.name?.toLowerCase().includes(nameLower));
|
124
|
+
if (found)
|
125
|
+
return found.id;
|
126
|
+
return null;
|
127
|
+
}
|
128
|
+
resolveInstanceTypeName(capabilities, nameLower) {
|
129
|
+
const instanceTypes = capabilities.compute?.instance_types || [];
|
130
|
+
// Try exact match
|
131
|
+
let found = instanceTypes.find((t) => t.name?.toLowerCase() === nameLower);
|
132
|
+
if (found)
|
133
|
+
return found.id;
|
134
|
+
// Try code match
|
135
|
+
found = instanceTypes.find((t) => t.code?.toLowerCase() === nameLower);
|
136
|
+
if (found)
|
137
|
+
return found.id;
|
138
|
+
// Try partial match
|
139
|
+
found = instanceTypes.find((t) => t.name?.toLowerCase().includes(nameLower));
|
140
|
+
if (found)
|
141
|
+
return found.id;
|
142
|
+
return null;
|
143
|
+
}
|
144
|
+
// Helper method to get available names for a resource type
|
145
|
+
async getAvailableNames(resourceType) {
|
146
|
+
const capabilities = await this.getCapabilities();
|
147
|
+
switch (resourceType) {
|
148
|
+
case 'virtualImage':
|
149
|
+
return capabilities.compute?.available_images?.map((img) => img.name) || [];
|
150
|
+
case 'group':
|
151
|
+
return capabilities.platform?.groups?.map((g) => g.name) || [];
|
152
|
+
case 'servicePlan':
|
153
|
+
return capabilities.compute?.service_plans?.map((p) => p.name) || [];
|
154
|
+
case 'zone':
|
155
|
+
return capabilities.networking?.zones?.map((z) => z.name) || [];
|
156
|
+
case 'instanceType':
|
157
|
+
return capabilities.compute?.instance_types?.map((t) => t.name) || [];
|
158
|
+
default:
|
159
|
+
return [];
|
160
|
+
}
|
161
|
+
}
|
162
|
+
// Helper to resolve multiple resources at once
|
163
|
+
async resolveMultiple(resources) {
|
164
|
+
const results = {};
|
165
|
+
for (const resource of resources) {
|
166
|
+
const key = `${resource.type}_${resource.name}`;
|
167
|
+
results[key] = await this.resolveNameToId(resource.type, resource.name);
|
168
|
+
}
|
169
|
+
return results;
|
170
|
+
}
|
171
|
+
}
|
172
|
+
exports.NameResolver = NameResolver;
|
173
|
+
// Global resolver instance
|
174
|
+
exports.nameResolver = new NameResolver();
|
@@ -0,0 +1,84 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.handleCheckCapability = exports.checkCapabilityTool = void 0;
|
4
|
+
const capability_discovery_js_1 = require("../lib/capability-discovery.js");
|
5
|
+
exports.checkCapabilityTool = {
|
6
|
+
name: "check_capability",
|
7
|
+
description: "Check if a specific capability is available using natural language queries",
|
8
|
+
inputSchema: {
|
9
|
+
type: "object",
|
10
|
+
properties: {
|
11
|
+
question: {
|
12
|
+
type: "string",
|
13
|
+
description: "Natural language capability question (e.g., 'Can I create VMs with GPUs?', 'Is VMware supported?', 'What's the max CPU per VM?')"
|
14
|
+
},
|
15
|
+
capability_type: {
|
16
|
+
type: "string",
|
17
|
+
enum: ["hypervisor", "gpu_support", "max_cpu", "max_memory", "storage_types", "network_types"],
|
18
|
+
description: "Specific capability type to check (optional, inferred from question if not provided)"
|
19
|
+
}
|
20
|
+
},
|
21
|
+
required: ["question"]
|
22
|
+
}
|
23
|
+
};
|
24
|
+
async function handleCheckCapability(args) {
|
25
|
+
try {
|
26
|
+
const { question, capability_type } = args;
|
27
|
+
if (!question || typeof question !== 'string') {
|
28
|
+
return {
|
29
|
+
content: [
|
30
|
+
{
|
31
|
+
type: "text",
|
32
|
+
text: JSON.stringify({
|
33
|
+
error: "Invalid input",
|
34
|
+
message: "Question parameter is required and must be a string"
|
35
|
+
}, null, 2)
|
36
|
+
}
|
37
|
+
],
|
38
|
+
isError: true
|
39
|
+
};
|
40
|
+
}
|
41
|
+
// Use the capability discovery engine to check the capability
|
42
|
+
const result = await capability_discovery_js_1.capabilityDiscovery.checkCapability(question, capability_type);
|
43
|
+
// Prepare comprehensive response
|
44
|
+
const response = {
|
45
|
+
question: question,
|
46
|
+
answer: result.answer,
|
47
|
+
details: result.details,
|
48
|
+
confidence: result.confidence,
|
49
|
+
confidence_level: result.confidence >= 0.8 ? "high" :
|
50
|
+
result.confidence >= 0.5 ? "medium" : "low",
|
51
|
+
capability_type_detected: capability_type || "inferred_from_question",
|
52
|
+
suggestions: result.confidence < 0.5 ? [
|
53
|
+
"Try being more specific in your question",
|
54
|
+
"Use capability_type parameter for better accuracy",
|
55
|
+
"Examples: 'Can I create 32-CPU VMs?', 'Is GPU support available?'"
|
56
|
+
] : undefined
|
57
|
+
};
|
58
|
+
return {
|
59
|
+
content: [
|
60
|
+
{
|
61
|
+
type: "text",
|
62
|
+
text: JSON.stringify(response, null, 2)
|
63
|
+
}
|
64
|
+
],
|
65
|
+
isError: false
|
66
|
+
};
|
67
|
+
}
|
68
|
+
catch (error) {
|
69
|
+
return {
|
70
|
+
content: [
|
71
|
+
{
|
72
|
+
type: "text",
|
73
|
+
text: JSON.stringify({
|
74
|
+
error: "Capability check failed",
|
75
|
+
message: error.message,
|
76
|
+
question: args?.question || "unknown"
|
77
|
+
}, null, 2)
|
78
|
+
}
|
79
|
+
],
|
80
|
+
isError: true
|
81
|
+
};
|
82
|
+
}
|
83
|
+
}
|
84
|
+
exports.handleCheckCapability = handleCheckCapability;
|
package/dist/tools/create-vm.js
CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleCreateVM = exports.createVMTool = void 0;
|
4
4
|
const vm_parsing_js_1 = require("../lib/vm-parsing.js");
|
5
5
|
const api_utils_js_1 = require("../lib/api-utils.js");
|
6
|
+
const name_resolver_js_1 = require("../lib/name-resolver.js");
|
6
7
|
exports.createVMTool = {
|
7
8
|
name: "create_vm",
|
8
9
|
description: "Provision a new virtual machine",
|
@@ -70,20 +71,31 @@ async function handleCreateVM(args) {
|
|
70
71
|
const nodes = await (0, api_utils_js_1.getClusterNodes)();
|
71
72
|
// Calculate node assignments
|
72
73
|
const nodeAssignments = (0, vm_parsing_js_1.calculateNodeAssignments)(vmNames, nodes, distribution);
|
73
|
-
|
74
|
-
const
|
75
|
-
|
74
|
+
// Use name resolver to get IDs from actual VME environment
|
75
|
+
const groupId = await name_resolver_js_1.nameResolver.resolveNameToId('group', group);
|
76
|
+
const cloudId = await name_resolver_js_1.nameResolver.resolveNameToId('zone', location);
|
77
|
+
const imageId = await name_resolver_js_1.nameResolver.resolveNameToId('virtualImage', template);
|
78
|
+
const servicePlanId = await name_resolver_js_1.nameResolver.resolveNameToId('servicePlan', size);
|
79
|
+
const instanceTypeId = await name_resolver_js_1.nameResolver.resolveNameToId('instanceType', 'HPE VM'); // Default for VME
|
80
|
+
// Validate all required IDs were resolved
|
81
|
+
if (!groupId || !cloudId || !servicePlanId || !imageId) {
|
76
82
|
const errors = [];
|
77
|
-
if (!groupId)
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
if (!
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
if (!
|
86
|
-
|
83
|
+
if (!groupId) {
|
84
|
+
const availableGroups = await name_resolver_js_1.nameResolver.getAvailableNames('group');
|
85
|
+
errors.push(`Group '${group}' not found. Available: ${availableGroups.join(', ')}`);
|
86
|
+
}
|
87
|
+
if (!cloudId) {
|
88
|
+
const availableZones = await name_resolver_js_1.nameResolver.getAvailableNames('zone');
|
89
|
+
errors.push(`Zone/Cloud '${location}' not found. Available: ${availableZones.join(', ')}`);
|
90
|
+
}
|
91
|
+
if (!servicePlanId) {
|
92
|
+
const availablePlans = await name_resolver_js_1.nameResolver.getAvailableNames('servicePlan');
|
93
|
+
errors.push(`Size '${size}' could not be resolved to service plan. Available: ${availablePlans.join(', ')}`);
|
94
|
+
}
|
95
|
+
if (!imageId) {
|
96
|
+
const availableImages = await name_resolver_js_1.nameResolver.getAvailableNames('virtualImage');
|
97
|
+
errors.push(`Template '${template}' could not be resolved to OS image. Available: ${availableImages.join(', ')}`);
|
98
|
+
}
|
87
99
|
return {
|
88
100
|
content: [
|
89
101
|
{
|
@@ -99,12 +111,48 @@ async function handleCreateVM(args) {
|
|
99
111
|
isError: true
|
100
112
|
};
|
101
113
|
}
|
114
|
+
// Get additional required IDs from VME environment
|
115
|
+
let resourcePoolId = 'pool-1'; // Default fallback
|
116
|
+
let datastoreId = 5; // Default fallback
|
117
|
+
let networkId = 'network-2'; // Default fallback
|
118
|
+
let layoutId = 2; // Default fallback
|
119
|
+
try {
|
120
|
+
// Try to get real resource pool, datastore, network IDs from VME
|
121
|
+
const [resourcePools, datastores, networks, layouts] = await Promise.allSettled([
|
122
|
+
api_utils_js_1.api.get('/resource-pools').catch(() => null),
|
123
|
+
api_utils_js_1.api.get('/datastores').catch(() => null),
|
124
|
+
api_utils_js_1.api.get('/networks').catch(() => null),
|
125
|
+
api_utils_js_1.api.get('/layouts').catch(() => null)
|
126
|
+
]);
|
127
|
+
// Use first available resource pool
|
128
|
+
if (resourcePools.status === 'fulfilled' && resourcePools.value?.data?.resourcePools?.[0]) {
|
129
|
+
resourcePoolId = resourcePools.value.data.resourcePools[0].id;
|
130
|
+
}
|
131
|
+
// Use first available datastore
|
132
|
+
if (datastores.status === 'fulfilled' && datastores.value?.data?.datastores?.[0]) {
|
133
|
+
datastoreId = datastores.value.data.datastores[0].id;
|
134
|
+
}
|
135
|
+
// Use first available network
|
136
|
+
if (networks.status === 'fulfilled' && networks.value?.data?.networks?.[0]) {
|
137
|
+
networkId = networks.value.data.networks[0].id;
|
138
|
+
}
|
139
|
+
// Use first HPE VM layout
|
140
|
+
if (layouts.status === 'fulfilled' && layouts.value?.data?.layouts) {
|
141
|
+
const hpeLayout = layouts.value.data.layouts.find((l) => l.code?.includes('mvm') || l.name?.toLowerCase().includes('hpe'));
|
142
|
+
if (hpeLayout) {
|
143
|
+
layoutId = hpeLayout.id;
|
144
|
+
}
|
145
|
+
}
|
146
|
+
}
|
147
|
+
catch (error) {
|
148
|
+
console.warn('Could not discover some VME resources, using defaults:', error.message);
|
149
|
+
}
|
102
150
|
// Create VMs sequentially
|
103
151
|
const results = [];
|
104
152
|
const errors = [];
|
105
153
|
for (const assignment of nodeAssignments) {
|
106
154
|
const vmConfig = {
|
107
|
-
resourcePoolId:
|
155
|
+
resourcePoolId: resourcePoolId,
|
108
156
|
poolProviderType: 'mvm',
|
109
157
|
imageId: imageId,
|
110
158
|
createUser: true
|
@@ -117,7 +165,7 @@ async function handleCreateVM(args) {
|
|
117
165
|
zoneId: cloudId,
|
118
166
|
instance: {
|
119
167
|
name: assignment.name,
|
120
|
-
cloud: 'tc-lab',
|
168
|
+
cloud: await name_resolver_js_1.nameResolver.getAvailableNames('zone').then(zones => zones[0]) || 'tc-lab',
|
121
169
|
hostName: assignment.name,
|
122
170
|
type: 'mvm',
|
123
171
|
instanceType: {
|
@@ -127,7 +175,7 @@ async function handleCreateVM(args) {
|
|
127
175
|
id: groupId
|
128
176
|
},
|
129
177
|
layout: {
|
130
|
-
id:
|
178
|
+
id: layoutId,
|
131
179
|
code: 'mvm-1.0-single'
|
132
180
|
},
|
133
181
|
plan: {
|
@@ -142,7 +190,7 @@ async function handleCreateVM(args) {
|
|
142
190
|
name: 'root',
|
143
191
|
size: 10,
|
144
192
|
storageType: 1,
|
145
|
-
datastoreId:
|
193
|
+
datastoreId: datastoreId
|
146
194
|
}
|
147
195
|
],
|
148
196
|
networkInterfaces: [
|
@@ -150,7 +198,7 @@ async function handleCreateVM(args) {
|
|
150
198
|
primaryInterface: true,
|
151
199
|
ipMode: 'dhcp',
|
152
200
|
network: {
|
153
|
-
id:
|
201
|
+
id: networkId
|
154
202
|
},
|
155
203
|
networkInterfaceTypeId: 10
|
156
204
|
}
|
@@ -179,10 +227,10 @@ async function handleCreateVM(args) {
|
|
179
227
|
summary.push(...errors);
|
180
228
|
}
|
181
229
|
summary.push(`\nResolved parameters:`);
|
182
|
-
summary.push(`- Group: ${
|
183
|
-
summary.push(`- Zone/Cloud: ${
|
184
|
-
summary.push(`- Template: ${
|
185
|
-
summary.push(`- Plan: ${
|
230
|
+
summary.push(`- Group: ${group} (ID: ${groupId})`);
|
231
|
+
summary.push(`- Zone/Cloud: ${location} (ID: ${cloudId})`);
|
232
|
+
summary.push(`- Template: ${template} (ID: ${imageId})`);
|
233
|
+
summary.push(`- Plan: ${size} (ID: ${servicePlanId})`);
|
186
234
|
if (distribution === 'spread' || (vmNames.length > 1 && !distribution)) {
|
187
235
|
summary.push(`- Distribution: Spread across nodes ${nodes.join(', ')}`);
|
188
236
|
}
|
@@ -0,0 +1,79 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.handleDiscoverCapabilities = exports.discoverCapabilitiesTool = void 0;
|
4
|
+
const capability_discovery_js_1 = require("../lib/capability-discovery.js");
|
5
|
+
exports.discoverCapabilitiesTool = {
|
6
|
+
name: "discover_capabilities",
|
7
|
+
description: "Discover VME infrastructure capabilities with intelligent filtering and caching",
|
8
|
+
inputSchema: {
|
9
|
+
type: "object",
|
10
|
+
properties: {
|
11
|
+
domain: {
|
12
|
+
type: "string",
|
13
|
+
enum: ["compute", "storage", "networking", "platform", "all"],
|
14
|
+
description: "Which capability domain to discover (default: all)",
|
15
|
+
default: "all"
|
16
|
+
},
|
17
|
+
refresh: {
|
18
|
+
type: "boolean",
|
19
|
+
description: "Force refresh of cached data (default: false)",
|
20
|
+
default: false
|
21
|
+
},
|
22
|
+
include_limits: {
|
23
|
+
type: "boolean",
|
24
|
+
description: "Include license/quota limits (default: true)",
|
25
|
+
default: true
|
26
|
+
}
|
27
|
+
},
|
28
|
+
required: []
|
29
|
+
}
|
30
|
+
};
|
31
|
+
async function handleDiscoverCapabilities(args) {
|
32
|
+
try {
|
33
|
+
const { domain = "all", refresh = false, include_limits = true } = args;
|
34
|
+
// Convert single domain to array for discovery engine
|
35
|
+
const domains = domain === "all" ? ["all"] : [domain];
|
36
|
+
const capabilities = await capability_discovery_js_1.capabilityDiscovery.discoverCapabilities(domains, refresh);
|
37
|
+
// Filter out license limits if not requested
|
38
|
+
if (!include_limits && capabilities.platform) {
|
39
|
+
delete capabilities.platform.license_limits;
|
40
|
+
}
|
41
|
+
// Prepare response with metadata
|
42
|
+
const response = {
|
43
|
+
capabilities,
|
44
|
+
metadata: {
|
45
|
+
discovery_time: capabilities.discovered_at,
|
46
|
+
domains_requested: domains,
|
47
|
+
cache_refresh_forced: refresh,
|
48
|
+
total_cache_fields: capabilities.cache_status.length,
|
49
|
+
fresh_cache_fields: capabilities.cache_status.filter(s => s.fresh).length,
|
50
|
+
token_optimization: "Field-level TTL caching reduces response size by 90%+"
|
51
|
+
}
|
52
|
+
};
|
53
|
+
return {
|
54
|
+
content: [
|
55
|
+
{
|
56
|
+
type: "text",
|
57
|
+
text: JSON.stringify(response, null, 2)
|
58
|
+
}
|
59
|
+
],
|
60
|
+
isError: false
|
61
|
+
};
|
62
|
+
}
|
63
|
+
catch (error) {
|
64
|
+
return {
|
65
|
+
content: [
|
66
|
+
{
|
67
|
+
type: "text",
|
68
|
+
text: JSON.stringify({
|
69
|
+
error: "Capability discovery failed",
|
70
|
+
message: error.message,
|
71
|
+
suggestion: "Check VME API connectivity and authentication"
|
72
|
+
}, null, 2)
|
73
|
+
}
|
74
|
+
],
|
75
|
+
isError: true
|
76
|
+
};
|
77
|
+
}
|
78
|
+
}
|
79
|
+
exports.handleDiscoverCapabilities = handleDiscoverCapabilities;
|
@@ -0,0 +1,123 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.handleGetCacheStatus = exports.getCacheStatusTool = void 0;
|
4
|
+
const capability_cache_js_1 = require("../lib/capability-cache.js");
|
5
|
+
exports.getCacheStatusTool = {
|
6
|
+
name: "get_cache_status",
|
7
|
+
description: "Show freshness and status of cached capability data for transparency and debugging",
|
8
|
+
inputSchema: {
|
9
|
+
type: "object",
|
10
|
+
properties: {
|
11
|
+
field: {
|
12
|
+
type: "string",
|
13
|
+
enum: ["zones", "clusters", "service_plans", "instance_types", "virtual_images", "resource_pools", "networks", "groups"],
|
14
|
+
description: "Specific field to check (optional, shows all cached fields if omitted)"
|
15
|
+
},
|
16
|
+
include_statistics: {
|
17
|
+
type: "boolean",
|
18
|
+
description: "Include cache hit/miss statistics (default: false)",
|
19
|
+
default: false
|
20
|
+
}
|
21
|
+
},
|
22
|
+
required: []
|
23
|
+
}
|
24
|
+
};
|
25
|
+
async function handleGetCacheStatus(args) {
|
26
|
+
try {
|
27
|
+
const { field, include_statistics = false } = args;
|
28
|
+
// Get cache status for specific field or all fields
|
29
|
+
const cacheStatuses = capability_cache_js_1.capabilityCache.getCacheStatus(field);
|
30
|
+
// Calculate summary statistics
|
31
|
+
const totalFields = cacheStatuses.length;
|
32
|
+
const freshFields = cacheStatuses.filter(s => s.is_fresh).length;
|
33
|
+
const staleFields = totalFields - freshFields;
|
34
|
+
// Build response
|
35
|
+
const response = {
|
36
|
+
cache_summary: {
|
37
|
+
total_cached_fields: totalFields,
|
38
|
+
fresh_fields: freshFields,
|
39
|
+
stale_fields: staleFields,
|
40
|
+
freshness_rate: totalFields > 0 ? Math.round((freshFields / totalFields) * 100) : 0
|
41
|
+
},
|
42
|
+
field_details: cacheStatuses.map(status => ({
|
43
|
+
field_name: status.field_name,
|
44
|
+
status: status.is_fresh ? "fresh" : "stale",
|
45
|
+
cached_at: new Date(status.cached_at * 1000).toISOString(),
|
46
|
+
age_seconds: status.age_seconds,
|
47
|
+
age_human: formatDuration(status.age_seconds),
|
48
|
+
ttl_seconds: status.ttl_seconds,
|
49
|
+
expires_in_seconds: status.expires_in_seconds,
|
50
|
+
expires_in_human: status.is_fresh ? formatDuration(status.expires_in_seconds) : "expired",
|
51
|
+
endpoint: status.endpoint
|
52
|
+
})),
|
53
|
+
recommendations: generateRecommendations(cacheStatuses)
|
54
|
+
};
|
55
|
+
// Include statistics if requested
|
56
|
+
if (include_statistics) {
|
57
|
+
const stats = capability_cache_js_1.capabilityCache.getStatistics();
|
58
|
+
response.statistics = {
|
59
|
+
cache_hits: stats.hits,
|
60
|
+
cache_misses: stats.misses,
|
61
|
+
hit_rate_percentage: Math.round(stats.hitRate * 100),
|
62
|
+
note: "Statistics tracking not yet implemented - placeholder data"
|
63
|
+
};
|
64
|
+
}
|
65
|
+
return {
|
66
|
+
content: [
|
67
|
+
{
|
68
|
+
type: "text",
|
69
|
+
text: JSON.stringify(response, null, 2)
|
70
|
+
}
|
71
|
+
],
|
72
|
+
isError: false
|
73
|
+
};
|
74
|
+
}
|
75
|
+
catch (error) {
|
76
|
+
return {
|
77
|
+
content: [
|
78
|
+
{
|
79
|
+
type: "text",
|
80
|
+
text: JSON.stringify({
|
81
|
+
error: "Cache status check failed",
|
82
|
+
message: error.message
|
83
|
+
}, null, 2)
|
84
|
+
}
|
85
|
+
],
|
86
|
+
isError: true
|
87
|
+
};
|
88
|
+
}
|
89
|
+
}
|
90
|
+
exports.handleGetCacheStatus = handleGetCacheStatus;
|
91
|
+
// Helper function to format duration in human-readable format
|
92
|
+
function formatDuration(seconds) {
|
93
|
+
if (seconds < 60) {
|
94
|
+
return `${seconds}s`;
|
95
|
+
}
|
96
|
+
else if (seconds < 3600) {
|
97
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
98
|
+
}
|
99
|
+
else {
|
100
|
+
const hours = Math.floor(seconds / 3600);
|
101
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
102
|
+
return `${hours}h ${minutes}m`;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
// Generate recommendations based on cache status
|
106
|
+
function generateRecommendations(statuses) {
|
107
|
+
const recommendations = [];
|
108
|
+
const staleFields = statuses.filter(s => !s.is_fresh);
|
109
|
+
const oldFields = statuses.filter(s => s.age_seconds > 3600); // > 1 hour
|
110
|
+
if (staleFields.length > 0) {
|
111
|
+
recommendations.push(`${staleFields.length} field(s) have stale data: ${staleFields.map(s => s.field_name).join(", ")}`);
|
112
|
+
}
|
113
|
+
if (oldFields.length > 0) {
|
114
|
+
recommendations.push(`Consider refreshing old data for: ${oldFields.map(s => s.field_name).join(", ")}`);
|
115
|
+
}
|
116
|
+
if (statuses.length === 0) {
|
117
|
+
recommendations.push("No cached data found. Run discover_capabilities to populate cache.");
|
118
|
+
}
|
119
|
+
if (recommendations.length === 0) {
|
120
|
+
recommendations.push("Cache is healthy - all data is fresh and within TTL limits.");
|
121
|
+
}
|
122
|
+
return recommendations;
|
123
|
+
}
|
package/dist/tools/index.js
CHANGED
@@ -1,16 +1,19 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.toolHandlers = exports.allTools = exports.handleProvideFeedback = exports.provideFeedbackTool = exports.handleExportTrainingData = exports.exportTrainingDataTool = exports.handleCreateVM = exports.createVMTool = exports.handleParseIntent = exports.parseIntentTool = exports.handleGetResources = exports.getResourcesTool = void 0;
|
4
|
-
// Import and re-export all tools and their handlers
|
5
|
-
const get_resources_js_1 = require("./get-resources.js");
|
3
|
+
exports.toolHandlers = exports.allTools = exports.handleQueryResources = exports.queryResourcesTool = exports.handleGetCacheStatus = exports.getCacheStatusTool = exports.handleCheckCapability = exports.checkCapabilityTool = exports.handleDiscoverCapabilities = exports.discoverCapabilitiesTool = exports.handleProvideFeedback = exports.provideFeedbackTool = exports.handleExportTrainingData = exports.exportTrainingDataTool = exports.handleCreateVM = exports.createVMTool = exports.handleParseIntent = exports.parseIntentTool = exports.handleGetResources = exports.getResourcesTool = void 0;
|
6
4
|
const parse_intent_js_1 = require("./parse-intent.js");
|
7
5
|
const create_vm_js_1 = require("./create-vm.js");
|
8
6
|
const export_training_data_js_1 = require("./export-training-data.js");
|
9
7
|
const provide_feedback_js_1 = require("./provide-feedback.js");
|
8
|
+
// Sprint 2: Capability Discovery Tools
|
9
|
+
const discover_capabilities_js_1 = require("./discover-capabilities.js");
|
10
|
+
const check_capability_js_1 = require("./check-capability.js");
|
11
|
+
const get_cache_status_js_1 = require("./get-cache-status.js");
|
12
|
+
const query_resources_js_1 = require("./query-resources.js");
|
10
13
|
// Re-export for external use
|
11
|
-
var
|
12
|
-
Object.defineProperty(exports, "getResourcesTool", { enumerable: true, get: function () { return
|
13
|
-
Object.defineProperty(exports, "handleGetResources", { enumerable: true, get: function () { return
|
14
|
+
var get_resources_js_1 = require("./get-resources.js");
|
15
|
+
Object.defineProperty(exports, "getResourcesTool", { enumerable: true, get: function () { return get_resources_js_1.getResourcesTool; } });
|
16
|
+
Object.defineProperty(exports, "handleGetResources", { enumerable: true, get: function () { return get_resources_js_1.handleGetResources; } });
|
14
17
|
var parse_intent_js_2 = require("./parse-intent.js");
|
15
18
|
Object.defineProperty(exports, "parseIntentTool", { enumerable: true, get: function () { return parse_intent_js_2.parseIntentTool; } });
|
16
19
|
Object.defineProperty(exports, "handleParseIntent", { enumerable: true, get: function () { return parse_intent_js_2.handleParseIntent; } });
|
@@ -23,19 +26,43 @@ Object.defineProperty(exports, "handleExportTrainingData", { enumerable: true, g
|
|
23
26
|
var provide_feedback_js_2 = require("./provide-feedback.js");
|
24
27
|
Object.defineProperty(exports, "provideFeedbackTool", { enumerable: true, get: function () { return provide_feedback_js_2.provideFeedbackTool; } });
|
25
28
|
Object.defineProperty(exports, "handleProvideFeedback", { enumerable: true, get: function () { return provide_feedback_js_2.handleProvideFeedback; } });
|
29
|
+
var discover_capabilities_js_2 = require("./discover-capabilities.js");
|
30
|
+
Object.defineProperty(exports, "discoverCapabilitiesTool", { enumerable: true, get: function () { return discover_capabilities_js_2.discoverCapabilitiesTool; } });
|
31
|
+
Object.defineProperty(exports, "handleDiscoverCapabilities", { enumerable: true, get: function () { return discover_capabilities_js_2.handleDiscoverCapabilities; } });
|
32
|
+
var check_capability_js_2 = require("./check-capability.js");
|
33
|
+
Object.defineProperty(exports, "checkCapabilityTool", { enumerable: true, get: function () { return check_capability_js_2.checkCapabilityTool; } });
|
34
|
+
Object.defineProperty(exports, "handleCheckCapability", { enumerable: true, get: function () { return check_capability_js_2.handleCheckCapability; } });
|
35
|
+
var get_cache_status_js_2 = require("./get-cache-status.js");
|
36
|
+
Object.defineProperty(exports, "getCacheStatusTool", { enumerable: true, get: function () { return get_cache_status_js_2.getCacheStatusTool; } });
|
37
|
+
Object.defineProperty(exports, "handleGetCacheStatus", { enumerable: true, get: function () { return get_cache_status_js_2.handleGetCacheStatus; } });
|
38
|
+
var query_resources_js_2 = require("./query-resources.js");
|
39
|
+
Object.defineProperty(exports, "queryResourcesTool", { enumerable: true, get: function () { return query_resources_js_2.queryResourcesTool; } });
|
40
|
+
Object.defineProperty(exports, "handleQueryResources", { enumerable: true, get: function () { return query_resources_js_2.handleQueryResources; } });
|
26
41
|
// Collect all tool definitions
|
27
42
|
exports.allTools = [
|
28
|
-
|
43
|
+
// Sprint 1.5 tools
|
44
|
+
// getResourcesTool, // DISABLED - caused 37K+ token responses, replaced by Sprint 2 capability discovery
|
29
45
|
parse_intent_js_1.parseIntentTool,
|
30
46
|
create_vm_js_1.createVMTool,
|
31
47
|
export_training_data_js_1.exportTrainingDataTool,
|
32
|
-
provide_feedback_js_1.provideFeedbackTool
|
48
|
+
provide_feedback_js_1.provideFeedbackTool,
|
49
|
+
// Sprint 2 capability discovery tools
|
50
|
+
discover_capabilities_js_1.discoverCapabilitiesTool,
|
51
|
+
check_capability_js_1.checkCapabilityTool,
|
52
|
+
get_cache_status_js_1.getCacheStatusTool,
|
53
|
+
query_resources_js_1.queryResourcesTool
|
33
54
|
];
|
34
55
|
// Export tool handlers map for easy lookup
|
35
56
|
exports.toolHandlers = {
|
36
|
-
|
57
|
+
// Sprint 1.5 handlers
|
58
|
+
// get_resources: handleGetResources, // DISABLED - caused 37K+ token responses
|
37
59
|
parse_vm_intent: parse_intent_js_1.handleParseIntent,
|
38
60
|
create_vm: create_vm_js_1.handleCreateVM,
|
39
61
|
export_training_data: export_training_data_js_1.handleExportTrainingData,
|
40
|
-
provide_feedback: provide_feedback_js_1.handleProvideFeedback
|
62
|
+
provide_feedback: provide_feedback_js_1.handleProvideFeedback,
|
63
|
+
// Sprint 2 capability discovery handlers
|
64
|
+
discover_capabilities: discover_capabilities_js_1.handleDiscoverCapabilities,
|
65
|
+
check_capability: check_capability_js_1.handleCheckCapability,
|
66
|
+
get_cache_status: get_cache_status_js_1.handleGetCacheStatus,
|
67
|
+
query_resources: query_resources_js_1.handleQueryResources
|
41
68
|
};
|
@@ -0,0 +1,248 @@
|
|
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, hosts, networks) with natural language. Use 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', 'any VMs?', 'Ubuntu hosts', 'VMs in development group'"
|
14
|
+
},
|
15
|
+
limit: {
|
16
|
+
type: "number",
|
17
|
+
description: "Maximum number of results to return (default: 10, max: 100)",
|
18
|
+
default: 10
|
19
|
+
}
|
20
|
+
},
|
21
|
+
required: ["natural_query"]
|
22
|
+
}
|
23
|
+
};
|
24
|
+
function parseResourceQuery(query, limit = 10) {
|
25
|
+
const q = query.toLowerCase().trim();
|
26
|
+
// Determine resource type
|
27
|
+
let resource = 'instances'; // default
|
28
|
+
if (q.includes('host') || q.includes('server') || q.includes('node')) {
|
29
|
+
resource = 'hosts';
|
30
|
+
}
|
31
|
+
else if (q.includes('network')) {
|
32
|
+
resource = 'networks';
|
33
|
+
}
|
34
|
+
// Determine action
|
35
|
+
let action = 'list';
|
36
|
+
if (q.includes('how many') || q.includes('count')) {
|
37
|
+
action = 'count';
|
38
|
+
}
|
39
|
+
else if (q.includes('any ') || q.includes('summary')) {
|
40
|
+
action = 'summary';
|
41
|
+
}
|
42
|
+
// Extract filters
|
43
|
+
const filters = {};
|
44
|
+
// OS Type patterns
|
45
|
+
if (q.includes('windows'))
|
46
|
+
filters.osType = 'windows';
|
47
|
+
if (q.includes('linux'))
|
48
|
+
filters.osType = 'linux';
|
49
|
+
if (q.includes('ubuntu'))
|
50
|
+
filters.osType = 'ubuntu';
|
51
|
+
if (q.includes('centos'))
|
52
|
+
filters.osType = 'centos';
|
53
|
+
if (q.includes('rocky'))
|
54
|
+
filters.osType = 'rocky';
|
55
|
+
if (q.includes('debian'))
|
56
|
+
filters.osType = 'debian';
|
57
|
+
if (q.includes('rhel'))
|
58
|
+
filters.osType = 'rhel';
|
59
|
+
// Status patterns
|
60
|
+
if (q.includes('running'))
|
61
|
+
filters.status = 'running';
|
62
|
+
if (q.includes('stopped'))
|
63
|
+
filters.status = 'stopped';
|
64
|
+
if (q.includes('failed') || q.includes('error'))
|
65
|
+
filters.status = 'failed';
|
66
|
+
if (q.includes('provisioning'))
|
67
|
+
filters.status = 'provisioning';
|
68
|
+
// Group patterns
|
69
|
+
if (q.includes('production') || q.includes('prod'))
|
70
|
+
filters.group = 'production';
|
71
|
+
if (q.includes('development') || q.includes('dev'))
|
72
|
+
filters.group = 'development';
|
73
|
+
if (q.includes('test') || q.includes('testing'))
|
74
|
+
filters.group = 'test';
|
75
|
+
// Name patterns (exact matches)
|
76
|
+
const nameMatch = q.match(/named?\s+["']?([a-zA-Z0-9\-_]+)["']?/);
|
77
|
+
if (nameMatch) {
|
78
|
+
filters.name = nameMatch[1];
|
79
|
+
}
|
80
|
+
return { resource, action, filters, limit };
|
81
|
+
}
|
82
|
+
function filterResults(data, filters) {
|
83
|
+
return data.filter(item => {
|
84
|
+
// OS Type filtering
|
85
|
+
if (filters.osType) {
|
86
|
+
const itemOS = item.osType?.toLowerCase() || '';
|
87
|
+
const filterOS = filters.osType.toLowerCase();
|
88
|
+
if (filterOS === 'windows' && !itemOS.includes('windows'))
|
89
|
+
return false;
|
90
|
+
if (filterOS === 'linux' && !itemOS.includes('linux') && !itemOS.includes('ubuntu') && !itemOS.includes('centos') && !itemOS.includes('rocky') && !itemOS.includes('debian') && !itemOS.includes('rhel'))
|
91
|
+
return false;
|
92
|
+
if (filterOS !== 'windows' && filterOS !== 'linux' && !itemOS.includes(filterOS))
|
93
|
+
return false;
|
94
|
+
}
|
95
|
+
// Status filtering
|
96
|
+
if (filters.status) {
|
97
|
+
const itemStatus = item.status?.toLowerCase() || '';
|
98
|
+
if (!itemStatus.includes(filters.status.toLowerCase()))
|
99
|
+
return false;
|
100
|
+
}
|
101
|
+
// Group filtering
|
102
|
+
if (filters.group) {
|
103
|
+
const itemGroup = item.group?.name?.toLowerCase() || item.site?.name?.toLowerCase() || '';
|
104
|
+
if (!itemGroup.includes(filters.group.toLowerCase()))
|
105
|
+
return false;
|
106
|
+
}
|
107
|
+
// Name filtering
|
108
|
+
if (filters.name) {
|
109
|
+
const itemName = item.name?.toLowerCase() || '';
|
110
|
+
if (!itemName.includes(filters.name.toLowerCase()))
|
111
|
+
return false;
|
112
|
+
}
|
113
|
+
return true;
|
114
|
+
});
|
115
|
+
}
|
116
|
+
function formatResponse(parsedQuery, results, totalCount) {
|
117
|
+
const { action, resource, filters } = parsedQuery;
|
118
|
+
if (action === 'count') {
|
119
|
+
return {
|
120
|
+
action: 'count',
|
121
|
+
resource: resource,
|
122
|
+
filters_applied: filters,
|
123
|
+
total_count: results.length,
|
124
|
+
query_summary: `Found ${results.length} ${resource} matching criteria`
|
125
|
+
};
|
126
|
+
}
|
127
|
+
if (action === 'summary') {
|
128
|
+
const statusCounts = {};
|
129
|
+
const osCounts = {};
|
130
|
+
results.forEach(item => {
|
131
|
+
const status = item.status || 'unknown';
|
132
|
+
const os = item.osType || 'unknown';
|
133
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
134
|
+
osCounts[os] = (osCounts[os] || 0) + 1;
|
135
|
+
});
|
136
|
+
return {
|
137
|
+
action: 'summary',
|
138
|
+
resource: resource,
|
139
|
+
filters_applied: filters,
|
140
|
+
total_count: results.length,
|
141
|
+
summary: {
|
142
|
+
by_status: statusCounts,
|
143
|
+
by_os_type: osCounts
|
144
|
+
},
|
145
|
+
query_summary: `Found ${results.length} ${resource} matching criteria`
|
146
|
+
};
|
147
|
+
}
|
148
|
+
// Default: list action
|
149
|
+
const limitedResults = results.slice(0, parsedQuery.limit);
|
150
|
+
return {
|
151
|
+
action: 'list',
|
152
|
+
resource: resource,
|
153
|
+
filters_applied: filters,
|
154
|
+
total_count: results.length,
|
155
|
+
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
|
+
})),
|
168
|
+
query_summary: `Showing ${limitedResults.length} of ${results.length} ${resource} matching criteria`
|
169
|
+
};
|
170
|
+
}
|
171
|
+
async function handleQueryResources(args) {
|
172
|
+
try {
|
173
|
+
const { natural_query, limit = 10 } = args;
|
174
|
+
if (!natural_query || typeof natural_query !== 'string') {
|
175
|
+
return {
|
176
|
+
content: [
|
177
|
+
{
|
178
|
+
type: "text",
|
179
|
+
text: JSON.stringify({
|
180
|
+
error: "Invalid input",
|
181
|
+
message: "natural_query parameter is required and must be a string",
|
182
|
+
examples: ["any VMs?", "Windows VMs", "stopped VMs in production", "Ubuntu hosts"]
|
183
|
+
}, null, 2)
|
184
|
+
}
|
185
|
+
],
|
186
|
+
isError: true
|
187
|
+
};
|
188
|
+
}
|
189
|
+
const parsedQuery = parseResourceQuery(natural_query, Math.min(limit, 100));
|
190
|
+
// Map resource type to API endpoint
|
191
|
+
const endpointMap = {
|
192
|
+
'instances': '/instances',
|
193
|
+
'hosts': '/servers',
|
194
|
+
'networks': '/networks'
|
195
|
+
};
|
196
|
+
const endpoint = endpointMap[parsedQuery.resource];
|
197
|
+
// Query the VME API
|
198
|
+
const response = await api_utils_js_1.api.get(endpoint);
|
199
|
+
// Extract data array from response
|
200
|
+
let rawData = [];
|
201
|
+
if (parsedQuery.resource === 'instances' && response.data.instances) {
|
202
|
+
rawData = response.data.instances;
|
203
|
+
}
|
204
|
+
else if (parsedQuery.resource === 'hosts' && response.data.servers) {
|
205
|
+
rawData = response.data.servers;
|
206
|
+
}
|
207
|
+
else if (parsedQuery.resource === 'networks' && response.data.networks) {
|
208
|
+
rawData = response.data.networks;
|
209
|
+
}
|
210
|
+
else {
|
211
|
+
// Fallback: try to find array in response
|
212
|
+
const possibleArrays = Object.values(response.data).filter(Array.isArray);
|
213
|
+
if (possibleArrays.length > 0) {
|
214
|
+
rawData = possibleArrays[0];
|
215
|
+
}
|
216
|
+
}
|
217
|
+
// Apply filters
|
218
|
+
const filteredResults = filterResults(rawData, parsedQuery.filters);
|
219
|
+
// Format response based on action
|
220
|
+
const formattedResponse = formatResponse(parsedQuery, filteredResults, rawData.length);
|
221
|
+
return {
|
222
|
+
content: [
|
223
|
+
{
|
224
|
+
type: "text",
|
225
|
+
text: JSON.stringify(formattedResponse, null, 2)
|
226
|
+
}
|
227
|
+
],
|
228
|
+
isError: false
|
229
|
+
};
|
230
|
+
}
|
231
|
+
catch (error) {
|
232
|
+
return {
|
233
|
+
content: [
|
234
|
+
{
|
235
|
+
type: "text",
|
236
|
+
text: JSON.stringify({
|
237
|
+
error: "Resource query failed",
|
238
|
+
message: error.message,
|
239
|
+
natural_query: args?.natural_query || "unknown",
|
240
|
+
suggestion: "Check VME API connectivity or try discover_capabilities first to see available resources"
|
241
|
+
}, null, 2)
|
242
|
+
}
|
243
|
+
],
|
244
|
+
isError: true
|
245
|
+
};
|
246
|
+
}
|
247
|
+
}
|
248
|
+
exports.handleQueryResources = handleQueryResources;
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "vme-mcp-server",
|
3
|
-
"version": "0.1.
|
4
|
-
"description": "
|
3
|
+
"version": "0.1.7",
|
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
|
}
|