diff --git a/README.md b/README.md
index f1f968130..1a0b93315 100644
--- a/README.md
+++ b/README.md
@@ -274,6 +274,7 @@ and only needed for the project-specific scenarios noted.
| [Kiali](https://kiali.io) | `kiali` | 16 |
| [Kubernetes](https://kubernetes.io) | - | 32 |
| [KubeVirt](https://kubevirt.io) | `kubevirt`, `tekton` | 19 |
+| [LVMS](https://github.com/openshift/lvm-operator) | `lvms` | 11 |
| [Tekton](https://tekton.dev) | `tekton` | 9 |
@@ -292,6 +293,7 @@ The following sets of tools are available (toolsets marked with ✓ in the Defau
| kcp | Manage kcp workspaces and multi-tenancy features | |
| kiali | Most common tools for managing Kiali, check the [Kiali documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/KIALI.md) for more details. | |
| kubevirt | KubeVirt virtual machine management tools, check the [KubeVirt documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/kubevirt.md) for more details. | |
+| lvms | LVMS (Logical Volume Manager Storage) troubleshooting prompts for diagnosing storage issues | |
| netobserv | Network observability tools backed by the NetObserv console plugin API (flows, metrics, export). Check the [NetObserv documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/NETOBSERV.md) for more details. | |
| tekton | Tekton pipeline management tools for Pipelines, PipelineRuns, Tasks, and TaskRuns. | |
@@ -498,6 +500,19 @@ In case multi-cluster support is enabled (default) and you have access to multip
+lvms
+
+- **lvms-troubleshoot** - Diagnose LVMS storage issues by gathering LVMCluster status, volume group health, and node-level LVM data with interpretation of domain-specific fields
+ - `namespace` (`string`) - The LVMS namespace (default: openshift-lvm-storage)
+ - `node` (`string`) - Specific node to inspect (default: all nodes with LVMS)
+
+- **lvms-capacity** - Check LVMS storage capacity across all nodes including available space per node, PVC usage, and capacity warnings
+ - `namespace` (`string`) - The LVMS namespace (default: openshift-lvm-storage)
+
+
+
+
+
netobserv
- **netobserv_list_flows** - Lists NetObserv network flow records from Loki. Use when investigating traffic between workloads, IPs, ports, or protocols in a namespace or time window.
diff --git a/docs/configuration.md b/docs/configuration.md
index aef5b6165..4ad1d3e6e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -318,6 +318,7 @@ Toolsets group related tools together. Enable only the toolsets you need to redu
| kcp | Manage kcp workspaces and multi-tenancy features | |
| kiali | Most common tools for managing Kiali, check the [Kiali documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/KIALI.md) for more details. | |
| kubevirt | KubeVirt virtual machine management tools, check the [KubeVirt documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/kubevirt.md) for more details. | |
+| lvms | LVMS (Logical Volume Manager Storage) troubleshooting prompts for diagnosing storage issues | |
| netobserv | Network observability tools backed by the NetObserv console plugin API (flows, metrics, export). Check the [NetObserv documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/NETOBSERV.md) for more details. | |
| tekton | Tekton pipeline management tools for Pipelines, PipelineRuns, Tasks, and TaskRuns. | |
diff --git a/evals/claude-code/eval.yaml b/evals/claude-code/eval.yaml
index 8fe931c9c..8a9f4137e 100644
--- a/evals/claude-code/eval.yaml
+++ b/evals/claude-code/eval.yaml
@@ -84,3 +84,14 @@ config:
toolPattern: "netobserv_.*"
minToolCalls: 1
maxToolCalls: 10
+ # LVMS tasks - requires LVMS operator on cluster
+ # LVMS is a prompts-only toolset; uses core tools for K8s queries
+ - glob: ../tasks/lvms/*/*.yaml
+ labelSelector:
+ suite: lvms
+ assertions:
+ toolsUsed:
+ - server: kubernetes
+ toolPattern: ".*"
+ minToolCalls: 1
+ maxToolCalls: 20
diff --git a/evals/openai-agent/eval.yaml b/evals/openai-agent/eval.yaml
index ecc61a16b..ba98db546 100644
--- a/evals/openai-agent/eval.yaml
+++ b/evals/openai-agent/eval.yaml
@@ -82,3 +82,14 @@ config:
toolPattern: "netobserv_.*"
minToolCalls: 1
maxToolCalls: 10
+ # LVMS tasks - requires LVMS operator on cluster
+ # LVMS is a prompts-only toolset; uses core tools for K8s queries
+ - glob: ../tasks/lvms/*/*.yaml
+ labelSelector:
+ suite: lvms
+ assertions:
+ toolsUsed:
+ - server: kubernetes
+ toolPattern: ".*"
+ minToolCalls: 1
+ maxToolCalls: 20
diff --git a/evals/tasks/lvms/README.md b/evals/tasks/lvms/README.md
new file mode 100644
index 000000000..c60471654
--- /dev/null
+++ b/evals/tasks/lvms/README.md
@@ -0,0 +1,57 @@
+# LVMS Task Library
+
+Evaluation tasks for the LVMS (Logical Volume Manager Storage) toolset. These tasks exercise storage troubleshooting workflows on OpenShift clusters using LVMS.
+
+## Prerequisites
+
+- LVMS operator installed on the cluster
+- LVMCluster CR configured (typically in `openshift-lvm-storage` namespace)
+- Available block devices on nodes for volume group creation
+
+## Toolset Design
+
+LVMS is a **prompts-only toolset** — it provides no dedicated tools. This design follows the toolset-design principle that CRUD operations on Kubernetes resources should use the core toolset's generic tools (`resources_list`, `resources_get`, etc.).
+
+### What the LVMS toolset provides:
+
+- **`lvms-troubleshoot` prompt** — Pre-fetches and presents:
+ - LVMCluster status and device class configuration
+ - LVMVolumeGroupNodeStatus per node
+ - vg-manager pod health and logs
+ - **Node-level LVM data** (vgs, lvs, pvs output via debug pods)
+ - Events in the LVMS namespace
+ - Domain knowledge (vg_attr/lv_attr field interpretation)
+
+### What eval tasks use:
+
+| Tool | Source | Purpose |
+|------|--------|---------|
+| `resources_list` / `resources_get` | core | Query LVMS CRDs (LVMCluster, LVMVolumeGroup, etc.) |
+| `nodes_debug_exec` | cluster-diagnostics | Run LVM commands on nodes (vgs, lvs, pvs, lsblk) |
+| `lvms-troubleshoot` prompt | lvms | Guided diagnostics with pre-fetched data |
+
+## Task Categories
+
+| Task | Difficulty | Description |
+|------|------------|-------------|
+| check-lvmcluster-status | Easy | Verify LVMCluster health and device class configuration |
+| check-capacity | Medium | Analyze thin pool utilization and storage capacity |
+| check-thin-pool-utilization | Medium | Check thin pool data% and metadata% thresholds |
+| list-node-block-devices | Easy | List available block devices on nodes |
+| show-volume-groups | Easy | Display volume group status |
+| show-lvm-physical-volumes | Easy | Display physical volume status |
+| diagnose-no-space-left | Hard | Diagnose thin pool exhaustion issues |
+| diagnose-disk-not-used | Hard | Investigate why a disk isn't being used by LVMS |
+| troubleshoot-stuck-pvc | Hard | Diagnose why a PVC is stuck in Pending state |
+| troubleshoot-prompt | Medium | Use the lvms-troubleshoot prompt for guided diagnosis |
+| forced-cleanup | Hard | Perform forced cleanup of stuck LVMS resources |
+
+## Running Tasks
+
+```bash
+# Run all LVMS tasks
+mcpchecker check evals/claude-code/eval.yaml --label-selector suite=lvms
+
+# Run specific difficulty level
+mcpchecker check evals/claude-code/eval.yaml --label-selector suite=lvms,difficulty=hard
+```
diff --git a/evals/tasks/lvms/check-capacity/task.yaml b/evals/tasks/lvms/check-capacity/task.yaml
new file mode 100644
index 000000000..46dd52a1a
--- /dev/null
+++ b/evals/tasks/lvms/check-capacity/task.yaml
@@ -0,0 +1,68 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "check-lvms-capacity"
+ difficulty: medium
+ description: "Check LVMS storage capacity and thin pool utilization across nodes"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ # Verify LVMS is installed
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ # Check if an LVMCluster exists and is ready
+ CLUSTER=$(kubectl get lvmclusters -n "$NS" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
+ if [ -z "$CLUSTER" ]; then
+ echo "No LVMCluster found - LVMS must be configured"
+ exit 1
+ fi
+
+ STATE=$(kubectl get lvmcluster "$CLUSTER" -n "$NS" -o jsonpath='{.status.state}' 2>/dev/null)
+ if [ "$STATE" != "Ready" ]; then
+ echo "LVMCluster '$CLUSTER' is not ready (state: $STATE)"
+ exit 1
+ fi
+
+ echo "LVMS is configured with LVMCluster: $CLUSTER"
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ echo "=== Verification: Checking agent capacity analysis ==="
+
+ # List node statuses
+ kubectl get lvmvolumegroupnodestatuses -n "$NS" -o custom-columns='NODE:.metadata.name,STATUS:.status' 2>/dev/null || true
+
+ echo ""
+ echo "=== Capacity Eval Complete ==="
+ echo "The agent should have:"
+ echo " 1. Queried LVMS volume group status across nodes"
+ echo " 2. Reported per-node storage capacity (total, allocatable)"
+ echo " 3. Identified any nodes with low available space"
+ echo " 4. Provided capacity planning recommendations if applicable"
+ echo ""
+
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ # No cleanup needed - read-only operation
+ exit 0
+ prompt:
+ inline: |-
+ We're planning to deploy several new applications that will need persistent storage.
+ How much LVMS storage capacity do we have available across the cluster, and are there
+ any nodes running low on space?
diff --git a/evals/tasks/lvms/check-lvmcluster-status/task.yaml b/evals/tasks/lvms/check-lvmcluster-status/task.yaml
new file mode 100644
index 000000000..9004f29b6
--- /dev/null
+++ b/evals/tasks/lvms/check-lvmcluster-status/task.yaml
@@ -0,0 +1,81 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "check-lvms-cluster-status"
+ difficulty: easy
+ description: "Check the status of the LVMCluster and verify it is healthy"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ # Verify LVMS is installed
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ # Check if an LVMCluster exists
+ if [ -z "$(kubectl get lvmclusters -n "$NS" -o name 2>/dev/null)" ]; then
+ echo "No LVMCluster found in $NS - creating test cluster"
+ cat > /tmp/lvmcluster.yaml << 'EOF'
+ apiVersion: lvm.topolvm.io/v1alpha1
+ kind: LVMCluster
+ metadata:
+ name: eval-lvmcluster
+ namespace: openshift-lvm-storage
+ spec:
+ storage:
+ deviceClasses:
+ - name: vg1
+ default: true
+ fstype: xfs
+ thinPoolConfig:
+ name: thin-pool-1
+ sizePercent: 90
+ overprovisionRatio: 10
+ EOF
+ kubectl apply -f /tmp/lvmcluster.yaml
+ rm -f /tmp/lvmcluster.yaml
+ echo "Created eval-lvmcluster - waiting for reconciliation"
+ sleep 30
+ fi
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ echo "=== Verification: Checking agent findings ==="
+
+ # The agent should have used list_lvmclusters or describe_lvmcluster
+ # and reported the cluster status
+
+ CLUSTER=$(kubectl get lvmclusters -n "$NS" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
+ if [ -n "$CLUSTER" ]; then
+ STATE=$(kubectl get lvmcluster "$CLUSTER" -n "$NS" -o jsonpath='{.status.state}' 2>/dev/null || echo "Unknown")
+ READY=$(kubectl get lvmcluster "$CLUSTER" -n "$NS" -o jsonpath='{.status.ready}' 2>/dev/null || echo "Unknown")
+ echo "LVMCluster '$CLUSTER' state: $STATE, ready: $READY"
+ fi
+
+ echo ""
+ echo "=== Eval Complete ==="
+ echo "The agent should have:"
+ echo " 1. Queried the LVMCluster resource"
+ echo " 2. Reported the cluster name, state, and ready status"
+ echo " 3. Listed any device classes configured"
+
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ # Only clean up the eval cluster if we created it
+ kubectl delete lvmcluster eval-lvmcluster -n openshift-lvm-storage --ignore-not-found
+ prompt:
+ inline: Is LVMS storage configured and working properly on this cluster? What storage options are available?
diff --git a/evals/tasks/lvms/check-thin-pool-utilization/task.yaml b/evals/tasks/lvms/check-thin-pool-utilization/task.yaml
new file mode 100644
index 000000000..f28797aa3
--- /dev/null
+++ b/evals/tasks/lvms/check-thin-pool-utilization/task.yaml
@@ -0,0 +1,52 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "check-thin-pool-utilization"
+ difficulty: medium
+ description: "Check actual thin pool data and metadata utilization on nodes"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ # Verify LVMS is installed and ready
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ CLUSTER=$(kubectl get lvmclusters -n "$NS" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
+ if [ -z "$CLUSTER" ]; then
+ echo "No LVMCluster found - LVMS must be configured"
+ exit 1
+ fi
+
+ echo "LVMS is configured. This task requires checking LIVE thin pool utilization."
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ echo "=== Verification ==="
+ echo "The agent should have:"
+ echo " 1. Run 'lvs' command on the node to get thin pool data"
+ echo " 2. Reported data_percent and metadata_percent for thin pools"
+ echo " 3. This CANNOT be done with resources_list/resources_get"
+ echo ""
+ echo "If the agent only queried LVMVolumeGroupNodeStatus CRD,"
+ echo "it did NOT get live thin pool utilization - that requires lvs on node."
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ exit 0
+ prompt:
+ inline: |-
+ What is the current thin pool utilization on each node? I need the actual
+ data_percent and metadata_percent values from the LVM thin pools, not just
+ the Kubernetes CRD status.
diff --git a/evals/tasks/lvms/diagnose-disk-not-used/task.yaml b/evals/tasks/lvms/diagnose-disk-not-used/task.yaml
new file mode 100644
index 000000000..a11ccb976
--- /dev/null
+++ b/evals/tasks/lvms/diagnose-disk-not-used/task.yaml
@@ -0,0 +1,54 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "diagnose-disk-not-used"
+ difficulty: hard
+ description: "Diagnose why a specific disk is not being used by LVMS"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ # Verify LVMS is installed
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ echo "LVMS installed. This task tests disk filtering knowledge."
+ echo ""
+ echo "The agent must know LVMS device filtering rules:"
+ echo "- Read-only devices are excluded"
+ echo "- Devices with children (partitions) are excluded"
+ echo "- Devices with reserved partition labels are excluded"
+ echo "- LVM2_member fstype devices are excluded unless explicitly listed"
+ echo "- Loop devices in use by Kubernetes are excluded"
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ echo "=== Verification ==="
+ echo "The agent should have:"
+ echo " 1. Identified the node where the disk exists"
+ echo " 2. Run lsblk to inspect disk properties"
+ echo " 3. Checked LVMCluster deviceSelector configuration"
+ echo " 4. Identified which LVMS filtering rule excludes the disk"
+ echo " 5. Suggested how to make the disk available (if possible)"
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ exit 0
+ prompt:
+ inline: |-
+ A user reports that /dev/sdb on node worker-1 is not being used by LVMS
+ even though they expected it to be available for storage.
+
+ Investigate why this disk is not being used by LVMS and explain
+ the reason to the user.
diff --git a/evals/tasks/lvms/diagnose-no-space-left/task.yaml b/evals/tasks/lvms/diagnose-no-space-left/task.yaml
new file mode 100644
index 000000000..30831b3f6
--- /dev/null
+++ b/evals/tasks/lvms/diagnose-no-space-left/task.yaml
@@ -0,0 +1,46 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "diagnose-no-space-left"
+ difficulty: hard
+ description: "Diagnose why a workload is getting 'no space left on device' errors"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ echo "LVMS installed. This is a hard diagnostic task."
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ echo "=== Verification ==="
+ echo "The agent should have:"
+ echo " 1. Checked thin pool utilization (requires lvs on node)"
+ echo " 2. Checked VG free space (requires vgs on node)"
+ echo " 3. Correlated with PVC/LV sizes"
+ echo " 4. Identified if thin pool is exhausted"
+ echo ""
+ echo "This task REQUIRES node-level LVM commands to diagnose properly."
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ exit 0
+ prompt:
+ inline: |-
+ A workload using LVMS storage is reporting "no space left on device" errors,
+ but the PVC shows it still has capacity. This might be a thin pool exhaustion
+ issue. Can you check the actual thin pool and volume group utilization to
+ diagnose what's happening?
diff --git a/evals/tasks/lvms/forced-cleanup/task.yaml b/evals/tasks/lvms/forced-cleanup/task.yaml
new file mode 100644
index 000000000..67963d9ed
--- /dev/null
+++ b/evals/tasks/lvms/forced-cleanup/task.yaml
@@ -0,0 +1,89 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "lvms-forced-cleanup"
+ difficulty: hard
+ description: "Perform LVMS forced cleanup in correct order with finalizer handling"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ # Verify LVMS is installed
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ # Create test resources that simulate stuck LVMS state
+ # These will have finalizers that block deletion
+
+ # Create a LogicalVolume with a finalizer (cluster-scoped resource, no namespace)
+ cat > /tmp/logicalvolume.yaml << 'EOF'
+ apiVersion: topolvm.io/v1
+ kind: LogicalVolume
+ metadata:
+ name: stuck-lv-test
+ finalizers:
+ - topolvm.io/logicalvolume
+ spec:
+ name: stuck-lv-test
+ nodeName: test-node
+ size: 1Gi
+ deviceClass: vg1
+ EOF
+ kubectl apply -f /tmp/logicalvolume.yaml
+ rm -f /tmp/logicalvolume.yaml
+
+ echo "Created stuck LogicalVolume with finalizer"
+ echo "Agent must remove finalizer before deletion"
+ sleep 2
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ echo "=== Verification: Checking forced cleanup was performed correctly ==="
+
+ # Check if the LogicalVolume was removed (cluster-scoped, no namespace)
+ if kubectl get logicalvolume stuck-lv-test > /dev/null 2>&1; then
+ echo "FAIL: LogicalVolume stuck-lv-test still exists"
+ echo "Agent should have:"
+ echo " 1. Patched to remove finalizer: kubectl patch logicalvolume stuck-lv-test -p '{\"metadata\":{\"finalizers\":[]}}' --type=merge"
+ echo " 2. Deleted the resource: kubectl delete logicalvolume stuck-lv-test"
+ exit 1
+ fi
+
+ echo "SUCCESS: LogicalVolume stuck-lv-test was properly cleaned up"
+ echo ""
+ echo "The agent correctly:"
+ echo " 1. Identified the resource had a finalizer blocking deletion"
+ echo " 2. Removed the finalizer before attempting delete"
+ echo " 3. Deleted the resource"
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ # Force remove in case test failed (cluster-scoped, no namespace)
+ kubectl patch logicalvolume stuck-lv-test -p '{"metadata":{"finalizers":[]}}' --type=merge 2>/dev/null || true
+ kubectl delete logicalvolume stuck-lv-test --ignore-not-found 2>/dev/null || true
+ prompt:
+ inline: |-
+ There is a stuck LogicalVolume named "stuck-lv-test" (cluster-scoped resource, no namespace)
+ that cannot be deleted normally because it has a finalizer.
+
+ First, call the lvms-troubleshoot prompt to get the LVMS forced cleanup procedure.
+ Then use the MCP tools (resources_create_or_update to remove finalizers, resources_delete to delete)
+ to perform the cleanup:
+ 1. Remove the finalizer that is blocking deletion
+ 2. Delete the resource
+
+ Do NOT use bash/kubectl commands - use the MCP tools only.
+ Report what you did and confirm the resource was successfully removed.
diff --git a/evals/tasks/lvms/list-node-block-devices/task.yaml b/evals/tasks/lvms/list-node-block-devices/task.yaml
new file mode 100644
index 000000000..b40aa4bc1
--- /dev/null
+++ b/evals/tasks/lvms/list-node-block-devices/task.yaml
@@ -0,0 +1,43 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "list-node-block-devices"
+ difficulty: medium
+ description: "List block devices on a node to see what's available for LVMS"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ # Get a node name for testing
+ NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
+ if [ -z "$NODE" ]; then
+ echo "No nodes found"
+ exit 1
+ fi
+ echo "Test node: $NODE"
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ echo "=== Verification ==="
+ echo "The agent should have:"
+ echo " 1. Run 'lsblk' command on the node"
+ echo " 2. Shown disk devices, partitions, sizes"
+ echo " 3. This CANNOT be done with Kubernetes API - requires node access"
+ echo ""
+ echo "If the agent only queried Kubernetes resources, it FAILED."
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ exit 0
+ prompt:
+ inline: |-
+ We want to add more storage capacity to LVMS. What unused disks or
+ partitions are available on the cluster nodes that could potentially
+ be added to the volume groups?
diff --git a/evals/tasks/lvms/show-lvm-physical-volumes/task.yaml b/evals/tasks/lvms/show-lvm-physical-volumes/task.yaml
new file mode 100644
index 000000000..819525972
--- /dev/null
+++ b/evals/tasks/lvms/show-lvm-physical-volumes/task.yaml
@@ -0,0 +1,43 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "show-lvm-physical-volumes"
+ difficulty: medium
+ description: "Show LVM physical volumes on nodes using pvs command"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ # Verify LVMS is installed
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ echo "LVMS installed. This task requires running pvs on nodes."
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ echo "=== Verification ==="
+ echo "The agent should have:"
+ echo " 1. Run 'pvs' command on the node(s)"
+ echo " 2. Shown PV name, VG name, size, free space"
+ echo " 3. This requires node debug access - not available via K8s API"
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ exit 0
+ prompt:
+ inline: |-
+ I'm trying to understand how our LVMS storage is set up. Which physical
+ devices are being used for LVM storage, and how much space does each
+ device contribute to the volume groups?
diff --git a/evals/tasks/lvms/show-volume-groups/task.yaml b/evals/tasks/lvms/show-volume-groups/task.yaml
new file mode 100644
index 000000000..f19a64097
--- /dev/null
+++ b/evals/tasks/lvms/show-volume-groups/task.yaml
@@ -0,0 +1,42 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "show-volume-groups"
+ difficulty: medium
+ description: "Show LVM volume groups on nodes using vgs command"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ echo "LVMS installed. This task requires running vgs on nodes."
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ echo "=== Verification ==="
+ echo "The agent should have:"
+ echo " 1. Run 'vgs' command on the node(s)"
+ echo " 2. Shown VG name, PV count, LV count, size, free space"
+ echo " 3. This requires node debug access"
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ exit 0
+ prompt:
+ inline: |-
+ How is LVMS organizing storage on each node? I want to understand the
+ volume group layout - total capacity, how much is free, and how many
+ logical volumes exist in each VG.
diff --git a/evals/tasks/lvms/troubleshoot-prompt/task.yaml b/evals/tasks/lvms/troubleshoot-prompt/task.yaml
new file mode 100644
index 000000000..c4466a6c8
--- /dev/null
+++ b/evals/tasks/lvms/troubleshoot-prompt/task.yaml
@@ -0,0 +1,46 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "lvms-troubleshoot-prompt"
+ difficulty: medium
+ description: "Use the lvms-troubleshoot prompt to diagnose LVMS storage health"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="openshift-lvm-storage"
+
+ # Verify LVMS is installed
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ echo "LVMS installed. Testing lvms-troubleshoot prompt."
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ echo "=== Verification ==="
+ echo "The agent should have:"
+ echo " 1. Used the lvms-troubleshoot prompt"
+ echo " 2. Received pre-fetched LVMCluster status, node statuses, and pod info"
+ echo " 3. Analyzed vg_attr and other LVMS-specific fields"
+ echo " 4. Reported findings on cluster health"
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ exit 0
+ prompt:
+ inline: |-
+ Use the lvms-troubleshoot prompt to check the health of LVMS storage on this cluster.
+ Report your findings including:
+ - LVMCluster state
+ - Volume group health on each node
+ - Any issues or warnings detected
diff --git a/evals/tasks/lvms/troubleshoot-stuck-pvc/task.yaml b/evals/tasks/lvms/troubleshoot-stuck-pvc/task.yaml
new file mode 100644
index 000000000..3ca545ab1
--- /dev/null
+++ b/evals/tasks/lvms/troubleshoot-stuck-pvc/task.yaml
@@ -0,0 +1,78 @@
+kind: Task
+metadata:
+ labels:
+ suite: lvms
+ project: lvms
+ requires: lvms
+ annotations:
+ project-name: "LVMS"
+ project-url: "https://github.com/openshift/lvm-operator"
+ name: "troubleshoot-lvms-stuck-pvc"
+ difficulty: hard
+ description: "Diagnose why a PVC using LVMS storage is stuck in Pending state"
+steps:
+ setup:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="lvms-eval-app"
+
+ # Verify LVMS is installed
+ if ! kubectl get crd lvmclusters.lvm.topolvm.io > /dev/null 2>&1; then
+ echo "LVMS CRD not found - LVMS must be installed"
+ exit 1
+ fi
+
+ # Create test namespace
+ kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -
+
+ # Create a PVC that will be stuck (requesting impossibly large storage)
+ cat > /tmp/stuck-pvc.yaml << 'EOF'
+ apiVersion: v1
+ kind: PersistentVolumeClaim
+ metadata:
+ name: stuck-pvc
+ namespace: lvms-eval-app
+ spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 500Ti
+ storageClassName: lvms-vg1
+ EOF
+ kubectl apply -f /tmp/stuck-pvc.yaml
+ rm -f /tmp/stuck-pvc.yaml
+
+ echo "Created PVC requesting 500Ti - this should be stuck pending"
+ sleep 10
+ verify:
+ inline: |-
+ #!/usr/bin/env bash
+ NS="lvms-eval-app"
+
+ echo "=== Verification: Checking if agent diagnosed the issue ==="
+
+ # Check PVC status
+ PHASE=$(kubectl get pvc stuck-pvc -n "$NS" -o jsonpath='{.status.phase}' 2>/dev/null || echo "Unknown")
+ echo "PVC phase: $PHASE"
+
+ echo ""
+ echo "=== Troubleshooting Eval Complete ==="
+ echo "The agent should have:"
+ echo " 1. Investigated the PVC and its storage class"
+ echo " 2. Checked LVMS cluster status and node capacity"
+ echo " 3. Identified insufficient capacity as the root cause (500Ti requested)"
+ echo " 4. Recommended action (reduce PVC size or add storage capacity)"
+ echo ""
+
+ exit 0
+ cleanup:
+ inline: |-
+ #!/usr/bin/env bash
+ kubectl delete pvc stuck-pvc -n lvms-eval-app --ignore-not-found
+ kubectl delete namespace lvms-eval-app --ignore-not-found
+ prompt:
+ inline: |-
+ A PVC named "stuck-pvc" in the lvms-eval-app namespace has been stuck in Pending state for over an hour.
+ The application team is blocked waiting for storage. Investigate why this PVC cannot bind and tell me
+ what needs to be fixed.
diff --git a/internal/tools/update-readme/main.go b/internal/tools/update-readme/main.go
index a10780111..7c25f6af2 100644
--- a/internal/tools/update-readme/main.go
+++ b/internal/tools/update-readme/main.go
@@ -22,6 +22,7 @@ import (
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kcp"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt"
+ _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/lvms"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/netobserv"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton"
"k8s.io/apimachinery/pkg/runtime/schema"
diff --git a/pkg/mcp/modules.go b/pkg/mcp/modules.go
index 0c5ba3c6c..22266aec9 100644
--- a/pkg/mcp/modules.go
+++ b/pkg/mcp/modules.go
@@ -7,6 +7,7 @@ import (
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kcp"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt"
+ _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/lvms"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/netobserv"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton"
)
diff --git a/pkg/mcp/testdata/toolsets-lvms-tools.json b/pkg/mcp/testdata/toolsets-lvms-tools.json
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/pkg/mcp/testdata/toolsets-lvms-tools.json
@@ -0,0 +1 @@
+[]
diff --git a/pkg/mcp/toolsets_test.go b/pkg/mcp/toolsets_test.go
index 9a19cff0b..5845918c1 100644
--- a/pkg/mcp/toolsets_test.go
+++ b/pkg/mcp/toolsets_test.go
@@ -20,6 +20,7 @@ import (
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/kcp"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt"
+ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/lvms"
mgToolset "github.com/containers/kubernetes-mcp-server/pkg/toolsets/mustgather"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/openshift"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton"
@@ -201,6 +202,7 @@ func (s *ToolsetsSuite) TestGranularToolsetsTools() {
&helm.Toolset{},
&kiali.Toolset{},
&kubevirt.Toolset{},
+ &lvms.Toolset{},
&tekton.Toolset{},
&clusterDiagnosticsToolset.Toolset{},
}
diff --git a/pkg/toolsets/lvms/lvms_capacity.go b/pkg/toolsets/lvms/lvms_capacity.go
new file mode 100644
index 000000000..5677bd31a
--- /dev/null
+++ b/pkg/toolsets/lvms/lvms_capacity.go
@@ -0,0 +1,350 @@
+package lvms
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/dynamic"
+
+ "github.com/containers/kubernetes-mcp-server/pkg/api"
+)
+
+var (
+ csiStorageCapacityGVR = schema.GroupVersionResource{
+ Group: "storage.k8s.io",
+ Version: "v1",
+ Resource: "csistoragecapacities",
+ }
+ pvGVR = schema.GroupVersionResource{
+ Group: "",
+ Version: "v1",
+ Resource: "persistentvolumes",
+ }
+)
+
+// initLVMSCapacity initializes the LVMS capacity monitoring prompt
+func initLVMSCapacity() []api.ServerPrompt {
+ return []api.ServerPrompt{
+ {
+ Prompt: api.Prompt{
+ Name: "lvms-capacity",
+ Title: "LVMS Capacity Check",
+ Description: "Check LVMS storage capacity across all nodes including available space per node, PVC usage, and capacity warnings",
+ Arguments: []api.PromptArgument{
+ {
+ Name: "namespace",
+ Description: "The LVMS namespace (default: openshift-lvm-storage)",
+ Required: false,
+ },
+ },
+ },
+ Handler: lvmsCapacityHandler,
+ },
+ }
+}
+
+// lvmsCapacityHandler implements the LVMS capacity monitoring prompt
+func lvmsCapacityHandler(params api.PromptHandlerParams) (*api.PromptCallResult, error) {
+ args := params.GetArguments()
+ namespace := args["namespace"]
+ if namespace == "" {
+ namespace = DefaultLVMSNamespace
+ }
+
+ ctx := params.Context
+ if ctx == nil {
+ ctx = context.Background()
+ }
+
+ dynamicClient := params.DynamicClient()
+
+ // Fetch all capacity data
+ capacitySummary := fetchCSICapacity(ctx, dynamicClient, namespace)
+ pvcSummary := fetchPVCCapacitySummary(ctx, dynamicClient)
+ pvSummary := fetchPVCapacitySummary(ctx, dynamicClient)
+
+ guideText := fmt.Sprintf(`# LVMS Capacity Report
+
+## Overview
+
+This report shows LVMS storage capacity across all nodes to help with capacity planning
+and identify nodes that may need additional storage.
+
+---
+
+## Available Capacity by Node
+
+CSIStorageCapacity reports how much space TopoLVM can provision on each node:
+
+%s
+
+---
+
+## PVC Usage Summary
+
+PVCs using TopoLVM storage classes:
+
+%s
+
+---
+
+## PV Capacity Summary
+
+Provisioned PersistentVolumes by TopoLVM:
+
+%s
+
+---
+
+## Capacity Analysis
+
+Based on the data above, assess:
+
+1. **Available Headroom**: How much free space remains per node?
+2. **Usage Distribution**: Is storage evenly distributed or concentrated on few nodes?
+3. **Growth Trend**: Are PVCs consuming space faster than expected?
+4. **At-Risk Nodes**: Any nodes with less than 20%% free capacity?
+
+---
+
+## Recommendations
+
+- **If a node has < 20%% free**: Consider adding disks to LVMCluster deviceSelector
+- **If PVCs are pending**: Check if any node has sufficient capacity
+- **For capacity planning**: Total available = sum of all node capacities
+- **For HA workloads**: Ensure multiple nodes have sufficient capacity
+
+---
+
+## Report Summary
+
+Provide:
+- **Overall Status**: OK / Low Capacity / Critical
+- **Total Available**: Sum across all nodes
+- **Total Used**: Sum of all PV capacities
+- **Utilization**: Used / (Used + Available) %%
+- **Action Items**: Any immediate concerns
+`, capacitySummary, pvcSummary, pvSummary)
+
+ return api.NewPromptCallResult(
+ "LVMS capacity report generated",
+ []api.PromptMessage{
+ {
+ Role: "user",
+ Content: api.PromptContent{
+ Type: "text",
+ Text: guideText,
+ },
+ },
+ {
+ Role: "assistant",
+ Content: api.PromptContent{
+ Type: "text",
+ Text: "I'll analyze the LVMS capacity data to assess storage health and identify any capacity concerns.",
+ },
+ },
+ },
+ nil,
+ ), nil
+}
+
+// fetchCSICapacity fetches CSIStorageCapacity resources for TopoLVM
+func fetchCSICapacity(ctx context.Context, dynamicClient dynamic.Interface, namespace string) string {
+ var result strings.Builder
+
+ capacities, err := dynamicClient.Resource(csiStorageCapacityGVR).Namespace(namespace).List(ctx, metav1.ListOptions{
+ LabelSelector: "csi.storage.k8s.io/drivername=topolvm.io",
+ })
+ if err != nil {
+ return fmt.Sprintf("*Error listing CSIStorageCapacity: %v*\n", err)
+ }
+
+ if len(capacities.Items) == 0 {
+ return "*No CSIStorageCapacity found for TopoLVM — LVMS may not be configured*\n"
+ }
+
+ result.WriteString("| Node | Storage Class | Available Capacity |\n")
+ result.WriteString("|------|---------------|--------------------|\n")
+
+ var totalCapacity int64
+ for _, cap := range capacities.Items {
+ storageClass, _, _ := unstructured.NestedString(cap.Object, "storageClassName")
+ capacity, _, _ := unstructured.NestedString(cap.Object, "capacity")
+
+ // Get node from topology
+ node := "unknown"
+ if topology, found, _ := unstructured.NestedMap(cap.Object, "nodeTopology", "matchLabels"); found {
+ if n, ok := topology["topology.topolvm.io/node"].(string); ok {
+ node = n
+ }
+ }
+
+ // Parse capacity for total
+ totalCapacity += parseCapacity(capacity)
+
+ fmt.Fprintf(&result, "| %s | %s | %s |\n", node, storageClass, capacity)
+ }
+
+ fmt.Fprintf(&result, "\n**Total Available: %s**\n", formatCapacity(totalCapacity))
+
+ return result.String()
+}
+
+// fetchPVCCapacitySummary fetches PVC summary for TopoLVM storage classes
+func fetchPVCCapacitySummary(ctx context.Context, dynamicClient dynamic.Interface) string {
+ var result strings.Builder
+
+ // Get TopoLVM storage classes first
+ scList, err := dynamicClient.Resource(storageClassGVR).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return fmt.Sprintf("*Error listing StorageClasses: %v*\n", err)
+ }
+
+ topoLVMSCs := make(map[string]bool)
+ for _, sc := range scList.Items {
+ provisioner, _, _ := unstructured.NestedString(sc.Object, "provisioner")
+ if strings.Contains(provisioner, "topolvm") {
+ topoLVMSCs[sc.GetName()] = true
+ }
+ }
+
+ if len(topoLVMSCs) == 0 {
+ return "*No TopoLVM storage classes found*\n"
+ }
+
+ // Get PVCs
+ pvcList, err := dynamicClient.Resource(pvcGVR).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return fmt.Sprintf("*Error listing PVCs: %v*\n", err)
+ }
+
+ var boundCount, pendingCount int
+ var totalRequested int64
+ pendingPVCs := []string{}
+
+ for _, pvc := range pvcList.Items {
+ scName, _, _ := unstructured.NestedString(pvc.Object, "spec", "storageClassName")
+ if !topoLVMSCs[scName] {
+ continue
+ }
+
+ phase, _, _ := unstructured.NestedString(pvc.Object, "status", "phase")
+ requestedStorage, _, _ := unstructured.NestedString(pvc.Object, "spec", "resources", "requests", "storage")
+
+ switch phase {
+ case "Bound":
+ boundCount++
+ totalRequested += parseCapacity(requestedStorage)
+ case "Pending":
+ pendingCount++
+ pendingPVCs = append(pendingPVCs, fmt.Sprintf("%s/%s (%s)",
+ pvc.GetNamespace(), pvc.GetName(), requestedStorage))
+ }
+ }
+
+ fmt.Fprintf(&result, "- **Bound PVCs**: %d (Total requested: %s)\n", boundCount, formatCapacity(totalRequested))
+ fmt.Fprintf(&result, "- **Pending PVCs**: %d\n", pendingCount)
+
+ if len(pendingPVCs) > 0 {
+ result.WriteString("\n**Pending PVCs (need attention):**\n")
+ for _, pvc := range pendingPVCs {
+ fmt.Fprintf(&result, "- %s\n", pvc)
+ }
+ }
+
+ return result.String()
+}
+
+// fetchPVCapacitySummary fetches PV summary for TopoLVM
+func fetchPVCapacitySummary(ctx context.Context, dynamicClient dynamic.Interface) string {
+ var result strings.Builder
+
+ pvList, err := dynamicClient.Resource(pvGVR).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return fmt.Sprintf("*Error listing PVs: %v*\n", err)
+ }
+
+ var topoLVMCount int
+ var totalCapacity int64
+ nodeCapacity := make(map[string]int64)
+
+ for _, pv := range pvList.Items {
+ csiDriver, _, _ := unstructured.NestedString(pv.Object, "spec", "csi", "driver")
+ if csiDriver != "topolvm.io" {
+ continue
+ }
+
+ topoLVMCount++
+ capacity, _, _ := unstructured.NestedString(pv.Object, "spec", "capacity", "storage")
+ capBytes := parseCapacity(capacity)
+ totalCapacity += capBytes
+
+ // Get node affinity
+ terms, found, _ := unstructured.NestedSlice(pv.Object, "spec", "nodeAffinity", "required", "nodeSelectorTerms")
+ if found && len(terms) > 0 {
+ if term, ok := terms[0].(map[string]any); ok {
+ if exprs, ok := term["matchExpressions"].([]any); ok {
+ for _, expr := range exprs {
+ if e, ok := expr.(map[string]any); ok {
+ if e["key"] == "topology.topolvm.io/node" {
+ if values, ok := e["values"].([]any); ok && len(values) > 0 {
+ if node, ok := values[0].(string); ok {
+ nodeCapacity[node] += capBytes
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fmt.Fprintf(&result, "- **Total PVs**: %d\n", topoLVMCount)
+ fmt.Fprintf(&result, "- **Total Provisioned**: %s\n", formatCapacity(totalCapacity))
+
+ if len(nodeCapacity) > 0 {
+ result.WriteString("\n**Provisioned by Node:**\n\n")
+ result.WriteString("| Node | Provisioned |\n")
+ result.WriteString("|------|-------------|\n")
+ for node, cap := range nodeCapacity {
+ fmt.Fprintf(&result, "| %s | %s |\n", node, formatCapacity(cap))
+ }
+ }
+
+ return result.String()
+}
+
+// parseCapacity parses a Kubernetes quantity string to bytes
+func parseCapacity(s string) int64 {
+ if s == "" {
+ return 0
+ }
+ q, err := resource.ParseQuantity(s)
+ if err != nil {
+ return 0
+ }
+ return q.Value()
+}
+
+// formatCapacity formats bytes to human readable string
+func formatCapacity(bytes int64) string {
+ if bytes == 0 {
+ return "0"
+ }
+ const unit = 1024
+ if bytes < unit {
+ return fmt.Sprintf("%d B", bytes)
+ }
+ div, exp := int64(unit), 0
+ for n := bytes / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+ return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
+}
diff --git a/pkg/toolsets/lvms/lvms_troubleshoot.go b/pkg/toolsets/lvms/lvms_troubleshoot.go
new file mode 100644
index 000000000..5a2b4f1ab
--- /dev/null
+++ b/pkg/toolsets/lvms/lvms_troubleshoot.go
@@ -0,0 +1,760 @@
+package lvms
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/dynamic"
+
+ "github.com/containers/kubernetes-mcp-server/pkg/api"
+ "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
+ "github.com/containers/kubernetes-mcp-server/pkg/output"
+)
+
+const (
+ // DefaultLVMSNamespace is the default namespace for LVMS components
+ DefaultLVMSNamespace = "openshift-lvm-storage"
+)
+
+// GVRs for LVMS resources
+var (
+ lvmClusterGVR = schema.GroupVersionResource{
+ Group: "lvm.topolvm.io",
+ Version: "v1alpha1",
+ Resource: "lvmclusters",
+ }
+ lvmVolumeGroupNodeStatusGVR = schema.GroupVersionResource{
+ Group: "lvm.topolvm.io",
+ Version: "v1alpha1",
+ Resource: "lvmvolumegroupnodestatuses",
+ }
+ podGVR = schema.GroupVersionResource{
+ Group: "",
+ Version: "v1",
+ Resource: "pods",
+ }
+ csvGVR = schema.GroupVersionResource{
+ Group: "operators.coreos.com",
+ Version: "v1alpha1",
+ Resource: "clusterserviceversions",
+ }
+ storageClassGVR = schema.GroupVersionResource{
+ Group: "storage.k8s.io",
+ Version: "v1",
+ Resource: "storageclasses",
+ }
+ pvcGVR = schema.GroupVersionResource{
+ Group: "",
+ Version: "v1",
+ Resource: "persistentvolumeclaims",
+ }
+)
+
+// initLVMSTroubleshoot initializes the LVMS troubleshooting prompt
+func initLVMSTroubleshoot() []api.ServerPrompt {
+ return []api.ServerPrompt{
+ {
+ Prompt: api.Prompt{
+ Name: "lvms-troubleshoot",
+ Title: "LVMS Troubleshoot",
+ Description: "Diagnose LVMS storage issues by gathering LVMCluster status, volume group health, and node-level LVM data with interpretation of domain-specific fields",
+ Arguments: []api.PromptArgument{
+ {
+ Name: "namespace",
+ Description: "The LVMS namespace (default: openshift-lvm-storage)",
+ Required: false,
+ },
+ {
+ Name: "node",
+ Description: "Specific node to inspect (default: all nodes with LVMS)",
+ Required: false,
+ },
+ },
+ },
+ Handler: lvmsTroubleshootHandler,
+ },
+ }
+}
+
+// lvmsTroubleshootHandler implements the LVMS troubleshooting prompt
+func lvmsTroubleshootHandler(params api.PromptHandlerParams) (*api.PromptCallResult, error) {
+ args := params.GetArguments()
+ namespace := args["namespace"]
+ if namespace == "" {
+ namespace = DefaultLVMSNamespace
+ }
+ targetNode := args["node"]
+
+ ctx := params.Context
+ if ctx == nil {
+ ctx = context.Background()
+ }
+
+ dynamicClient := params.DynamicClient()
+
+ // Fetch all relevant LVMS resources
+ operatorHealthYaml := fetchOperatorHealth(ctx, dynamicClient, namespace)
+ lvmClusterYaml := fetchLVMClusterStatus(ctx, dynamicClient, namespace)
+ vgNodeStatusYaml := fetchVolumeGroupNodeStatus(ctx, dynamicClient, namespace, targetNode)
+ storageClassYaml := fetchTopoLVMStorageClasses(ctx, dynamicClient)
+ pvcStatusYaml := fetchTopoLVMPVCs(ctx, dynamicClient)
+ vgManagerPodsYaml, podNames := fetchVGManagerPods(ctx, dynamicClient, namespace)
+ vgManagerLogsText := fetchVGManagerLogs(ctx, params.KubernetesClient, namespace, podNames)
+ eventsYaml := fetchLVMSEvents(ctx, params.KubernetesClient, namespace)
+
+ guideText := fmt.Sprintf(`# LVMS Troubleshooting Guide
+
+## Namespace: %s
+
+Use this guide to diagnose LVMS storage issues. The relevant resource data and LVM-specific field interpretations are provided below.
+
+---
+
+## LVM Attribute Field Reference
+
+Understanding these fields is critical for LVMS troubleshooting:
+
+### Volume Group Attributes (vg_attr)
+The vg_attr field is a 6-character string where each position has meaning:
+
+| Position | Character | Meaning |
+|----------|-----------|---------|
+| 1 | w/r | Writeable / Read-only |
+| 2 | z/- | Resizeable / Fixed |
+| 3 | x/- | Exported / Normal |
+| 4 | p/- | Partial (missing PVs) / Complete |
+| 5 | c/l/n/s | Allocation: contiguous/cling/normal/anywhere |
+| 6 | c/- | Clustered / Local |
+
+**Healthy VG:** "wz--n-" (writeable, resizeable, not partial, normal allocation)
+**Degraded VG:** "wz-pn-" (the 'p' in position 4 means missing PVs!)
+
+### Logical Volume Attributes (lv_attr)
+The lv_attr field is a 10-character string. Key positions for LVMS:
+
+| Position | Key Values | Meaning |
+|----------|------------|---------|
+| 1 | V/v/t | Virtual (thin), thin pool, or other type |
+| 5 | a/- | Active / Inactive |
+| 9 | p/- | Partial activation (RAID degraded!) / Normal |
+
+**Healthy LV:** Position 9 is '-'
+**Degraded RAID LV:** Position 9 is 'p' (partial)
+
+### Key Diagnostic Fields
+- **vg_missing_pv_count > 0**: Volume group has missing physical volumes (disk failure)
+- **vg_free**: Available space in volume group
+- **lv_health_status**: "partial" indicates RAID degradation
+- **raid_sync_percent**: < 100 means RAID is rebuilding
+
+---
+
+## Step 1: Operator Health
+
+Check LVMS operator status:
+- CSV should be "Succeeded"
+- All operator pods should be "Running"
+
+%s
+
+---
+
+## Step 2: LVMCluster Status
+
+The LVMCluster CR is the primary configuration. Check:
+- Does an LVMCluster exist?
+- What is the state? ("Ready" = healthy)
+- Are all deviceClasses configured correctly?
+- **Device paths**: Use /dev/disk/by-id/ not /dev/sdX (sdX paths change on reboot!)
+
+%s
+
+---
+
+## Step 3: Volume Group Node Status
+
+LVMVolumeGroupNodeStatus shows per-node volume group health:
+- Check status for each node
+- Look for "Degraded" or "Failed" states
+- Review raidStatus if RAID is configured
+
+%s
+
+---
+
+## Step 4: Storage Classes
+
+TopoLVM storage classes provisioned by LVMS:
+
+%s
+
+---
+
+## Step 5: PVC Status
+
+PVCs using TopoLVM provisioner:
+- Pending PVCs indicate capacity or configuration issues
+- Check events on Pending PVCs for root cause
+
+%s
+
+---
+
+## Step 6: vg-manager Pod Health
+
+The vg-manager pods manage volume groups on each node:
+- Pods should be "Running" with all containers ready
+- CrashLoopBackOff indicates disk discovery issues
+- High restart counts suggest persistent problems
+
+%s
+
+---
+
+## Step 7: vg-manager Pod Logs
+
+Review logs for errors:
+- Look for disk discovery failures
+- Check for LVM command errors
+- Review any "error" or "failed" messages
+
+%s
+
+---
+
+## Step 8: Events
+
+Review events in the LVMS namespace:
+- Warning events indicate problems
+- Look for scheduling, disk, or permission issues
+
+%s
+
+---
+
+## Troubleshooting Analysis
+
+Based on the data above, analyze:
+
+1. **LVMCluster State**: Is it "Ready"? If not, what deviceClass is failing?
+2. **Node Status**: Are all nodes healthy? Look for "Degraded" or "Failed" states in LVMVolumeGroupNodeStatus.
+3. **vg-manager Pods**: Are they running? Check for CrashLoopBackOff or high restart counts.
+4. **Events**: Are there Warning events indicating problems?
+
+---
+
+## Common Issues and Recovery Procedures
+
+### 1. VG has missing PVs (vg_attr contains 'p')
+Disk failure detected. Recovery options:
+- **Option A - Reduce VG**: Remove failed disk from VG
+ `+"`"+`vgchange --activate n `+"`"+`
+ `+"`"+`vgreduce --removemissing --force `+"`"+`
+ Then patch LVMCluster to remove failed device path.
+
+- **Option B - Replace disk**: If replacement disk available
+ `+"`"+`pvcreate --restorefile /etc/lvm/backup/ --uuid /dev/`+"`"+`
+ `+"`"+`vgextend --restoremissing /dev/`+"`"+`
+
+### 2. Thin pool nearly full (data_percent > 80%%)
+- Extend the thin pool or add disks
+- Delete unused PVCs to free space
+- Check for stuck snapshots consuming space
+
+### 3. vg-manager CrashLoopBackOff
+- Check if deviceSelector.paths point to valid disks
+- Run `+"`"+`lsblk --paths --json`+"`"+` on the node to verify disk availability
+- Ensure disks are not in use by other systems
+
+### 4. RAID degraded (lv_health_status: partial)
+- Check raid_sync_percent - if syncing, wait for completion
+- If stuck at 0%%, the RAID may need repair:
+ `+"`"+`lvconvert --repair /`+"`"+`
+
+### 5. LVMCluster deletion stuck
+Perform forced cleanup (in order):
+1. Delete all PVCs using LVMS
+2. Remove finalizers from LogicalVolumes, then delete them
+3. Remove finalizers from LVMVolumeGroups, then delete them
+4. Remove finalizers from LVMVolumeGroupNodeStatus, then delete them
+5. Delete LVMCluster
+
+### 6. PVC stuck in Pending
+- Check if StorageClass exists
+- Verify LVMCluster is Ready
+- Check node has available capacity
+- If using WaitForFirstConsumer, PVC stays Pending until a Pod requests it (this is normal)
+
+---
+
+## Report Findings
+
+After analysis, report:
+- **Status:** Healthy / Degraded / Failed
+- **Root Cause:** Description of the issue found (or "None found")
+- **Affected Nodes:** List of nodes with problems
+- **Recommended Action:** Steps to resolve the issue
+`, namespace, operatorHealthYaml, lvmClusterYaml, vgNodeStatusYaml, storageClassYaml, pvcStatusYaml, vgManagerPodsYaml, vgManagerLogsText, eventsYaml)
+
+ return api.NewPromptCallResult(
+ "LVMS troubleshooting guide generated",
+ []api.PromptMessage{
+ {
+ Role: "user",
+ Content: api.PromptContent{
+ Type: "text",
+ Text: guideText,
+ },
+ },
+ {
+ Role: "assistant",
+ Content: api.PromptContent{
+ Type: "text",
+ Text: "I'll analyze the collected LVMS data to diagnose storage issues, paying special attention to vg_attr flags and node-level health indicators.",
+ },
+ },
+ },
+ nil,
+ ), nil
+}
+
+// fetchLVMClusterStatus fetches LVMCluster resources and returns their status
+func fetchLVMClusterStatus(ctx context.Context, dynamicClient dynamic.Interface, namespace string) string {
+ clusters, err := dynamicClient.Resource(lvmClusterGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return fmt.Sprintf("### LVMCluster\n\n*Error listing LVMClusters: %v*", err)
+ }
+
+ if len(clusters.Items) == 0 {
+ return "### LVMCluster\n\n*No LVMCluster found — LVMS may not be configured*"
+ }
+
+ var result strings.Builder
+ result.WriteString("### LVMCluster\n\n")
+
+ for _, cluster := range clusters.Items {
+ fmt.Fprintf(&result, "#### %s\n\n", cluster.GetName())
+
+ // Get overall state
+ state, _, _ := unstructured.NestedString(cluster.Object, "status", "state")
+ fmt.Fprintf(&result, "**State:** %s\n\n", valueOrNA(state))
+
+ // Get deviceClassStatuses
+ deviceStatuses, found, _ := unstructured.NestedSlice(cluster.Object, "status", "deviceClassStatuses")
+ if found && len(deviceStatuses) > 0 {
+ result.WriteString("**Device Class Statuses:**\n\n")
+ result.WriteString("| Name | Node | Status |\n")
+ result.WriteString("|------|------|--------|\n")
+
+ for _, ds := range deviceStatuses {
+ dsMap, ok := ds.(map[string]any)
+ if !ok {
+ continue
+ }
+ name, _ := dsMap["name"].(string)
+ nodeStatuses, _ := dsMap["nodeStatus"].([]any)
+ for _, ns := range nodeStatuses {
+ nsMap, ok := ns.(map[string]any)
+ if !ok {
+ continue
+ }
+ nodeName, _ := nsMap["node"].(string)
+ status, _ := nsMap["status"].(string)
+ fmt.Fprintf(&result, "| %s | %s | %s |\n", name, nodeName, status)
+ }
+ }
+ result.WriteString("\n")
+ }
+
+ // Get spec for device configuration
+ spec, found, _ := unstructured.NestedMap(cluster.Object, "spec", "storage")
+ if found {
+ specYaml, err := output.MarshalYaml(spec)
+ if err == nil {
+ fmt.Fprintf(&result, "**Storage Spec:**\n```yaml\n%s```\n\n", specYaml)
+ }
+ }
+ }
+
+ return result.String()
+}
+
+// fetchVolumeGroupNodeStatus fetches LVMVolumeGroupNodeStatus resources
+func fetchVolumeGroupNodeStatus(ctx context.Context, dynamicClient dynamic.Interface, namespace, targetNode string) string {
+ statuses, err := dynamicClient.Resource(lvmVolumeGroupNodeStatusGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return fmt.Sprintf("### LVMVolumeGroupNodeStatus\n\n*Error listing node statuses: %v*", err)
+ }
+
+ if len(statuses.Items) == 0 {
+ return "### LVMVolumeGroupNodeStatus\n\n*No LVMVolumeGroupNodeStatus found*"
+ }
+
+ var result strings.Builder
+ result.WriteString("### LVMVolumeGroupNodeStatus\n\n")
+
+ for _, status := range statuses.Items {
+ nodeName := status.GetName()
+
+ // Filter by target node if specified
+ if targetNode != "" && nodeName != targetNode {
+ continue
+ }
+
+ fmt.Fprintf(&result, "#### Node: %s\n\n", nodeName)
+
+ // Get spec which contains the actual status data
+ spec, found, _ := unstructured.NestedMap(status.Object, "spec")
+ if !found {
+ result.WriteString("*No spec found*\n\n")
+ continue
+ }
+
+ specYaml, err := output.MarshalYaml(spec)
+ if err == nil {
+ fmt.Fprintf(&result, "```yaml\n%s```\n\n", specYaml)
+ }
+ }
+
+ return result.String()
+}
+
+// fetchVGManagerPods fetches vg-manager pods
+func fetchVGManagerPods(ctx context.Context, dynamicClient dynamic.Interface, namespace string) (string, []string) {
+ pods, err := dynamicClient.Resource(podGVR).Namespace(namespace).List(ctx, metav1.ListOptions{
+ LabelSelector: "app.kubernetes.io/name=vg-manager",
+ })
+ if err != nil {
+ return fmt.Sprintf("### vg-manager Pods\n\n*Error listing pods: %v*", err), nil
+ }
+
+ if len(pods.Items) == 0 {
+ return "### vg-manager Pods\n\n*No vg-manager pods found — LVMS may not be installed*", nil
+ }
+
+ var result strings.Builder
+ var podNames []string
+ result.WriteString("### vg-manager Pods\n\n")
+ result.WriteString("| Name | Node | Phase | Ready | Restarts |\n")
+ result.WriteString("|------|------|-------|-------|----------|\n")
+
+ for _, pod := range pods.Items {
+ podNames = append(podNames, pod.GetName())
+ phase, _, _ := unstructured.NestedString(pod.Object, "status", "phase")
+ nodeName, _, _ := unstructured.NestedString(pod.Object, "spec", "nodeName")
+ ready, restarts := getPodReadyAndRestarts(&pod)
+
+ fmt.Fprintf(&result, "| %s | %s | %s | %s | %d |\n",
+ pod.GetName(), valueOrNA(nodeName), valueOrNA(phase), ready, restarts)
+ }
+ result.WriteString("\n")
+
+ return result.String(), podNames
+}
+
+// fetchVGManagerLogs fetches logs from vg-manager pods
+func fetchVGManagerLogs(ctx context.Context, client api.KubernetesClient, namespace string, podNames []string) string {
+ if len(podNames) == 0 {
+ return "### vg-manager Logs\n\n*No vg-manager pods found*"
+ }
+
+ core := kubernetes.NewCore(client)
+ var result strings.Builder
+ result.WriteString("### vg-manager Logs\n\n")
+
+ for _, podName := range podNames {
+ fmt.Fprintf(&result, "#### Pod: %s\n\n", podName)
+ // Use empty container name to get logs from the default container
+ logs, err := core.PodsLog(ctx, namespace, podName, "", false, 100)
+ if err != nil {
+ fmt.Fprintf(&result, "*Error fetching logs: %v*\n\n", err)
+ continue
+ }
+
+ errorLines := filterLogLines(logs, []string{"error", "failed", "unable", "cannot", "no available"})
+ if errorLines != "" {
+ fmt.Fprintf(&result, "**Error lines (last 100 log lines):**\n```\n%s\n```\n\n", errorLines)
+ } else {
+ result.WriteString("*No error lines found in recent logs*\n\n")
+ }
+ }
+
+ return result.String()
+}
+
+// fetchLVMSEvents fetches events in the LVMS namespace
+func fetchLVMSEvents(ctx context.Context, client api.KubernetesClient, namespace string) string {
+ core := kubernetes.NewCore(client)
+ eventMap, err := core.EventsList(ctx, namespace, api.ListOptions{})
+ if err != nil {
+ return fmt.Sprintf("### Events\n\n*Error listing events: %v*", err)
+ }
+
+ if len(eventMap) == 0 {
+ return "### Events\n\n*No events found in namespace*"
+ }
+
+ // Filter for warning events
+ var warningEvents []map[string]any
+ for _, event := range eventMap {
+ eventType, _ := event["Type"].(string)
+ if eventType == "Warning" {
+ warningEvents = append(warningEvents, event)
+ }
+ }
+
+ if len(warningEvents) == 0 {
+ return "### Events\n\n*No Warning events found — this is good*"
+ }
+
+ // Limit to most recent 10
+ if len(warningEvents) > 10 {
+ warningEvents = warningEvents[len(warningEvents)-10:]
+ }
+
+ yamlStr, err := output.MarshalYaml(warningEvents)
+ if err != nil {
+ return fmt.Sprintf("### Events\n\n*Error marshaling events: %v*", err)
+ }
+
+ return fmt.Sprintf("### Warning Events (last 10)\n\n```yaml\n%s```", yamlStr)
+}
+
+// Helper functions
+
+func valueOrNA(s string) string {
+ if s == "" {
+ return "N/A"
+ }
+ return s
+}
+
+func getPodReadyAndRestarts(pod *unstructured.Unstructured) (string, int64) {
+ containerStatuses, found, _ := unstructured.NestedSlice(pod.Object, "status", "containerStatuses")
+ if !found {
+ return "N/A", 0
+ }
+
+ totalContainers := len(containerStatuses)
+ readyCount := 0
+ var totalRestarts int64
+
+ for _, cs := range containerStatuses {
+ csMap, ok := cs.(map[string]any)
+ if !ok {
+ continue
+ }
+ if ready, ok := csMap["ready"].(bool); ok && ready {
+ readyCount++
+ }
+ switch restarts := csMap["restartCount"].(type) {
+ case int64:
+ totalRestarts += restarts
+ case float64:
+ totalRestarts += int64(restarts)
+ case json.Number:
+ if r, err := restarts.Int64(); err == nil {
+ totalRestarts += r
+ }
+ }
+ }
+
+ return fmt.Sprintf("%d/%d", readyCount, totalContainers), totalRestarts
+}
+
+func filterLogLines(logs string, keywords []string) string {
+ var matched []string
+ for _, line := range strings.Split(logs, "\n") {
+ lower := strings.ToLower(line)
+ for _, kw := range keywords {
+ if strings.Contains(lower, strings.ToLower(kw)) {
+ matched = append(matched, line)
+ break
+ }
+ }
+ }
+ return strings.Join(matched, "\n")
+}
+
+// fetchOperatorHealth fetches LVMS operator CSV and pod status
+func fetchOperatorHealth(ctx context.Context, dynamicClient dynamic.Interface, namespace string) string {
+ var result strings.Builder
+ result.WriteString("### Operator Health\n\n")
+
+ // Get CSV status
+ csvList, err := dynamicClient.Resource(csvGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ fmt.Fprintf(&result, "*Error listing CSVs: %v*\n\n", err)
+ } else {
+ var lvmsCSV *unstructured.Unstructured
+ for i := range csvList.Items {
+ csv := &csvList.Items[i]
+ name := csv.GetName()
+ if strings.HasPrefix(name, "lvms-operator") || strings.HasPrefix(name, "lvm-operator") {
+ lvmsCSV = csv
+ break
+ }
+ }
+ if lvmsCSV == nil {
+ result.WriteString("*No LVMS operator CSV found*\n\n")
+ } else {
+ phase, _, _ := unstructured.NestedString(lvmsCSV.Object, "status", "phase")
+ reason, _, _ := unstructured.NestedString(lvmsCSV.Object, "status", "reason")
+ fmt.Fprintf(&result, "**CSV:** %s\n- **Phase:** %s\n- **Reason:** %s\n\n",
+ lvmsCSV.GetName(), valueOrNA(phase), valueOrNA(reason))
+ }
+ }
+
+ // Get operator pods
+ labelSelector := "app.kubernetes.io/name=lvms-operator"
+ podList, err := dynamicClient.Resource(podGVR).Namespace(namespace).List(ctx, metav1.ListOptions{
+ LabelSelector: labelSelector,
+ })
+ if err != nil {
+ fmt.Fprintf(&result, "*Error listing operator pods: %v*\n\n", err)
+ } else if len(podList.Items) == 0 {
+ result.WriteString("*No LVMS operator pods found*\n\n")
+ } else {
+ result.WriteString("**Operator Pods:**\n\n")
+ for _, pod := range podList.Items {
+ phase, _, _ := unstructured.NestedString(pod.Object, "status", "phase")
+ ready, restarts := getPodReadyAndRestarts(&pod)
+ fmt.Fprintf(&result, "- %s: %s (Ready: %s, Restarts: %d)\n",
+ pod.GetName(), phase, ready, restarts)
+ }
+ result.WriteString("\n")
+ }
+
+ return result.String()
+}
+
+// fetchTopoLVMStorageClasses fetches storage classes provisioned by TopoLVM
+func fetchTopoLVMStorageClasses(ctx context.Context, dynamicClient dynamic.Interface) string {
+ var result strings.Builder
+ result.WriteString("### TopoLVM Storage Classes\n\n")
+
+ scList, err := dynamicClient.Resource(storageClassGVR).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ fmt.Fprintf(&result, "*Error listing StorageClasses: %v*\n\n", err)
+ return result.String()
+ }
+
+ var topoLVMSCs []map[string]any
+ for _, sc := range scList.Items {
+ provisioner, _, _ := unstructured.NestedString(sc.Object, "provisioner")
+ if strings.Contains(provisioner, "topolvm") {
+ scData := map[string]any{
+ "name": sc.GetName(),
+ "provisioner": provisioner,
+ "reclaimPolicy": sc.Object["reclaimPolicy"],
+ "volumeBindingMode": sc.Object["volumeBindingMode"],
+ "allowVolumeExpansion": sc.Object["allowVolumeExpansion"],
+ }
+ if params, found, _ := unstructured.NestedStringMap(sc.Object, "parameters"); found {
+ scData["parameters"] = params
+ }
+ topoLVMSCs = append(topoLVMSCs, scData)
+ }
+ }
+
+ if len(topoLVMSCs) == 0 {
+ result.WriteString("*No TopoLVM storage classes found*\n\n")
+ return result.String()
+ }
+
+ yamlStr, err := output.MarshalYaml(topoLVMSCs)
+ if err != nil {
+ fmt.Fprintf(&result, "*Error marshaling storage classes: %v*\n\n", err)
+ return result.String()
+ }
+
+ fmt.Fprintf(&result, "```yaml\n%s```\n\n", yamlStr)
+ return result.String()
+}
+
+// fetchTopoLVMPVCs fetches PVCs using TopoLVM storage classes
+func fetchTopoLVMPVCs(ctx context.Context, dynamicClient dynamic.Interface) string {
+ var result strings.Builder
+ result.WriteString("### PVCs Using TopoLVM\n\n")
+
+ // First get TopoLVM storage class names
+ scList, err := dynamicClient.Resource(storageClassGVR).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ fmt.Fprintf(&result, "*Error listing StorageClasses: %v*\n\n", err)
+ return result.String()
+ }
+
+ topoLVMSCNames := make(map[string]bool)
+ for _, sc := range scList.Items {
+ provisioner, _, _ := unstructured.NestedString(sc.Object, "provisioner")
+ if strings.Contains(provisioner, "topolvm") {
+ topoLVMSCNames[sc.GetName()] = true
+ }
+ }
+
+ if len(topoLVMSCNames) == 0 {
+ result.WriteString("*No TopoLVM storage classes found*\n\n")
+ return result.String()
+ }
+
+ // Get all PVCs
+ pvcList, err := dynamicClient.Resource(pvcGVR).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ fmt.Fprintf(&result, "*Error listing PVCs: %v*\n\n", err)
+ return result.String()
+ }
+
+ var pendingPVCs, boundPVCs []map[string]any
+ for _, pvc := range pvcList.Items {
+ scName, _, _ := unstructured.NestedString(pvc.Object, "spec", "storageClassName")
+ if !topoLVMSCNames[scName] {
+ continue
+ }
+
+ phase, _, _ := unstructured.NestedString(pvc.Object, "status", "phase")
+ capacity, _, _ := unstructured.NestedString(pvc.Object, "status", "capacity", "storage")
+
+ pvcData := map[string]any{
+ "namespace": pvc.GetNamespace(),
+ "name": pvc.GetName(),
+ "storageClass": scName,
+ "phase": phase,
+ "capacity": capacity,
+ }
+
+ if phase == "Pending" {
+ pendingPVCs = append(pendingPVCs, pvcData)
+ } else {
+ boundPVCs = append(boundPVCs, pvcData)
+ }
+ }
+
+ if len(pendingPVCs) > 0 {
+ result.WriteString("**Pending PVCs (needs attention):**\n\n")
+ yamlStr, _ := output.MarshalYaml(pendingPVCs)
+ fmt.Fprintf(&result, "```yaml\n%s```\n\n", yamlStr)
+ }
+
+ if len(boundPVCs) > 0 {
+ // Limit bound PVCs to 20 most recent
+ if len(boundPVCs) > 20 {
+ boundPVCs = boundPVCs[len(boundPVCs)-20:]
+ result.WriteString("**Bound PVCs (showing last 20):**\n\n")
+ } else {
+ result.WriteString("**Bound PVCs:**\n\n")
+ }
+ yamlStr, _ := output.MarshalYaml(boundPVCs)
+ fmt.Fprintf(&result, "```yaml\n%s```\n\n", yamlStr)
+ }
+
+ if len(pendingPVCs) == 0 && len(boundPVCs) == 0 {
+ result.WriteString("*No PVCs found using TopoLVM storage classes*\n\n")
+ }
+
+ return result.String()
+}
diff --git a/pkg/toolsets/lvms/toolset.go b/pkg/toolsets/lvms/toolset.go
new file mode 100644
index 000000000..bebda8d7b
--- /dev/null
+++ b/pkg/toolsets/lvms/toolset.go
@@ -0,0 +1,48 @@
+package lvms
+
+import (
+ "github.com/containers/kubernetes-mcp-server/pkg/api"
+ "github.com/containers/kubernetes-mcp-server/pkg/toolsets"
+)
+
+// Toolset provides LVMS (Logical Volume Manager Storage) prompts for troubleshooting
+// storage issues on OpenShift clusters. LVMS resources are managed via the core
+// toolset's generic resource tools. This toolset provides a troubleshooting prompt
+// that gathers diagnostic data and explains LVMS-specific fields like vg_attr flags.
+type Toolset struct{}
+
+var _ api.Toolset = (*Toolset)(nil)
+
+// GetName returns the name of the toolset.
+func (t *Toolset) GetName() string {
+ return "lvms"
+}
+
+// GetDescription returns a human-readable description of the toolset.
+func (t *Toolset) GetDescription() string {
+ return "LVMS (Logical Volume Manager Storage) troubleshooting prompts for diagnosing storage issues"
+}
+
+// GetTools returns nil — LVMS resources are managed via the core toolset's generic resource tools.
+func (t *Toolset) GetTools(_ api.FilteringProvider) []api.ServerTool {
+ return nil
+}
+
+// GetPrompts returns the prompts provided by this toolset.
+func (t *Toolset) GetPrompts() []api.ServerPrompt {
+ return append(initLVMSTroubleshoot(), initLVMSCapacity()...)
+}
+
+// GetResources returns the resources provided by this toolset.
+func (t *Toolset) GetResources() []api.ServerResource {
+ return nil
+}
+
+// GetResourceTemplates returns the resource templates provided by this toolset.
+func (t *Toolset) GetResourceTemplates() []api.ServerResourceTemplate {
+ return nil
+}
+
+func init() {
+ toolsets.Register(&Toolset{})
+}
diff --git a/pkg/toolsets/lvms/toolset_test.go b/pkg/toolsets/lvms/toolset_test.go
new file mode 100644
index 000000000..741ffac52
--- /dev/null
+++ b/pkg/toolsets/lvms/toolset_test.go
@@ -0,0 +1,81 @@
+package lvms
+
+import (
+ "testing"
+
+ "github.com/containers/kubernetes-mcp-server/pkg/api"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestToolsetImplementsInterface(t *testing.T) {
+ var _ api.Toolset = (*Toolset)(nil)
+}
+
+func TestToolsetGetName(t *testing.T) {
+ toolset := &Toolset{}
+ assert.Equal(t, "lvms", toolset.GetName())
+}
+
+func TestToolsetGetDescription(t *testing.T) {
+ toolset := &Toolset{}
+ desc := toolset.GetDescription()
+ assert.Contains(t, desc, "LVMS")
+ assert.Contains(t, desc, "troubleshooting")
+}
+
+func TestToolsetGetTools(t *testing.T) {
+ toolset := &Toolset{}
+ tools := toolset.GetTools(nil)
+
+ // LVMS is a prompts-only toolset - no tools needed.
+ // All LVMS operations are covered by:
+ // - Core tools (resources_list, resources_get) for LVMS CRD queries
+ // - nodes_debug_exec (cluster-diagnostics) for raw LVM commands on nodes
+ assert.Nil(t, tools, "LVMS toolset should have no tools - it's a prompts-only toolset")
+}
+
+func TestToolsetGetPrompts(t *testing.T) {
+ toolset := &Toolset{}
+ prompts := toolset.GetPrompts()
+
+ require.NotEmpty(t, prompts, "toolset should return prompts")
+ require.Len(t, prompts, 2, "toolset should return 2 prompts")
+
+ // Verify lvms-troubleshoot prompt
+ var foundTroubleshoot, foundCapacity bool
+ for _, prompt := range prompts {
+ switch prompt.Prompt.Name {
+ case "lvms-troubleshoot":
+ foundTroubleshoot = true
+ assert.Equal(t, "LVMS Troubleshoot", prompt.Prompt.Title)
+ assert.NotEmpty(t, prompt.Prompt.Description)
+ assert.NotNil(t, prompt.Handler)
+ require.Len(t, prompt.Prompt.Arguments, 2)
+ assert.Equal(t, "namespace", prompt.Prompt.Arguments[0].Name)
+ assert.Equal(t, "node", prompt.Prompt.Arguments[1].Name)
+
+ case "lvms-capacity":
+ foundCapacity = true
+ assert.Equal(t, "LVMS Capacity Check", prompt.Prompt.Title)
+ assert.NotEmpty(t, prompt.Prompt.Description)
+ assert.NotNil(t, prompt.Handler)
+ require.Len(t, prompt.Prompt.Arguments, 1)
+ assert.Equal(t, "namespace", prompt.Prompt.Arguments[0].Name)
+ }
+ }
+ assert.True(t, foundTroubleshoot, "lvms-troubleshoot prompt should be registered")
+ assert.True(t, foundCapacity, "lvms-capacity prompt should be registered")
+}
+
+func TestToolsetGetResources(t *testing.T) {
+ toolset := &Toolset{}
+ resources := toolset.GetResources()
+ assert.Nil(t, resources, "LVMS toolset currently has no resources")
+}
+
+func TestToolsetGetResourceTemplates(t *testing.T) {
+ toolset := &Toolset{}
+ templates := toolset.GetResourceTemplates()
+ assert.Nil(t, templates, "LVMS toolset currently has no resource templates")
+}