@calltelemetry/ct-lab-mcp 0.1.4 → 0.2.0

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 (139) hide show
  1. package/dist/bin/ct-lab-mcp.d.ts.map +1 -1
  2. package/dist/bin/ct-lab-mcp.js +21 -7
  3. package/dist/bin/ct-lab-mcp.js.map +1 -1
  4. package/dist/src/appliance/commands.d.ts.map +1 -1
  5. package/dist/src/appliance/commands.js +24 -0
  6. package/dist/src/appliance/commands.js.map +1 -1
  7. package/dist/src/appliance/executor.d.ts.map +1 -1
  8. package/dist/src/appliance/executor.js +7 -1
  9. package/dist/src/appliance/executor.js.map +1 -1
  10. package/dist/src/appliance/mock.d.ts +6 -0
  11. package/dist/src/appliance/mock.d.ts.map +1 -1
  12. package/dist/src/appliance/mock.js +27 -6
  13. package/dist/src/appliance/mock.js.map +1 -1
  14. package/dist/src/appliance/types.d.ts +20 -1
  15. package/dist/src/appliance/types.d.ts.map +1 -1
  16. package/dist/src/appliance/types.js.map +1 -1
  17. package/dist/src/credentials/errors.d.ts +16 -0
  18. package/dist/src/credentials/errors.d.ts.map +1 -0
  19. package/dist/src/credentials/errors.js +21 -0
  20. package/dist/src/credentials/errors.js.map +1 -0
  21. package/dist/src/credentials/resolver.d.ts +16 -0
  22. package/dist/src/credentials/resolver.d.ts.map +1 -1
  23. package/dist/src/credentials/resolver.js +104 -10
  24. package/dist/src/credentials/resolver.js.map +1 -1
  25. package/dist/src/credentials/types.d.ts +8 -0
  26. package/dist/src/credentials/types.d.ts.map +1 -1
  27. package/dist/src/diagnostics/collector.d.ts +37 -0
  28. package/dist/src/diagnostics/collector.d.ts.map +1 -0
  29. package/dist/src/diagnostics/collector.js +452 -0
  30. package/dist/src/diagnostics/collector.js.map +1 -0
  31. package/dist/src/diagnostics/types.d.ts +103 -0
  32. package/dist/src/diagnostics/types.d.ts.map +1 -0
  33. package/dist/src/diagnostics/types.js +8 -0
  34. package/dist/src/diagnostics/types.js.map +1 -0
  35. package/dist/src/hypervisors/esxi/client.d.ts +1 -0
  36. package/dist/src/hypervisors/esxi/client.d.ts.map +1 -1
  37. package/dist/src/hypervisors/esxi/client.js +15 -3
  38. package/dist/src/hypervisors/esxi/client.js.map +1 -1
  39. package/dist/src/hypervisors/esxi/mock.d.ts +1 -0
  40. package/dist/src/hypervisors/esxi/mock.d.ts.map +1 -1
  41. package/dist/src/hypervisors/esxi/mock.js +4 -0
  42. package/dist/src/hypervisors/esxi/mock.js.map +1 -1
  43. package/dist/src/hypervisors/manager.d.ts.map +1 -1
  44. package/dist/src/hypervisors/manager.js +37 -3
  45. package/dist/src/hypervisors/manager.js.map +1 -1
  46. package/dist/src/hypervisors/proxmox/client.d.ts +18 -1
  47. package/dist/src/hypervisors/proxmox/client.d.ts.map +1 -1
  48. package/dist/src/hypervisors/proxmox/client.js +563 -3
  49. package/dist/src/hypervisors/proxmox/client.js.map +1 -1
  50. package/dist/src/hypervisors/proxmox/mock.d.ts +1 -0
  51. package/dist/src/hypervisors/proxmox/mock.d.ts.map +1 -1
  52. package/dist/src/hypervisors/proxmox/mock.js +4 -0
  53. package/dist/src/hypervisors/proxmox/mock.js.map +1 -1
  54. package/dist/src/hypervisors/types.d.ts +159 -1
  55. package/dist/src/hypervisors/types.d.ts.map +1 -1
  56. package/dist/src/proxmox/mock.d.ts +2 -1
  57. package/dist/src/proxmox/mock.d.ts.map +1 -1
  58. package/dist/src/proxmox/mock.js +202 -0
  59. package/dist/src/proxmox/mock.js.map +1 -1
  60. package/dist/src/server.d.ts.map +1 -1
  61. package/dist/src/server.js +2 -0
  62. package/dist/src/server.js.map +1 -1
  63. package/dist/src/slot/manager.d.ts +26 -1
  64. package/dist/src/slot/manager.d.ts.map +1 -1
  65. package/dist/src/slot/manager.js +278 -1
  66. package/dist/src/slot/manager.js.map +1 -1
  67. package/dist/src/slot/types.d.ts +49 -0
  68. package/dist/src/slot/types.d.ts.map +1 -1
  69. package/dist/src/slot/types.js +18 -0
  70. package/dist/src/slot/types.js.map +1 -1
  71. package/dist/src/tools/credential-status.d.ts +29 -0
  72. package/dist/src/tools/credential-status.d.ts.map +1 -0
  73. package/dist/src/tools/credential-status.js +81 -0
  74. package/dist/src/tools/credential-status.js.map +1 -0
  75. package/dist/src/tools/cube-provision.d.ts +288 -0
  76. package/dist/src/tools/cube-provision.d.ts.map +1 -0
  77. package/dist/src/tools/cube-provision.js +892 -0
  78. package/dist/src/tools/cube-provision.js.map +1 -0
  79. package/dist/src/tools/cube-tools.d.ts +137 -0
  80. package/dist/src/tools/cube-tools.d.ts.map +1 -0
  81. package/dist/src/tools/cube-tools.js +292 -0
  82. package/dist/src/tools/cube-tools.js.map +1 -0
  83. package/dist/src/tools/cucm-emulator-tool.d.ts +85 -0
  84. package/dist/src/tools/cucm-emulator-tool.d.ts.map +1 -0
  85. package/dist/src/tools/cucm-emulator-tool.js +191 -0
  86. package/dist/src/tools/cucm-emulator-tool.js.map +1 -0
  87. package/dist/src/tools/cucm-trunk.d.ts +165 -0
  88. package/dist/src/tools/cucm-trunk.d.ts.map +1 -0
  89. package/dist/src/tools/cucm-trunk.js +354 -0
  90. package/dist/src/tools/cucm-trunk.js.map +1 -0
  91. package/dist/src/tools/diagnostics.d.ts +58 -0
  92. package/dist/src/tools/diagnostics.d.ts.map +1 -0
  93. package/dist/src/tools/diagnostics.js +212 -0
  94. package/dist/src/tools/diagnostics.js.map +1 -0
  95. package/dist/src/tools/guest-agent.d.ts +34 -0
  96. package/dist/src/tools/guest-agent.d.ts.map +1 -0
  97. package/dist/src/tools/guest-agent.js +133 -0
  98. package/dist/src/tools/guest-agent.js.map +1 -0
  99. package/dist/src/tools/index.d.ts +11 -0
  100. package/dist/src/tools/index.d.ts.map +1 -1
  101. package/dist/src/tools/index.js +111 -1
  102. package/dist/src/tools/index.js.map +1 -1
  103. package/dist/src/tools/list-hosts.d.ts +9 -0
  104. package/dist/src/tools/list-hosts.d.ts.map +1 -1
  105. package/dist/src/tools/list-hosts.js.map +1 -1
  106. package/dist/src/tools/network-tools.d.ts +65 -0
  107. package/dist/src/tools/network-tools.d.ts.map +1 -0
  108. package/dist/src/tools/network-tools.js +200 -0
  109. package/dist/src/tools/network-tools.js.map +1 -0
  110. package/dist/src/tools/provision-tools.d.ts.map +1 -1
  111. package/dist/src/tools/provision-tools.js +19 -0
  112. package/dist/src/tools/provision-tools.js.map +1 -1
  113. package/dist/src/tools/server-management.d.ts +51 -0
  114. package/dist/src/tools/server-management.d.ts.map +1 -0
  115. package/dist/src/tools/server-management.js +175 -0
  116. package/dist/src/tools/server-management.js.map +1 -0
  117. package/dist/src/tools/storage-tools.d.ts +250 -0
  118. package/dist/src/tools/storage-tools.d.ts.map +1 -0
  119. package/dist/src/tools/storage-tools.js +685 -0
  120. package/dist/src/tools/storage-tools.js.map +1 -0
  121. package/dist/src/tools/update-cli.d.ts +62 -0
  122. package/dist/src/tools/update-cli.d.ts.map +1 -0
  123. package/dist/src/tools/update-cli.js +142 -0
  124. package/dist/src/tools/update-cli.js.map +1 -0
  125. package/dist/src/tools/vm-action.d.ts +6 -6
  126. package/dist/src/tools/vm-action.d.ts.map +1 -1
  127. package/dist/src/tools/vm-action.js +3 -1
  128. package/dist/src/tools/vm-action.js.map +1 -1
  129. package/dist/src/types/mcp.d.ts +5 -1
  130. package/dist/src/types/mcp.d.ts.map +1 -1
  131. package/dist/src/types/mcp.js +14 -0
  132. package/dist/src/types/mcp.js.map +1 -1
  133. package/dist/src/types/proxmox.d.ts +1 -0
  134. package/dist/src/types/proxmox.d.ts.map +1 -1
  135. package/dist/src/utils/formatters.d.ts +7 -0
  136. package/dist/src/utils/formatters.d.ts.map +1 -1
  137. package/dist/src/utils/formatters.js +48 -5
  138. package/dist/src/utils/formatters.js.map +1 -1
  139. package/package.json +1 -1
@@ -0,0 +1,685 @@
1
+ /**
2
+ * Storage & Datastore Management MCP Tools
3
+ *
4
+ * Tools:
5
+ * 1. list_storage - Query datastores, pools, free space, and volume contents
6
+ * 2. list_snapshots - Query VM snapshots across Proxmox and ESXi hosts
7
+ * 3. clean_storage - Delete unreferenced disks, stale ISOs, or snapshots with safety guardrails
8
+ */
9
+ import { z } from "zod";
10
+ import { ProxmoxErrorCode, createToolErrorResult } from "../types/mcp.js";
11
+ import { logger } from "../utils/logger.js";
12
+ import { telemetry } from "../telemetry/registry.js";
13
+ // ============================================================================
14
+ // 1. list_storage
15
+ // ============================================================================
16
+ export const ListStorageInputShape = {
17
+ host: z
18
+ .enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "all"])
19
+ .optional()
20
+ .default("all")
21
+ .describe("Specific host or 'all' to inspect."),
22
+ includeContent: z
23
+ .boolean()
24
+ .optional()
25
+ .default(true)
26
+ .describe("Whether to list detailed volume contents (ISOs, VM disks, backups) inside storage pools."),
27
+ storage: z
28
+ .string()
29
+ .optional()
30
+ .describe("Filter to a specific storage pool name (e.g. 'local', 'local-lvm', 'synology-cisco-isos').")
31
+ };
32
+ export const ListStorageInputSchema = z.object(ListStorageInputShape);
33
+ export async function handleListStorage(args, context) {
34
+ const parsed = ListStorageInputSchema.safeParse(args || {});
35
+ if (!parsed.success) {
36
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for list_storage.");
37
+ }
38
+ const { host, includeContent, storage: targetStorage } = parsed.data;
39
+ const drivers = context.hypervisorManager?.getAllDrivers() || [];
40
+ const matchedDrivers = drivers.filter((d) => {
41
+ if (host === "all")
42
+ return true;
43
+ return d.hostName === host || (host === "proxmox-mini" && d.hostName === "proxmox-minis-lab");
44
+ });
45
+ if (matchedDrivers.length === 0) {
46
+ return createToolErrorResult(ProxmoxErrorCode.HOST_NOT_FOUND, `No matching hypervisor driver found for host '${host}'.`);
47
+ }
48
+ const allPools = [];
49
+ const allContents = [];
50
+ for (const driver of matchedDrivers) {
51
+ if (driver.listStorage) {
52
+ try {
53
+ const pools = await driver.listStorage();
54
+ for (const p of pools) {
55
+ if (!targetStorage || p.storage.toLowerCase() === targetStorage.toLowerCase()) {
56
+ allPools.push(p);
57
+ if (includeContent && driver.listStorageContent && p.active) {
58
+ try {
59
+ const contents = await driver.listStorageContent(p.storage);
60
+ allContents.push(...contents);
61
+ }
62
+ catch (err) {
63
+ logger.warn(`Failed to query storage content for pool ${p.storage} on ${driver.hostName}:`, err.message);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ catch (err) {
70
+ logger.error(`Failed to list storage for ${driver.hostName}:`, err.message);
71
+ }
72
+ }
73
+ }
74
+ const markdown = formatStorageReport(allPools, allContents);
75
+ return {
76
+ content: [{ type: "text", text: markdown }],
77
+ structuredContent: {
78
+ success: true,
79
+ totalPools: allPools.length,
80
+ pools: allPools,
81
+ contents: allContents
82
+ }
83
+ };
84
+ }
85
+ function formatStorageReport(pools, contents) {
86
+ if (pools.length === 0) {
87
+ return "### 💾 Lab Storage Pools\n\nNo storage pools found.";
88
+ }
89
+ const poolRows = pools.map((p) => {
90
+ const totalGb = (p.totalBytes / (1024 * 1024 * 1024)).toFixed(1);
91
+ const usedGb = (p.usedBytes / (1024 * 1024 * 1024)).toFixed(1);
92
+ const availGb = (p.availableBytes / (1024 * 1024 * 1024)).toFixed(1);
93
+ const pct = Math.round(p.usedFraction * 100);
94
+ const statusIcon = pct > 85 ? "🔴" : pct > 70 ? "🟡" : "🟢";
95
+ return `| **\`${p.hostName}\`** | **\`${p.storage}\`** | \`${p.type}\` | ${totalGb} GB | ${usedGb} GB (${pct}%) | **${availGb} GB** | ${statusIcon} ${p.active ? "Active" : "Inactive"} | \`${p.content}\` |`;
96
+ }).join("\n");
97
+ let contentSection = "";
98
+ if (contents.length > 0) {
99
+ const contentRows = contents.map((c) => {
100
+ const sizeGb = (c.sizeBytes / (1024 * 1024 * 1024)).toFixed(2);
101
+ const vmidStr = c.vmid ? `VM ${c.vmid}` : "N/A (Host/ISO)";
102
+ return `| \`${c.hostName}\` | \`${c.storage}\` | **\`${c.volid}\`** | ${sizeGb} GB | \`${c.content}\` | ${vmidStr} |`;
103
+ }).join("\n");
104
+ contentSection = `\n\n#### 📦 Storage Volume & ISO Content Breakdown\n\n| Host | Storage | Volume ID / ISO | Size | Type | Owner |\n|---|---|---|:---:|:---:|:---:|\n${contentRows}`;
105
+ }
106
+ return `### 💾 Lab Storage Pools & Datastore Analysis
107
+
108
+ | Host | Storage Pool | Type | Total | Used (%) | Free Space | Status | Content Types |
109
+ |---|---|:---:|:---:|:---:|:---:|:---:|---|
110
+ ${poolRows}${contentSection}`;
111
+ }
112
+ // ============================================================================
113
+ // 2. list_snapshots
114
+ // ============================================================================
115
+ export const ListSnapshotsInputShape = {
116
+ host: z
117
+ .enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "all"])
118
+ .optional()
119
+ .default("all")
120
+ .describe("Specific host or 'all'."),
121
+ vmId: z
122
+ .string()
123
+ .optional()
124
+ .describe("Specific VM ID or name to inspect snapshots for.")
125
+ };
126
+ export const ListSnapshotsInputSchema = z.object(ListSnapshotsInputShape);
127
+ export async function handleListSnapshots(args, context) {
128
+ const parsed = ListSnapshotsInputSchema.safeParse(args || {});
129
+ if (!parsed.success) {
130
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for list_snapshots.");
131
+ }
132
+ const { host, vmId } = parsed.data;
133
+ const drivers = context.hypervisorManager?.getAllDrivers() || [];
134
+ const matchedDrivers = drivers.filter((d) => {
135
+ if (host === "all")
136
+ return true;
137
+ return d.hostName === host || (host === "proxmox-mini" && d.hostName === "proxmox-minis-lab");
138
+ });
139
+ const allSnapshots = [];
140
+ for (const driver of matchedDrivers) {
141
+ if (driver.listSnapshots) {
142
+ if (vmId) {
143
+ try {
144
+ const snaps = await driver.listSnapshots(vmId);
145
+ allSnapshots.push(...snaps);
146
+ }
147
+ catch (err) {
148
+ logger.warn(`Failed to get snapshots for VM ${vmId} on ${driver.hostName}:`, err.message);
149
+ }
150
+ }
151
+ else {
152
+ const vms = await driver.listVms();
153
+ for (const vm of vms) {
154
+ try {
155
+ const snaps = await driver.listSnapshots(vm.id);
156
+ allSnapshots.push(...snaps);
157
+ }
158
+ catch { }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ const rows = allSnapshots.map((s) => {
164
+ const dateStr = s.snapdate ? s.snapdate.split("T")[0] : "Unknown";
165
+ return `| \`${s.hostName}\` | **${s.vmName}** (\`${s.vmId}\`) | **\`${s.name}\`** | ${dateStr} | ${s.description || "None"} | ${s.vmstate ? "RAM Included" : "Disk Only"} |`;
166
+ }).join("\n");
167
+ const markdown = allSnapshots.length === 0
168
+ ? "### 📸 VM Snapshots Analysis\n\nNo VM snapshots found across inspected hosts."
169
+ : `### 📸 VM Snapshots Analysis (${allSnapshots.length} Found)\n\n| Host | VM Name | Snapshot Name | Date | Description | State |\n|---|---|---|:---:|---|:---:|\n${rows}`;
170
+ return {
171
+ content: [{ type: "text", text: markdown }],
172
+ structuredContent: {
173
+ success: true,
174
+ totalSnapshots: allSnapshots.length,
175
+ snapshots: allSnapshots
176
+ }
177
+ };
178
+ }
179
+ // ============================================================================
180
+ // 3. clean_storage
181
+ // ============================================================================
182
+ export const CleanStorageInputShape = {
183
+ host: z
184
+ .enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176"])
185
+ .describe("Target hypervisor host where the volume or snapshot resides."),
186
+ action: z
187
+ .enum(["delete_volume", "delete_snapshot"])
188
+ .describe("Action to perform: 'delete_volume' (e.g. stale ISO/disk) or 'delete_snapshot'."),
189
+ volumeId: z
190
+ .string()
191
+ .optional()
192
+ .describe("Volume ID to delete (e.g. 'local:iso/AlmaLinux-9-latest-x86_64-dvd.iso' or 'local-lvm:vm-105-disk-0'). Required if action is 'delete_volume'."),
193
+ vmId: z
194
+ .string()
195
+ .optional()
196
+ .describe("VM ID for snapshot deletion. Required if action is 'delete_snapshot'."),
197
+ snapshotName: z
198
+ .string()
199
+ .optional()
200
+ .describe("Snapshot name to delete. Required if action is 'delete_snapshot'.")
201
+ };
202
+ export const CleanStorageInputSchema = z.object(CleanStorageInputShape);
203
+ export async function handleCleanStorage(args, context) {
204
+ const parsed = CleanStorageInputSchema.safeParse(args);
205
+ if (!parsed.success) {
206
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for clean_storage.");
207
+ }
208
+ const { host, action, volumeId, vmId, snapshotName } = parsed.data;
209
+ const driver = context.hypervisorManager?.getDriver(host);
210
+ if (!driver) {
211
+ return createToolErrorResult(ProxmoxErrorCode.HOST_NOT_FOUND, `Hypervisor host '${host}' not found.`);
212
+ }
213
+ try {
214
+ if (action === "delete_volume") {
215
+ if (!volumeId) {
216
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "'volumeId' is required for action 'delete_volume'.");
217
+ }
218
+ if (!driver.deleteVolume) {
219
+ return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${host}' does not support direct volume deletion.`);
220
+ }
221
+ logger.info(`Deleting volume '${volumeId}' on host '${host}'...`);
222
+ await driver.deleteVolume(volumeId);
223
+ telemetry.emit({
224
+ event: "storage.delete_volume",
225
+ actor: "ct-lab-mcp",
226
+ status: "success",
227
+ hostId: host,
228
+ metadata: { volumeId }
229
+ });
230
+ return {
231
+ content: [{ type: "text", text: `### 🗑️ Volume Deleted Successfully\n\nVolume **\`${volumeId}\`** was deleted from host **\`${host}\`**.` }],
232
+ structuredContent: { success: true, host, volumeId }
233
+ };
234
+ }
235
+ if (action === "delete_snapshot") {
236
+ if (!vmId || !snapshotName) {
237
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "'vmId' and 'snapshotName' are required for action 'delete_snapshot'.");
238
+ }
239
+ if (!driver.deleteSnapshot) {
240
+ return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${host}' does not support snapshot deletion.`);
241
+ }
242
+ logger.info(`Deleting snapshot '${snapshotName}' on VM '${vmId}' (${host})...`);
243
+ await driver.deleteSnapshot(vmId, snapshotName);
244
+ telemetry.emit({
245
+ event: "storage.delete_snapshot",
246
+ actor: "ct-lab-mcp",
247
+ status: "success",
248
+ hostId: host,
249
+ vmId,
250
+ metadata: { snapshotName }
251
+ });
252
+ return {
253
+ content: [{ type: "text", text: `### 🗑️ Snapshot Deleted Successfully\n\nSnapshot **\`${snapshotName}\`** on VM **\`${vmId}\`** (${host}) was deleted.` }],
254
+ structuredContent: { success: true, host, vmId, snapshotName }
255
+ };
256
+ }
257
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, `Unknown action '${action}'.`);
258
+ }
259
+ catch (err) {
260
+ const msg = err instanceof Error ? err.message : String(err);
261
+ logger.error(`clean_storage failed on ${host}: ${msg}`);
262
+ return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to clean storage on '${host}': ${msg}`);
263
+ }
264
+ }
265
+ // ============================================================================
266
+ // 4. get_vm_storage
267
+ // ============================================================================
268
+ export const GetVmStorageInputShape = {
269
+ target: z
270
+ .union([z.string(), z.number()])
271
+ .optional()
272
+ .describe("VM ID or name to query storage for (e.g. '105' or 'ct-appliance-slot2-thin')."),
273
+ vmIdOrName: z
274
+ .union([z.string(), z.number()])
275
+ .optional()
276
+ .describe("Alias for target."),
277
+ host: z
278
+ .enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
279
+ .optional()
280
+ .default("auto")
281
+ .describe("Hypervisor host or 'auto' to auto-discover.")
282
+ };
283
+ export const GetVmStorageInputSchema = z
284
+ .object(GetVmStorageInputShape)
285
+ .refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
286
+ export async function handleGetVmStorage(args, context) {
287
+ const parsed = GetVmStorageInputSchema.safeParse(args || {});
288
+ if (!parsed.success) {
289
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for get_vm_storage.");
290
+ }
291
+ const target = parsed.data.target || parsed.data.vmIdOrName;
292
+ const targetHost = parsed.data.host;
293
+ const drivers = context.hypervisorManager?.getAllDrivers() || [];
294
+ let foundDriver = targetHost && targetHost !== "auto"
295
+ ? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
296
+ : null;
297
+ let vmRecord = null;
298
+ if (foundDriver) {
299
+ vmRecord = await foundDriver.getVm(target);
300
+ }
301
+ else {
302
+ for (const d of drivers) {
303
+ const vm = await d.getVm(target);
304
+ if (vm) {
305
+ foundDriver = d;
306
+ vmRecord = vm;
307
+ break;
308
+ }
309
+ }
310
+ }
311
+ if (!foundDriver || !vmRecord) {
312
+ return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' was not found on any hypervisor host.`);
313
+ }
314
+ if (!foundDriver.getVmStorage) {
315
+ return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support getVmStorage.`);
316
+ }
317
+ try {
318
+ const report = await foundDriver.getVmStorage(vmRecord.id);
319
+ // Format Markdown tables
320
+ const diskRows = report.disks.map((d) => {
321
+ const sizeStr = d.sizeGb ? `${d.sizeGb.toFixed(1)} GB` : "Unknown";
322
+ const flags = [d.discard ? "discard" : null, d.ssd ? "ssd" : null].filter(Boolean).join(", ");
323
+ return `| **\`${d.drive}\`** | \`${d.storage}\` | **\`${d.volid}\`** | ${sizeStr} | \`${d.format}\` | ${flags || "standard"} |`;
324
+ }).join("\n");
325
+ const cdromRows = report.cdroms.map((c) => {
326
+ const mediaStr = c.volid === "none" || !c.volid ? "*(Empty / Ejected)*" : `**\`${c.volid}\`**`;
327
+ return `| **\`${c.drive}\`** | ${mediaStr} | \`${c.storage}\` | \`cdrom\` |`;
328
+ }).join("\n");
329
+ let fsSection = "";
330
+ if (report.guestFilesystems && report.guestFilesystems.length > 0) {
331
+ const fsRows = report.guestFilesystems.map((fs) => {
332
+ const totalGb = (fs.totalBytes / (1024 * 1024 * 1024)).toFixed(2);
333
+ const usedGb = (fs.usedBytes / (1024 * 1024 * 1024)).toFixed(2);
334
+ const availGb = (fs.availableBytes / (1024 * 1024 * 1024)).toFixed(2);
335
+ const icon = fs.usedPercent > 85 ? "🔴" : fs.usedPercent > 70 ? "🟡" : "🟢";
336
+ return `| **\`${fs.mountPoint}\`** | \`${fs.fsType}\` | ${totalGb} GB | ${usedGb} GB (${fs.usedPercent}%) | **${availGb} GB** | ${icon} | \`${fs.diskDevice || "N/A"}\` |`;
337
+ }).join("\n");
338
+ fsSection = `\n\n#### 💽 Live Guest OS Filesystems (QEMU Agent)\n\n| Mount Point | Type | Total | Used (%) | Free | Status | Device |\n|---|:---:|:---:|:---:|:---:|:---:|:---:|\n${fsRows}`;
339
+ }
340
+ else {
341
+ fsSection = `\n\n*Guest Filesystem Info: ${report.status === "running" ? "QEMU Agent not responding / initializing" : "VM is stopped"}*`;
342
+ }
343
+ const markdown = `### 💾 VM Storage & Filesystem Analysis: **${report.vmName}** (\`${report.vmId}\`)
344
+
345
+ *Host:* \`${report.hostName}\` | *Status:* **${report.status}** | *Guest Agent:* **${report.guestAgentActive ? "🟢 Active" : "⚪ Offline"}**
346
+
347
+ #### 🗄️ Hypervisor Virtual Disks
348
+ | Drive | Storage Pool | Volume ID | Size | Format | Flags |
349
+ |---|---|---|:---:|:---:|:---:|
350
+ ${diskRows || "| - | No virtual disks attached | - | - | - | - |"}
351
+
352
+ #### 💿 Optical Drives & Mounted Media (CD-ROM)
353
+ | Drive | Mounted Media / ISO | Storage Pool | Type |
354
+ |---|---|---|:---:|
355
+ ${cdromRows || "| - | No CD-ROM drives configured | - | - |"}${fsSection}`;
356
+ return {
357
+ content: [{ type: "text", text: markdown }],
358
+ structuredContent: {
359
+ success: true,
360
+ report
361
+ }
362
+ };
363
+ }
364
+ catch (err) {
365
+ const msg = err instanceof Error ? err.message : String(err);
366
+ return createToolErrorResult(ProxmoxErrorCode.UPSTREAM_API_ERROR, `Failed to query VM storage for '${target}': ${msg}`);
367
+ }
368
+ }
369
+ // ============================================================================
370
+ // 5. mount_iso
371
+ // ============================================================================
372
+ export const MountIsoInputShape = {
373
+ target: z
374
+ .union([z.string(), z.number()])
375
+ .optional()
376
+ .describe("Target VM ID or name to mount ISO onto (e.g. '105' or 'ct-appliance-slot2-thin')."),
377
+ vmIdOrName: z
378
+ .union([z.string(), z.number()])
379
+ .optional()
380
+ .describe("Alias for target."),
381
+ iso: z
382
+ .string()
383
+ .describe("ISO Volume ID to mount (e.g. 'synology-cisco-isos:iso/c8000v-universalk9.17.18.02.iso' or 'local:iso/cube-pmx-internal-bootstrap.iso')."),
384
+ drive: z
385
+ .string()
386
+ .optional()
387
+ .default("ide2")
388
+ .describe("CD-ROM drive identifier (default: 'ide2')."),
389
+ host: z
390
+ .enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
391
+ .optional()
392
+ .default("auto")
393
+ .describe("Hypervisor host or 'auto'.")
394
+ };
395
+ export const MountIsoInputSchema = z
396
+ .object(MountIsoInputShape)
397
+ .refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
398
+ export async function handleMountIso(args, context) {
399
+ const parsed = MountIsoInputSchema.safeParse(args || {});
400
+ if (!parsed.success) {
401
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for mount_iso.");
402
+ }
403
+ const target = parsed.data.target || parsed.data.vmIdOrName;
404
+ const { iso, drive, host: targetHost } = parsed.data;
405
+ const drivers = context.hypervisorManager?.getAllDrivers() || [];
406
+ let foundDriver = targetHost && targetHost !== "auto"
407
+ ? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
408
+ : null;
409
+ let vmRecord = null;
410
+ if (foundDriver) {
411
+ vmRecord = await foundDriver.getVm(target);
412
+ }
413
+ else {
414
+ for (const d of drivers) {
415
+ const vm = await d.getVm(target);
416
+ if (vm) {
417
+ foundDriver = d;
418
+ vmRecord = vm;
419
+ break;
420
+ }
421
+ }
422
+ }
423
+ if (!foundDriver || !vmRecord) {
424
+ return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' not found.`);
425
+ }
426
+ if (!foundDriver.mountIso) {
427
+ return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support mountIso.`);
428
+ }
429
+ try {
430
+ await foundDriver.mountIso(vmRecord.id, iso, drive);
431
+ telemetry.emit({
432
+ event: "storage.mount_iso",
433
+ actor: "ct-lab-mcp",
434
+ status: "success",
435
+ hostId: foundDriver.hostName,
436
+ vmId: vmRecord.id,
437
+ metadata: { iso, drive }
438
+ });
439
+ return {
440
+ content: [{ type: "text", text: `### 💿 ISO Mounted Successfully\n\nISO **\`${iso}\`** was mounted to drive **\`${drive}\`** on VM **${vmRecord.name}** (\`${vmRecord.id}\`) on host \`${foundDriver.hostName}\`.` }],
441
+ structuredContent: { success: true, host: foundDriver.hostName, vmId: vmRecord.id, vmName: vmRecord.name, iso, drive }
442
+ };
443
+ }
444
+ catch (err) {
445
+ const msg = err instanceof Error ? err.message : String(err);
446
+ return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to mount ISO on VM '${target}': ${msg}`);
447
+ }
448
+ }
449
+ // ============================================================================
450
+ // 6. unmount_iso
451
+ // ============================================================================
452
+ export const UnmountIsoInputShape = {
453
+ target: z
454
+ .union([z.string(), z.number()])
455
+ .optional()
456
+ .describe("Target VM ID or name to unmount ISO from (e.g. '105')."),
457
+ vmIdOrName: z
458
+ .union([z.string(), z.number()])
459
+ .optional()
460
+ .describe("Alias for target."),
461
+ drive: z
462
+ .string()
463
+ .optional()
464
+ .default("ide2")
465
+ .describe("CD-ROM drive identifier (default: 'ide2')."),
466
+ host: z
467
+ .enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
468
+ .optional()
469
+ .default("auto")
470
+ .describe("Hypervisor host or 'auto'.")
471
+ };
472
+ export const UnmountIsoInputSchema = z
473
+ .object(UnmountIsoInputShape)
474
+ .refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
475
+ export async function handleUnmountIso(args, context) {
476
+ const parsed = UnmountIsoInputSchema.safeParse(args || {});
477
+ if (!parsed.success) {
478
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for unmount_iso.");
479
+ }
480
+ const target = parsed.data.target || parsed.data.vmIdOrName;
481
+ const { drive, host: targetHost } = parsed.data;
482
+ const drivers = context.hypervisorManager?.getAllDrivers() || [];
483
+ let foundDriver = targetHost && targetHost !== "auto"
484
+ ? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
485
+ : null;
486
+ let vmRecord = null;
487
+ if (foundDriver) {
488
+ vmRecord = await foundDriver.getVm(target);
489
+ }
490
+ else {
491
+ for (const d of drivers) {
492
+ const vm = await d.getVm(target);
493
+ if (vm) {
494
+ foundDriver = d;
495
+ vmRecord = vm;
496
+ break;
497
+ }
498
+ }
499
+ }
500
+ if (!foundDriver || !vmRecord) {
501
+ return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' not found.`);
502
+ }
503
+ if (!foundDriver.unmountIso) {
504
+ return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support unmountIso.`);
505
+ }
506
+ try {
507
+ await foundDriver.unmountIso(vmRecord.id, drive);
508
+ telemetry.emit({
509
+ event: "storage.unmount_iso",
510
+ actor: "ct-lab-mcp",
511
+ status: "success",
512
+ hostId: foundDriver.hostName,
513
+ vmId: vmRecord.id,
514
+ metadata: { drive }
515
+ });
516
+ return {
517
+ content: [{ type: "text", text: `### ⏏️ CD-ROM Drive Ejected\n\nDrive **\`${drive}\`** on VM **${vmRecord.name}** (\`${vmRecord.id}\`) on host \`${foundDriver.hostName}\` was unmounted / ejected.` }],
518
+ structuredContent: { success: true, host: foundDriver.hostName, vmId: vmRecord.id, vmName: vmRecord.name, drive }
519
+ };
520
+ }
521
+ catch (err) {
522
+ const msg = err instanceof Error ? err.message : String(err);
523
+ return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to unmount ISO on VM '${target}': ${msg}`);
524
+ }
525
+ }
526
+ // ============================================================================
527
+ // 7. create_snapshot
528
+ // ============================================================================
529
+ export const CreateSnapshotInputShape = {
530
+ target: z
531
+ .union([z.string(), z.number()])
532
+ .optional()
533
+ .describe("Target VM ID or name to snapshot (e.g. '105' or 'ct-appliance-slot2-thin')."),
534
+ vmIdOrName: z
535
+ .union([z.string(), z.number()])
536
+ .optional()
537
+ .describe("Alias for target."),
538
+ snapshotName: z
539
+ .string()
540
+ .describe("Name for the new snapshot (e.g. 'pre-upgrade', 'clean-base')."),
541
+ description: z
542
+ .string()
543
+ .optional()
544
+ .describe("Optional human-readable description for the snapshot."),
545
+ includeRam: z
546
+ .boolean()
547
+ .optional()
548
+ .default(false)
549
+ .describe("Whether to include RAM / live memory state in the snapshot (default: false)."),
550
+ host: z
551
+ .enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
552
+ .optional()
553
+ .default("auto")
554
+ .describe("Hypervisor host or 'auto'.")
555
+ };
556
+ export const CreateSnapshotInputSchema = z
557
+ .object(CreateSnapshotInputShape)
558
+ .refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
559
+ export async function handleCreateSnapshot(args, context) {
560
+ const parsed = CreateSnapshotInputSchema.safeParse(args || {});
561
+ if (!parsed.success) {
562
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for create_snapshot.");
563
+ }
564
+ const target = parsed.data.target || parsed.data.vmIdOrName;
565
+ const { snapshotName, description, includeRam, host: targetHost } = parsed.data;
566
+ const drivers = context.hypervisorManager?.getAllDrivers() || [];
567
+ let foundDriver = targetHost && targetHost !== "auto"
568
+ ? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
569
+ : null;
570
+ let vmRecord = null;
571
+ if (foundDriver) {
572
+ vmRecord = await foundDriver.getVm(target);
573
+ }
574
+ else {
575
+ for (const d of drivers) {
576
+ const vm = await d.getVm(target);
577
+ if (vm) {
578
+ foundDriver = d;
579
+ vmRecord = vm;
580
+ break;
581
+ }
582
+ }
583
+ }
584
+ if (!foundDriver || !vmRecord) {
585
+ return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' not found.`);
586
+ }
587
+ if (!foundDriver.createSnapshot) {
588
+ return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support createSnapshot.`);
589
+ }
590
+ try {
591
+ await foundDriver.createSnapshot(vmRecord.id, snapshotName, description, includeRam);
592
+ telemetry.emit({
593
+ event: "storage.create_snapshot",
594
+ actor: "ct-lab-mcp",
595
+ status: "success",
596
+ hostId: foundDriver.hostName,
597
+ vmId: vmRecord.id,
598
+ metadata: { snapshotName, includeRam }
599
+ });
600
+ return {
601
+ content: [{ type: "text", text: `### 📸 Snapshot Created Successfully\n\nSnapshot **\`${snapshotName}\`** was created for VM **${vmRecord.name}** (\`${vmRecord.id}\`) on host \`${foundDriver.hostName}\`.\n*RAM State:* ${includeRam ? "Included" : "Disk Only"}\n*Description:* ${description || "None"}` }],
602
+ structuredContent: { success: true, host: foundDriver.hostName, vmId: vmRecord.id, vmName: vmRecord.name, snapshotName, includeRam, description }
603
+ };
604
+ }
605
+ catch (err) {
606
+ const msg = err instanceof Error ? err.message : String(err);
607
+ return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to create snapshot '${snapshotName}' on VM '${target}': ${msg}`);
608
+ }
609
+ }
610
+ // ============================================================================
611
+ // 8. rollback_snapshot
612
+ // ============================================================================
613
+ export const RollbackSnapshotInputShape = {
614
+ target: z
615
+ .union([z.string(), z.number()])
616
+ .optional()
617
+ .describe("Target VM ID or name to rollback (e.g. '105')."),
618
+ vmIdOrName: z
619
+ .union([z.string(), z.number()])
620
+ .optional()
621
+ .describe("Alias for target."),
622
+ snapshotName: z
623
+ .string()
624
+ .describe("Name of the snapshot to rollback / revert to (e.g. 'clean-base')."),
625
+ host: z
626
+ .enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
627
+ .optional()
628
+ .default("auto")
629
+ .describe("Hypervisor host or 'auto'.")
630
+ };
631
+ export const RollbackSnapshotInputSchema = z
632
+ .object(RollbackSnapshotInputShape)
633
+ .refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
634
+ export async function handleRollbackSnapshot(args, context) {
635
+ const parsed = RollbackSnapshotInputSchema.safeParse(args || {});
636
+ if (!parsed.success) {
637
+ return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for rollback_snapshot.");
638
+ }
639
+ const target = parsed.data.target || parsed.data.vmIdOrName;
640
+ const { snapshotName, host: targetHost } = parsed.data;
641
+ const drivers = context.hypervisorManager?.getAllDrivers() || [];
642
+ let foundDriver = targetHost && targetHost !== "auto"
643
+ ? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
644
+ : null;
645
+ let vmRecord = null;
646
+ if (foundDriver) {
647
+ vmRecord = await foundDriver.getVm(target);
648
+ }
649
+ else {
650
+ for (const d of drivers) {
651
+ const vm = await d.getVm(target);
652
+ if (vm) {
653
+ foundDriver = d;
654
+ vmRecord = vm;
655
+ break;
656
+ }
657
+ }
658
+ }
659
+ if (!foundDriver || !vmRecord) {
660
+ return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' not found.`);
661
+ }
662
+ if (!foundDriver.rollbackSnapshot) {
663
+ return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support rollbackSnapshot.`);
664
+ }
665
+ try {
666
+ await foundDriver.rollbackSnapshot(vmRecord.id, snapshotName);
667
+ telemetry.emit({
668
+ event: "storage.rollback_snapshot",
669
+ actor: "ct-lab-mcp",
670
+ status: "success",
671
+ hostId: foundDriver.hostName,
672
+ vmId: vmRecord.id,
673
+ metadata: { snapshotName }
674
+ });
675
+ return {
676
+ content: [{ type: "text", text: `### 🔄 Snapshot Rollback Completed\n\nVM **${vmRecord.name}** (\`${vmRecord.id}\`) on host \`${foundDriver.hostName}\` was reverted to snapshot **\`${snapshotName}\`**.` }],
677
+ structuredContent: { success: true, host: foundDriver.hostName, vmId: vmRecord.id, vmName: vmRecord.name, snapshotName }
678
+ };
679
+ }
680
+ catch (err) {
681
+ const msg = err instanceof Error ? err.message : String(err);
682
+ return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to rollback snapshot '${snapshotName}' on VM '${target}': ${msg}`);
683
+ }
684
+ }
685
+ //# sourceMappingURL=storage-tools.js.map