kubectl-mcp-server 1.15.0__py3-none-any.whl → 1.17.0__py3-none-any.whl

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 (45) hide show
  1. {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/METADATA +34 -13
  2. kubectl_mcp_server-1.17.0.dist-info/RECORD +75 -0
  3. kubectl_mcp_tool/__init__.py +1 -1
  4. kubectl_mcp_tool/cli/cli.py +83 -9
  5. kubectl_mcp_tool/cli/output.py +14 -0
  6. kubectl_mcp_tool/config/__init__.py +46 -0
  7. kubectl_mcp_tool/config/loader.py +386 -0
  8. kubectl_mcp_tool/config/schema.py +184 -0
  9. kubectl_mcp_tool/crd_detector.py +247 -0
  10. kubectl_mcp_tool/k8s_config.py +19 -0
  11. kubectl_mcp_tool/mcp_server.py +246 -8
  12. kubectl_mcp_tool/observability/__init__.py +59 -0
  13. kubectl_mcp_tool/observability/metrics.py +223 -0
  14. kubectl_mcp_tool/observability/stats.py +255 -0
  15. kubectl_mcp_tool/observability/tracing.py +335 -0
  16. kubectl_mcp_tool/prompts/__init__.py +43 -0
  17. kubectl_mcp_tool/prompts/builtin.py +695 -0
  18. kubectl_mcp_tool/prompts/custom.py +298 -0
  19. kubectl_mcp_tool/prompts/prompts.py +180 -4
  20. kubectl_mcp_tool/safety.py +155 -0
  21. kubectl_mcp_tool/tools/__init__.py +20 -0
  22. kubectl_mcp_tool/tools/backup.py +881 -0
  23. kubectl_mcp_tool/tools/capi.py +727 -0
  24. kubectl_mcp_tool/tools/certs.py +709 -0
  25. kubectl_mcp_tool/tools/cilium.py +582 -0
  26. kubectl_mcp_tool/tools/cluster.py +384 -0
  27. kubectl_mcp_tool/tools/gitops.py +552 -0
  28. kubectl_mcp_tool/tools/keda.py +464 -0
  29. kubectl_mcp_tool/tools/kiali.py +652 -0
  30. kubectl_mcp_tool/tools/kubevirt.py +803 -0
  31. kubectl_mcp_tool/tools/policy.py +554 -0
  32. kubectl_mcp_tool/tools/rollouts.py +790 -0
  33. tests/test_browser.py +2 -2
  34. tests/test_config.py +386 -0
  35. tests/test_ecosystem.py +331 -0
  36. tests/test_mcp_integration.py +251 -0
  37. tests/test_observability.py +521 -0
  38. tests/test_prompts.py +716 -0
  39. tests/test_safety.py +218 -0
  40. tests/test_tools.py +70 -8
  41. kubectl_mcp_server-1.15.0.dist-info/RECORD +0 -49
  42. {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/WHEEL +0 -0
  43. {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt +0 -0
  44. {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE +0 -0
  45. {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,803 @@
1
+ """KubeVirt VM lifecycle toolset for kubectl-mcp-server.
2
+
3
+ Provides tools for managing virtual machines on Kubernetes via KubeVirt.
4
+ """
5
+
6
+ import subprocess
7
+ import json
8
+ from typing import Dict, Any, List
9
+
10
+ try:
11
+ from fastmcp import FastMCP
12
+ from fastmcp.tools import ToolAnnotations
13
+ except ImportError:
14
+ from mcp.server.fastmcp import FastMCP
15
+ from mcp.types import ToolAnnotations
16
+
17
+ from ..k8s_config import _get_kubectl_context_args
18
+ from ..crd_detector import crd_exists
19
+
20
+
21
+ # KubeVirt CRDs
22
+ VM_CRD = "virtualmachines.kubevirt.io"
23
+ VMI_CRD = "virtualmachineinstances.kubevirt.io"
24
+ VMIPRESET_CRD = "virtualmachineinstancepresets.kubevirt.io"
25
+ VMIRS_CRD = "virtualmachineinstancereplicasets.kubevirt.io"
26
+ VMPOOL_CRD = "virtualmachinepools.pool.kubevirt.io"
27
+ DATASOURCE_CRD = "datasources.cdi.kubevirt.io"
28
+ DATAVOLUME_CRD = "datavolumes.cdi.kubevirt.io"
29
+ VMCLONE_CRD = "virtualmachineclones.clone.kubevirt.io"
30
+ INSTANCETYPE_CRD = "virtualmachineinstancetypes.instancetype.kubevirt.io"
31
+ CLUSTERINSTANCETYPE_CRD = "virtualmachineclusterinstancetypes.instancetype.kubevirt.io"
32
+ PREFERENCE_CRD = "virtualmachinepreferences.instancetype.kubevirt.io"
33
+
34
+
35
+ def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
36
+ """Run kubectl command and return result."""
37
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
38
+ try:
39
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
40
+ if result.returncode == 0:
41
+ return {"success": True, "output": result.stdout}
42
+ return {"success": False, "error": result.stderr}
43
+ except subprocess.TimeoutExpired:
44
+ return {"success": False, "error": "Command timed out"}
45
+ except Exception as e:
46
+ return {"success": False, "error": str(e)}
47
+
48
+
49
+ def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
50
+ """Get Kubernetes resources of a specific kind."""
51
+ args = ["get", kind, "-o", "json"]
52
+ if namespace:
53
+ args.extend(["-n", namespace])
54
+ else:
55
+ args.append("-A")
56
+ if label_selector:
57
+ args.extend(["-l", label_selector])
58
+
59
+ result = _run_kubectl(args, context)
60
+ if result["success"]:
61
+ try:
62
+ data = json.loads(result["output"])
63
+ return data.get("items", [])
64
+ except json.JSONDecodeError:
65
+ return []
66
+ return []
67
+
68
+
69
+ def _virtctl_available() -> bool:
70
+ """Check if virtctl CLI is available."""
71
+ try:
72
+ result = subprocess.run(["virtctl", "version", "--client"],
73
+ capture_output=True, timeout=5)
74
+ return result.returncode == 0
75
+ except Exception:
76
+ return False
77
+
78
+
79
+ def _run_virtctl(args: List[str], context: str = "") -> Dict[str, Any]:
80
+ """Run virtctl command if available."""
81
+ if not _virtctl_available():
82
+ return {"success": False, "error": "virtctl CLI not available"}
83
+
84
+ cmd = ["virtctl"] + args
85
+ if context:
86
+ cmd.extend(["--context", context])
87
+
88
+ try:
89
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
90
+ if result.returncode == 0:
91
+ return {"success": True, "output": result.stdout}
92
+ return {"success": False, "error": result.stderr}
93
+ except subprocess.TimeoutExpired:
94
+ return {"success": False, "error": "Command timed out"}
95
+ except Exception as e:
96
+ return {"success": False, "error": str(e)}
97
+
98
+
99
+ def kubevirt_vms_list(
100
+ namespace: str = "",
101
+ context: str = "",
102
+ label_selector: str = ""
103
+ ) -> Dict[str, Any]:
104
+ """List KubeVirt VirtualMachines.
105
+
106
+ Args:
107
+ namespace: Filter by namespace (empty for all namespaces)
108
+ context: Kubernetes context to use (optional)
109
+ label_selector: Label selector to filter VMs
110
+
111
+ Returns:
112
+ List of VirtualMachines with their status
113
+ """
114
+ if not crd_exists(VM_CRD, context):
115
+ return {
116
+ "success": False,
117
+ "error": "KubeVirt is not installed (virtualmachines.kubevirt.io CRD not found)"
118
+ }
119
+
120
+ vms = []
121
+ for item in _get_resources("virtualmachines.kubevirt.io", namespace, context, label_selector):
122
+ status = item.get("status", {})
123
+ spec = item.get("spec", {})
124
+ conditions = status.get("conditions", [])
125
+
126
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
127
+ paused_cond = next((c for c in conditions if c.get("type") == "Paused"), {})
128
+
129
+ # Get resource info from template
130
+ template = spec.get("template", {}).get("spec", {})
131
+ domain = template.get("domain", {})
132
+ resources = domain.get("resources", {})
133
+ cpu = domain.get("cpu", {})
134
+
135
+ vms.append({
136
+ "name": item["metadata"]["name"],
137
+ "namespace": item["metadata"]["namespace"],
138
+ "running": spec.get("running", False),
139
+ "run_strategy": spec.get("runStrategy"),
140
+ "ready": ready_cond.get("status") == "True",
141
+ "paused": paused_cond.get("status") == "True",
142
+ "print_status": status.get("printableStatus", "Unknown"),
143
+ "created": status.get("created", False),
144
+ "cpu_cores": cpu.get("cores", 1),
145
+ "cpu_sockets": cpu.get("sockets", 1),
146
+ "cpu_threads": cpu.get("threads", 1),
147
+ "memory": resources.get("requests", {}).get("memory", ""),
148
+ "volume_count": len(template.get("volumes", [])),
149
+ "network_count": len(template.get("networks", [])),
150
+ "state_change_requests": status.get("stateChangeRequests", []),
151
+ })
152
+
153
+ # Summary
154
+ running = sum(1 for v in vms if v["running"])
155
+ ready = sum(1 for v in vms if v["ready"])
156
+
157
+ return {
158
+ "context": context or "current",
159
+ "total": len(vms),
160
+ "running": running,
161
+ "ready": ready,
162
+ "vms": vms,
163
+ }
164
+
165
+
166
+ def kubevirt_vm_get(
167
+ name: str,
168
+ namespace: str,
169
+ context: str = ""
170
+ ) -> Dict[str, Any]:
171
+ """Get detailed information about a VirtualMachine.
172
+
173
+ Args:
174
+ name: Name of the VM
175
+ namespace: Namespace of the VM
176
+ context: Kubernetes context to use (optional)
177
+
178
+ Returns:
179
+ Detailed VM information
180
+ """
181
+ if not crd_exists(VM_CRD, context):
182
+ return {"success": False, "error": "KubeVirt is not installed"}
183
+
184
+ args = ["get", "virtualmachines.kubevirt.io", name, "-n", namespace, "-o", "json"]
185
+ result = _run_kubectl(args, context)
186
+
187
+ if result["success"]:
188
+ try:
189
+ data = json.loads(result["output"])
190
+ return {
191
+ "success": True,
192
+ "context": context or "current",
193
+ "vm": data,
194
+ }
195
+ except json.JSONDecodeError:
196
+ return {"success": False, "error": "Failed to parse response"}
197
+
198
+ return {"success": False, "error": result.get("error", "Unknown error")}
199
+
200
+
201
+ def kubevirt_vmis_list(
202
+ namespace: str = "",
203
+ context: str = "",
204
+ label_selector: str = ""
205
+ ) -> Dict[str, Any]:
206
+ """List KubeVirt VirtualMachineInstances (running VMs).
207
+
208
+ Args:
209
+ namespace: Filter by namespace (empty for all namespaces)
210
+ context: Kubernetes context to use (optional)
211
+ label_selector: Label selector to filter VMIs
212
+
213
+ Returns:
214
+ List of running VirtualMachineInstances
215
+ """
216
+ if not crd_exists(VMI_CRD, context):
217
+ return {
218
+ "success": False,
219
+ "error": "KubeVirt is not installed"
220
+ }
221
+
222
+ vmis = []
223
+ for item in _get_resources("virtualmachineinstances.kubevirt.io", namespace, context, label_selector):
224
+ status = item.get("status", {})
225
+ spec = item.get("spec", {})
226
+ conditions = status.get("conditions", [])
227
+
228
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
229
+ live_migratable = next((c for c in conditions if c.get("type") == "LiveMigratable"), {})
230
+
231
+ domain = spec.get("domain", {})
232
+ resources = domain.get("resources", {})
233
+
234
+ # Get guest info if available
235
+ guest_info = status.get("guestOSInfo", {})
236
+
237
+ vmis.append({
238
+ "name": item["metadata"]["name"],
239
+ "namespace": item["metadata"]["namespace"],
240
+ "phase": status.get("phase", "Unknown"),
241
+ "ready": ready_cond.get("status") == "True",
242
+ "live_migratable": live_migratable.get("status") == "True",
243
+ "node": status.get("nodeName", ""),
244
+ "ip_addresses": [iface.get("ipAddress") for iface in status.get("interfaces", []) if iface.get("ipAddress")],
245
+ "memory": resources.get("requests", {}).get("memory", ""),
246
+ "guest_os": guest_info.get("name", ""),
247
+ "guest_os_version": guest_info.get("version", ""),
248
+ "migration_state": status.get("migrationState"),
249
+ "active_pods": status.get("activePods", {}),
250
+ })
251
+
252
+ # Summary
253
+ running = sum(1 for v in vmis if v["phase"] == "Running")
254
+ scheduled = sum(1 for v in vmis if v["phase"] == "Scheduled")
255
+
256
+ return {
257
+ "context": context or "current",
258
+ "total": len(vmis),
259
+ "running": running,
260
+ "scheduled": scheduled,
261
+ "vmis": vmis,
262
+ }
263
+
264
+
265
+ def kubevirt_vm_start(
266
+ name: str,
267
+ namespace: str,
268
+ context: str = ""
269
+ ) -> Dict[str, Any]:
270
+ """Start a VirtualMachine.
271
+
272
+ Args:
273
+ name: Name of the VM
274
+ namespace: Namespace of the VM
275
+ context: Kubernetes context to use (optional)
276
+
277
+ Returns:
278
+ Start result
279
+ """
280
+ if not crd_exists(VM_CRD, context):
281
+ return {"success": False, "error": "KubeVirt is not installed"}
282
+
283
+ # Try virtctl first
284
+ if _virtctl_available():
285
+ result = _run_virtctl(["start", name, "-n", namespace], context)
286
+ if result["success"]:
287
+ return {
288
+ "success": True,
289
+ "context": context or "current",
290
+ "message": f"Started VM {name}",
291
+ "output": result.get("output", ""),
292
+ }
293
+ # Don't return error, fall through to patch
294
+
295
+ # Fallback to patching
296
+ patch = {"spec": {"running": True}}
297
+ args = [
298
+ "patch", "virtualmachines.kubevirt.io", name,
299
+ "-n", namespace,
300
+ "--type=merge",
301
+ "-p", json.dumps(patch)
302
+ ]
303
+ result = _run_kubectl(args, context)
304
+
305
+ if result["success"]:
306
+ return {
307
+ "success": True,
308
+ "context": context or "current",
309
+ "message": f"Started VM {name}",
310
+ }
311
+
312
+ return {"success": False, "error": result.get("error", "Failed to start VM")}
313
+
314
+
315
+ def kubevirt_vm_stop(
316
+ name: str,
317
+ namespace: str,
318
+ force: bool = False,
319
+ context: str = ""
320
+ ) -> Dict[str, Any]:
321
+ """Stop a VirtualMachine.
322
+
323
+ Args:
324
+ name: Name of the VM
325
+ namespace: Namespace of the VM
326
+ force: Force stop (like pulling the power)
327
+ context: Kubernetes context to use (optional)
328
+
329
+ Returns:
330
+ Stop result
331
+ """
332
+ if not crd_exists(VM_CRD, context):
333
+ return {"success": False, "error": "KubeVirt is not installed"}
334
+
335
+ # Try virtctl first
336
+ if _virtctl_available():
337
+ cmd = ["stop", name, "-n", namespace]
338
+ if force:
339
+ cmd.append("--force")
340
+ result = _run_virtctl(cmd, context)
341
+ if result["success"]:
342
+ return {
343
+ "success": True,
344
+ "context": context or "current",
345
+ "message": f"Stopped VM {name}" + (" (forced)" if force else ""),
346
+ "output": result.get("output", ""),
347
+ }
348
+
349
+ # Fallback to patching
350
+ patch = {"spec": {"running": False}}
351
+ args = [
352
+ "patch", "virtualmachines.kubevirt.io", name,
353
+ "-n", namespace,
354
+ "--type=merge",
355
+ "-p", json.dumps(patch)
356
+ ]
357
+ result = _run_kubectl(args, context)
358
+
359
+ if result["success"]:
360
+ return {
361
+ "success": True,
362
+ "context": context or "current",
363
+ "message": f"Stopped VM {name}",
364
+ }
365
+
366
+ return {"success": False, "error": result.get("error", "Failed to stop VM")}
367
+
368
+
369
+ def kubevirt_vm_restart(
370
+ name: str,
371
+ namespace: str,
372
+ context: str = ""
373
+ ) -> Dict[str, Any]:
374
+ """Restart a VirtualMachine.
375
+
376
+ Args:
377
+ name: Name of the VM
378
+ namespace: Namespace of the VM
379
+ context: Kubernetes context to use (optional)
380
+
381
+ Returns:
382
+ Restart result
383
+ """
384
+ if not crd_exists(VM_CRD, context):
385
+ return {"success": False, "error": "KubeVirt is not installed"}
386
+
387
+ if _virtctl_available():
388
+ result = _run_virtctl(["restart", name, "-n", namespace], context)
389
+ if result["success"]:
390
+ return {
391
+ "success": True,
392
+ "context": context or "current",
393
+ "message": f"Restarted VM {name}",
394
+ "output": result.get("output", ""),
395
+ }
396
+ return {"success": False, "error": result.get("error", "Failed to restart")}
397
+
398
+ return {"success": False, "error": "virtctl CLI required for restart operation"}
399
+
400
+
401
+ def kubevirt_vm_pause(
402
+ name: str,
403
+ namespace: str,
404
+ context: str = ""
405
+ ) -> Dict[str, Any]:
406
+ """Pause a VirtualMachine.
407
+
408
+ Args:
409
+ name: Name of the VM
410
+ namespace: Namespace of the VM
411
+ context: Kubernetes context to use (optional)
412
+
413
+ Returns:
414
+ Pause result
415
+ """
416
+ if not crd_exists(VM_CRD, context):
417
+ return {"success": False, "error": "KubeVirt is not installed"}
418
+
419
+ if _virtctl_available():
420
+ result = _run_virtctl(["pause", "vm", name, "-n", namespace], context)
421
+ if result["success"]:
422
+ return {
423
+ "success": True,
424
+ "context": context or "current",
425
+ "message": f"Paused VM {name}",
426
+ "output": result.get("output", ""),
427
+ }
428
+ return {"success": False, "error": result.get("error", "Failed to pause")}
429
+
430
+ return {"success": False, "error": "virtctl CLI required for pause operation"}
431
+
432
+
433
+ def kubevirt_vm_unpause(
434
+ name: str,
435
+ namespace: str,
436
+ context: str = ""
437
+ ) -> Dict[str, Any]:
438
+ """Unpause a VirtualMachine.
439
+
440
+ Args:
441
+ name: Name of the VM
442
+ namespace: Namespace of the VM
443
+ context: Kubernetes context to use (optional)
444
+
445
+ Returns:
446
+ Unpause result
447
+ """
448
+ if not crd_exists(VM_CRD, context):
449
+ return {"success": False, "error": "KubeVirt is not installed"}
450
+
451
+ if _virtctl_available():
452
+ result = _run_virtctl(["unpause", "vm", name, "-n", namespace], context)
453
+ if result["success"]:
454
+ return {
455
+ "success": True,
456
+ "context": context or "current",
457
+ "message": f"Unpaused VM {name}",
458
+ "output": result.get("output", ""),
459
+ }
460
+ return {"success": False, "error": result.get("error", "Failed to unpause")}
461
+
462
+ return {"success": False, "error": "virtctl CLI required for unpause operation"}
463
+
464
+
465
+ def kubevirt_vm_migrate(
466
+ name: str,
467
+ namespace: str,
468
+ context: str = ""
469
+ ) -> Dict[str, Any]:
470
+ """Trigger live migration of a VirtualMachineInstance.
471
+
472
+ Args:
473
+ name: Name of the VM
474
+ namespace: Namespace of the VM
475
+ context: Kubernetes context to use (optional)
476
+
477
+ Returns:
478
+ Migration result
479
+ """
480
+ if not crd_exists(VM_CRD, context):
481
+ return {"success": False, "error": "KubeVirt is not installed"}
482
+
483
+ if _virtctl_available():
484
+ result = _run_virtctl(["migrate", name, "-n", namespace], context)
485
+ if result["success"]:
486
+ return {
487
+ "success": True,
488
+ "context": context or "current",
489
+ "message": f"Triggered migration for VM {name}",
490
+ "output": result.get("output", ""),
491
+ }
492
+ return {"success": False, "error": result.get("error", "Failed to migrate")}
493
+
494
+ return {"success": False, "error": "virtctl CLI required for migration operation"}
495
+
496
+
497
+ def kubevirt_datasources_list(
498
+ namespace: str = "",
499
+ context: str = "",
500
+ label_selector: str = ""
501
+ ) -> Dict[str, Any]:
502
+ """List KubeVirt DataSources (for disk images).
503
+
504
+ Args:
505
+ namespace: Filter by namespace (empty for all namespaces)
506
+ context: Kubernetes context to use (optional)
507
+ label_selector: Label selector to filter
508
+
509
+ Returns:
510
+ List of DataSources
511
+ """
512
+ if not crd_exists(DATASOURCE_CRD, context):
513
+ return {
514
+ "success": False,
515
+ "error": "CDI DataSources CRD not found"
516
+ }
517
+
518
+ datasources = []
519
+ for item in _get_resources("datasources.cdi.kubevirt.io", namespace, context, label_selector):
520
+ spec = item.get("spec", {})
521
+ status = item.get("status", {})
522
+ conditions = status.get("conditions", [])
523
+
524
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
525
+ source = spec.get("source", {})
526
+
527
+ datasources.append({
528
+ "name": item["metadata"]["name"],
529
+ "namespace": item["metadata"]["namespace"],
530
+ "ready": ready_cond.get("status") == "True",
531
+ "source_pvc": source.get("pvc", {}),
532
+ "source_snapshot": source.get("snapshot", {}),
533
+ })
534
+
535
+ return {
536
+ "context": context or "current",
537
+ "total": len(datasources),
538
+ "datasources": datasources,
539
+ }
540
+
541
+
542
+ def kubevirt_instancetypes_list(
543
+ namespace: str = "",
544
+ context: str = "",
545
+ include_cluster: bool = True
546
+ ) -> Dict[str, Any]:
547
+ """List KubeVirt InstanceTypes (VM sizing templates).
548
+
549
+ Args:
550
+ namespace: Filter by namespace (empty for all)
551
+ context: Kubernetes context to use (optional)
552
+ include_cluster: Include cluster-wide instance types
553
+
554
+ Returns:
555
+ List of InstanceTypes
556
+ """
557
+ instancetypes = []
558
+
559
+ if crd_exists(INSTANCETYPE_CRD, context):
560
+ for item in _get_resources("virtualmachineinstancetypes.instancetype.kubevirt.io", namespace, context):
561
+ spec = item.get("spec", {})
562
+ cpu = spec.get("cpu", {})
563
+ memory = spec.get("memory", {})
564
+
565
+ instancetypes.append({
566
+ "name": item["metadata"]["name"],
567
+ "namespace": item["metadata"]["namespace"],
568
+ "kind": "VirtualMachineInstancetype",
569
+ "cpu_guest": cpu.get("guest", 1),
570
+ "cpu_model": cpu.get("model"),
571
+ "memory_guest": memory.get("guest", ""),
572
+ "memory_hugepages": memory.get("hugepages", {}),
573
+ })
574
+
575
+ if include_cluster and crd_exists(CLUSTERINSTANCETYPE_CRD, context):
576
+ for item in _get_resources("virtualmachineclusterinstancetypes.instancetype.kubevirt.io", "", context):
577
+ spec = item.get("spec", {})
578
+ cpu = spec.get("cpu", {})
579
+ memory = spec.get("memory", {})
580
+
581
+ instancetypes.append({
582
+ "name": item["metadata"]["name"],
583
+ "namespace": "",
584
+ "kind": "VirtualMachineClusterInstancetype",
585
+ "cpu_guest": cpu.get("guest", 1),
586
+ "cpu_model": cpu.get("model"),
587
+ "memory_guest": memory.get("guest", ""),
588
+ "memory_hugepages": memory.get("hugepages", {}),
589
+ })
590
+
591
+ return {
592
+ "context": context or "current",
593
+ "total": len(instancetypes),
594
+ "instancetypes": instancetypes,
595
+ }
596
+
597
+
598
+ def kubevirt_datavolumes_list(
599
+ namespace: str = "",
600
+ context: str = "",
601
+ label_selector: str = ""
602
+ ) -> Dict[str, Any]:
603
+ """List KubeVirt DataVolumes (disk images).
604
+
605
+ Args:
606
+ namespace: Filter by namespace (empty for all namespaces)
607
+ context: Kubernetes context to use (optional)
608
+ label_selector: Label selector to filter
609
+
610
+ Returns:
611
+ List of DataVolumes
612
+ """
613
+ if not crd_exists(DATAVOLUME_CRD, context):
614
+ return {
615
+ "success": False,
616
+ "error": "CDI DataVolumes CRD not found"
617
+ }
618
+
619
+ datavolumes = []
620
+ for item in _get_resources("datavolumes.cdi.kubevirt.io", namespace, context, label_selector):
621
+ spec = item.get("spec", {})
622
+ status = item.get("status", {})
623
+ conditions = status.get("conditions", [])
624
+
625
+ ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
626
+ bound_cond = next((c for c in conditions if c.get("type") == "Bound"), {})
627
+
628
+ source = spec.get("source", {})
629
+ source_type = list(source.keys())[0] if source else "unknown"
630
+
631
+ datavolumes.append({
632
+ "name": item["metadata"]["name"],
633
+ "namespace": item["metadata"]["namespace"],
634
+ "phase": status.get("phase", "Unknown"),
635
+ "ready": ready_cond.get("status") == "True",
636
+ "bound": bound_cond.get("status") == "True",
637
+ "progress": status.get("progress", "N/A"),
638
+ "source_type": source_type,
639
+ "storage_size": spec.get("pvc", {}).get("resources", {}).get("requests", {}).get("storage", ""),
640
+ "storage_class": spec.get("pvc", {}).get("storageClassName", ""),
641
+ })
642
+
643
+ return {
644
+ "context": context or "current",
645
+ "total": len(datavolumes),
646
+ "datavolumes": datavolumes,
647
+ }
648
+
649
+
650
+ def kubevirt_detect(context: str = "") -> Dict[str, Any]:
651
+ """Detect if KubeVirt is installed and its components.
652
+
653
+ Args:
654
+ context: Kubernetes context to use (optional)
655
+
656
+ Returns:
657
+ Detection results for KubeVirt
658
+ """
659
+ return {
660
+ "context": context or "current",
661
+ "installed": crd_exists(VM_CRD, context),
662
+ "cli_available": _virtctl_available(),
663
+ "crds": {
664
+ "virtualmachines": crd_exists(VM_CRD, context),
665
+ "virtualmachineinstances": crd_exists(VMI_CRD, context),
666
+ "virtualmachineinstancepresets": crd_exists(VMIPRESET_CRD, context),
667
+ "virtualmachineinstancereplicasets": crd_exists(VMIRS_CRD, context),
668
+ "datasources": crd_exists(DATASOURCE_CRD, context),
669
+ "datavolumes": crd_exists(DATAVOLUME_CRD, context),
670
+ "instancetypes": crd_exists(INSTANCETYPE_CRD, context),
671
+ "clusterinstancetypes": crd_exists(CLUSTERINSTANCETYPE_CRD, context),
672
+ },
673
+ }
674
+
675
+
676
+ def register_kubevirt_tools(mcp: FastMCP, non_destructive: bool = False):
677
+ """Register KubeVirt tools with the MCP server."""
678
+
679
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
680
+ def kubevirt_vms_list_tool(
681
+ namespace: str = "",
682
+ context: str = "",
683
+ label_selector: str = ""
684
+ ) -> str:
685
+ """List KubeVirt VirtualMachines."""
686
+ return json.dumps(kubevirt_vms_list(namespace, context, label_selector), indent=2)
687
+
688
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
689
+ def kubevirt_vm_get_tool(
690
+ name: str,
691
+ namespace: str,
692
+ context: str = ""
693
+ ) -> str:
694
+ """Get detailed information about a VirtualMachine."""
695
+ return json.dumps(kubevirt_vm_get(name, namespace, context), indent=2)
696
+
697
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
698
+ def kubevirt_vmis_list_tool(
699
+ namespace: str = "",
700
+ context: str = "",
701
+ label_selector: str = ""
702
+ ) -> str:
703
+ """List running VirtualMachineInstances."""
704
+ return json.dumps(kubevirt_vmis_list(namespace, context, label_selector), indent=2)
705
+
706
+ @mcp.tool()
707
+ def kubevirt_vm_start_tool(
708
+ name: str,
709
+ namespace: str,
710
+ context: str = ""
711
+ ) -> str:
712
+ """Start a VirtualMachine."""
713
+ if non_destructive:
714
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
715
+ return json.dumps(kubevirt_vm_start(name, namespace, context), indent=2)
716
+
717
+ @mcp.tool()
718
+ def kubevirt_vm_stop_tool(
719
+ name: str,
720
+ namespace: str,
721
+ force: bool = False,
722
+ context: str = ""
723
+ ) -> str:
724
+ """Stop a VirtualMachine."""
725
+ if non_destructive:
726
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
727
+ return json.dumps(kubevirt_vm_stop(name, namespace, force, context), indent=2)
728
+
729
+ @mcp.tool()
730
+ def kubevirt_vm_restart_tool(
731
+ name: str,
732
+ namespace: str,
733
+ context: str = ""
734
+ ) -> str:
735
+ """Restart a VirtualMachine."""
736
+ if non_destructive:
737
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
738
+ return json.dumps(kubevirt_vm_restart(name, namespace, context), indent=2)
739
+
740
+ @mcp.tool()
741
+ def kubevirt_vm_pause_tool(
742
+ name: str,
743
+ namespace: str,
744
+ context: str = ""
745
+ ) -> str:
746
+ """Pause a VirtualMachine."""
747
+ if non_destructive:
748
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
749
+ return json.dumps(kubevirt_vm_pause(name, namespace, context), indent=2)
750
+
751
+ @mcp.tool()
752
+ def kubevirt_vm_unpause_tool(
753
+ name: str,
754
+ namespace: str,
755
+ context: str = ""
756
+ ) -> str:
757
+ """Unpause a VirtualMachine."""
758
+ if non_destructive:
759
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
760
+ return json.dumps(kubevirt_vm_unpause(name, namespace, context), indent=2)
761
+
762
+ @mcp.tool()
763
+ def kubevirt_vm_migrate_tool(
764
+ name: str,
765
+ namespace: str,
766
+ context: str = ""
767
+ ) -> str:
768
+ """Trigger live migration of a VirtualMachine."""
769
+ if non_destructive:
770
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
771
+ return json.dumps(kubevirt_vm_migrate(name, namespace, context), indent=2)
772
+
773
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
774
+ def kubevirt_datasources_list_tool(
775
+ namespace: str = "",
776
+ context: str = "",
777
+ label_selector: str = ""
778
+ ) -> str:
779
+ """List KubeVirt DataSources."""
780
+ return json.dumps(kubevirt_datasources_list(namespace, context, label_selector), indent=2)
781
+
782
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
783
+ def kubevirt_instancetypes_list_tool(
784
+ namespace: str = "",
785
+ context: str = "",
786
+ include_cluster: bool = True
787
+ ) -> str:
788
+ """List KubeVirt InstanceTypes (VM sizing templates)."""
789
+ return json.dumps(kubevirt_instancetypes_list(namespace, context, include_cluster), indent=2)
790
+
791
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
792
+ def kubevirt_datavolumes_list_tool(
793
+ namespace: str = "",
794
+ context: str = "",
795
+ label_selector: str = ""
796
+ ) -> str:
797
+ """List KubeVirt DataVolumes (disk images)."""
798
+ return json.dumps(kubevirt_datavolumes_list(namespace, context, label_selector), indent=2)
799
+
800
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
801
+ def kubevirt_detect_tool(context: str = "") -> str:
802
+ """Detect if KubeVirt is installed and its components."""
803
+ return json.dumps(kubevirt_detect(context), indent=2)