Skip to content

Commit 860de99

Browse files
committed
feat(kubevirt): Add vm_delete tool
Introduces a dedicated vm_delete tool to simplify VirtualMachine deletion. Based on agent evaluation results, multiple agents were searching for a vm_delete tool instead of using the generic resources_delete tool. Assisted-By: Claude <[email protected]> Signed-off-by: Lee Yarwood <[email protected]>
1 parent 256dbef commit 860de99

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed

pkg/toolsets/kubevirt/toolset.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
88
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
99
vm_create "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/create"
10+
vm_delete "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/delete"
1011
vm_pause "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/pause"
1112
vm_start "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/start"
1213
vm_stop "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/stop"
@@ -28,6 +29,7 @@ func (t *Toolset) GetDescription() string {
2829
func (t *Toolset) GetTools(o internalk8s.Openshift) []api.ServerTool {
2930
return slices.Concat(
3031
vm_create.Tools(),
32+
vm_delete.Tools(),
3133
vm_pause.Tools(),
3234
vm_start.Tools(),
3335
vm_stop.Tools(),
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package delete
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/containers/kubernetes-mcp-server/pkg/api"
7+
"github.com/google/jsonschema-go/jsonschema"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/runtime/schema"
10+
"k8s.io/client-go/dynamic"
11+
"k8s.io/utils/ptr"
12+
)
13+
14+
func Tools() []api.ServerTool {
15+
return []api.ServerTool{
16+
{
17+
Tool: api.Tool{
18+
Name: "vm_delete",
19+
Description: "Delete a VirtualMachine in the current cluster by providing its namespace and name",
20+
InputSchema: &jsonschema.Schema{
21+
Type: "object",
22+
Properties: map[string]*jsonschema.Schema{
23+
"namespace": {
24+
Type: "string",
25+
Description: "The namespace of the virtual machine",
26+
},
27+
"name": {
28+
Type: "string",
29+
Description: "The name of the virtual machine to delete",
30+
},
31+
},
32+
Required: []string{"namespace", "name"},
33+
},
34+
Annotations: api.ToolAnnotations{
35+
Title: "Virtual Machine: Delete",
36+
ReadOnlyHint: ptr.To(false),
37+
DestructiveHint: ptr.To(true),
38+
IdempotentHint: ptr.To(true),
39+
OpenWorldHint: ptr.To(false),
40+
},
41+
},
42+
Handler: deleteVM,
43+
},
44+
}
45+
}
46+
47+
func deleteVM(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
48+
// Parse required parameters
49+
namespace, err := params.GetRequiredString("namespace")
50+
if err != nil {
51+
return api.NewToolCallResult("", err), nil
52+
}
53+
54+
name, err := params.GetRequiredString("name")
55+
if err != nil {
56+
return api.NewToolCallResult("", err), nil
57+
}
58+
59+
// Get dynamic client
60+
restConfig := params.RESTConfig()
61+
if restConfig == nil {
62+
return api.NewToolCallResult("", fmt.Errorf("failed to get REST config")), nil
63+
}
64+
65+
dynamicClient, err := dynamic.NewForConfig(restConfig)
66+
if err != nil {
67+
return api.NewToolCallResult("", fmt.Errorf("failed to create dynamic client: %w", err)), nil
68+
}
69+
70+
// Define the VirtualMachine GVR
71+
gvr := schema.GroupVersionResource{
72+
Group: "kubevirt.io",
73+
Version: "v1",
74+
Resource: "virtualmachines",
75+
}
76+
77+
// Delete the VM
78+
err = dynamicClient.Resource(gvr).Namespace(namespace).Delete(
79+
params.Context,
80+
name,
81+
metav1.DeleteOptions{},
82+
)
83+
if err != nil {
84+
return api.NewToolCallResult("", fmt.Errorf("failed to delete VirtualMachine: %w", err)), nil
85+
}
86+
87+
return api.NewToolCallResult(fmt.Sprintf("# VirtualMachine deleted successfully\nVirtualMachine '%s' in namespace '%s' has been deleted.", name, namespace), nil), nil
88+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package delete_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/containers/kubernetes-mcp-server/pkg/api"
8+
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
9+
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/delete"
10+
)
11+
12+
type mockToolCallRequest struct {
13+
arguments map[string]interface{}
14+
}
15+
16+
func (m *mockToolCallRequest) GetArguments() map[string]any {
17+
return m.arguments
18+
}
19+
20+
func TestDeleteParameterValidation(t *testing.T) {
21+
tests := []struct {
22+
name string
23+
args map[string]interface{}
24+
wantErr bool
25+
errMsg string
26+
}{
27+
{
28+
name: "missing namespace parameter",
29+
args: map[string]interface{}{
30+
"name": "test-vm",
31+
},
32+
wantErr: true,
33+
errMsg: "namespace parameter required",
34+
},
35+
{
36+
name: "missing name parameter",
37+
args: map[string]interface{}{
38+
"namespace": "test-ns",
39+
},
40+
wantErr: true,
41+
errMsg: "name parameter required",
42+
},
43+
{
44+
name: "invalid namespace type",
45+
args: map[string]interface{}{
46+
"namespace": 123,
47+
"name": "test-vm",
48+
},
49+
wantErr: true,
50+
errMsg: "namespace parameter must be a string",
51+
},
52+
{
53+
name: "invalid name type",
54+
args: map[string]interface{}{
55+
"namespace": "test-ns",
56+
"name": 456,
57+
},
58+
wantErr: true,
59+
errMsg: "name parameter must be a string",
60+
},
61+
{
62+
name: "valid parameters - cluster interaction expected",
63+
args: map[string]interface{}{
64+
"namespace": "test-ns",
65+
"name": "test-vm",
66+
},
67+
wantErr: true, // Will fail due to missing cluster connection, but parameters are valid
68+
},
69+
}
70+
71+
// Get the tool through the public API
72+
tools := delete.Tools()
73+
if len(tools) != 1 {
74+
t.Fatalf("Expected 1 tool, got %d", len(tools))
75+
}
76+
vmDeleteTool := tools[0]
77+
78+
for _, tt := range tests {
79+
t.Run(tt.name, func(t *testing.T) {
80+
params := api.ToolHandlerParams{
81+
Context: context.Background(),
82+
Kubernetes: &internalk8s.Kubernetes{},
83+
ToolCallRequest: &mockToolCallRequest{arguments: tt.args},
84+
}
85+
86+
// Call through the public Handler interface
87+
result, err := vmDeleteTool.Handler(params)
88+
if err != nil {
89+
t.Errorf("Handler() unexpected Go error: %v", err)
90+
return
91+
}
92+
93+
if result == nil {
94+
t.Error("Expected non-nil result")
95+
return
96+
}
97+
98+
// For parameter validation errors, check the error message
99+
if tt.wantErr && tt.errMsg != "" {
100+
if result.Error == nil {
101+
t.Error("Expected error in result.Error, got nil")
102+
return
103+
}
104+
if result.Error.Error() != tt.errMsg {
105+
t.Errorf("Expected error message %q, got %q", tt.errMsg, result.Error.Error())
106+
}
107+
}
108+
})
109+
}
110+
}

0 commit comments

Comments
 (0)