Skip to content
1 change: 1 addition & 0 deletions internal/tools/update-readme/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/core"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/helm"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt"
)

type OpenShift struct{}
Expand Down
53 changes: 53 additions & 0 deletions pkg/api/toolsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"context"
"encoding/json"
"fmt"

internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/output"
Expand Down Expand Up @@ -71,6 +72,58 @@ type ToolHandlerParams struct {
ListOutput output.Output
}

// GetRequiredString extracts a required string parameter from the tool call arguments.
// Returns an error if the parameter is missing or not a string.
func (p ToolHandlerParams) GetRequiredString(key string) (string, error) {
args := p.GetArguments()
val, ok := args[key]
if !ok {
return "", fmt.Errorf("%s parameter required", key)
}
str, ok := val.(string)
if !ok {
return "", fmt.Errorf("%s parameter must be a string", key)
}
return str, nil
}

// GetOptionalString extracts an optional string parameter from the tool call arguments.
// Returns the provided default value if the parameter is missing or not a string.
// If no default value is provided, returns an empty string.
func (p ToolHandlerParams) GetOptionalString(key string, defaultValue ...string) string {
args := p.GetArguments()
val, ok := args[key]
if !ok {
if len(defaultValue) > 0 {
return defaultValue[0]
}
return ""
}
str, ok := val.(string)
if !ok {
if len(defaultValue) > 0 {
return defaultValue[0]
}
return ""
}
return str
}

// GetOptionalBool extracts an optional boolean parameter from the tool call arguments.
// Returns false if the parameter is missing or not a boolean.
func (p ToolHandlerParams) GetOptionalBool(key string) bool {
args := p.GetArguments()
val, ok := args[key]
if !ok {
return false
}
b, ok := val.(bool)
if !ok {
return false
}
return b
}

type ToolHandlerFunc func(params ToolHandlerParams) (*ToolCallResult, error)

type Tool struct {
Expand Down
2 changes: 1 addition & 1 deletion pkg/kubernetes-mcp-server/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestToolsets(t *testing.T) {
rootCmd := NewMCPServer(ioStreams)
rootCmd.SetArgs([]string{"--help"})
o, err := captureOutput(rootCmd.Execute) // --help doesn't use logger/klog, cobra prints directly to stdout
if !strings.Contains(o, "Comma-separated list of MCP toolsets to use (available toolsets: config, core, helm, kiali).") {
if !strings.Contains(o, "Comma-separated list of MCP toolsets to use (available toolsets: config, core, helm, kiali, kubevirt).") {
t.Fatalf("Expected all available toolsets, got %s %v", o, err)
}
})
Expand Down
11 changes: 10 additions & 1 deletion pkg/kubernetes/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package kubernetes
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"regexp"
"strings"

"k8s.io/apimachinery/pkg/runtime"

"github.com/containers/kubernetes-mcp-server/pkg/version"
authv1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -46,6 +47,10 @@ func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersion
}

func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) (*unstructured.Unstructured, error) {
if k.manager == nil {
return nil, fmt.Errorf("kubernetes manager not initialized")
}

gvr, err := k.resourceFor(gvk)
if err != nil {
return nil, err
Expand Down Expand Up @@ -73,6 +78,10 @@ func (k *Kubernetes) ResourcesCreateOrUpdate(ctx context.Context, resource strin
}

func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) error {
if k.manager == nil {
return fmt.Errorf("kubernetes manager not initialized")
}

gvr, err := k.resourceFor(gvk)
if err != nil {
return err
Expand Down
14 changes: 10 additions & 4 deletions pkg/mcp/modules.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package mcp

import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/config"
import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/core"
import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/helm"
import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali"
import (
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/config"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/core"

_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/helm"

_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali"

_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt"
)
40 changes: 40 additions & 0 deletions pkg/toolsets/kubevirt/toolset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package kubevirt

import (
"slices"

"github.com/containers/kubernetes-mcp-server/pkg/api"
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
vm_create "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/create"
vm_delete "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/delete"
vm_start "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/start"
vm_stop "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/stop"
vm_troubleshoot "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/troubleshoot"
)

type Toolset struct{}

var _ api.Toolset = (*Toolset)(nil)

func (t *Toolset) GetName() string {
return "kubevirt"
}

func (t *Toolset) GetDescription() string {
return "KubeVirt virtual machine management tools"
}

func (t *Toolset) GetTools(o internalk8s.Openshift) []api.ServerTool {
return slices.Concat(
vm_create.Tools(),
vm_delete.Tools(),
vm_start.Tools(),
vm_stop.Tools(),
vm_troubleshoot.Tools(),
)
}

func init() {
toolsets.Register(&Toolset{})
}
99 changes: 99 additions & 0 deletions pkg/toolsets/kubevirt/vm/create/plan.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# VirtualMachine Creation Plan

**IMPORTANT**: Always use `runStrategy` instead of the deprecated `running` field when creating VirtualMachines.

Use the `resources_create_or_update` tool with the following YAML:

```yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: {{.Name}}
namespace: {{.Namespace}}
spec:
runStrategy: Halted
{{- if .Instancetype}}
instancetype:
name: {{.Instancetype}}
kind: VirtualMachineClusterInstancetype
{{- end}}
{{- if .Preference}}
preference:
name: {{.Preference}}
kind: VirtualMachineClusterPreference
{{- end}}
{{- if .UseDataSource}}
dataVolumeTemplates:
- metadata:
name: {{.Name}}-rootdisk
spec:
sourceRef:
kind: DataSource
name: {{.DataSourceName}}
namespace: {{.DataSourceNamespace}}
storage:
resources:
requests:
storage: 30Gi
{{- end}}
template:
spec:
domain:
devices:
disks:
- name: {{.Name}}-rootdisk
{{- if not .Instancetype}}
memory:
guest: 2Gi
{{- end}}
volumes:
- name: {{.Name}}-rootdisk
{{- if .UseDataSource}}
dataVolume:
name: {{.Name}}-rootdisk
{{- else}}
containerDisk:
image: {{.ContainerDisk}}
{{- end}}
```

## Run Strategy Options

The VM is created with `runStrategy: Halted` (stopped state). You can modify the `runStrategy` field to control the VM's execution:

- **`Halted`** - VM is stopped and will not run
- **`Always`** - VM should always be running (restarts automatically)
- **`RerunOnFailure`** - Restart the VM only if it fails
- **`Manual`** - Manual start/stop control via `virtctl start/stop`
- **`Once`** - Run the VM once, then stop when it terminates

To start the VM after creation, change `runStrategy: Halted` to `runStrategy: Always` or use the Manual strategy and start it with virtctl.

## Verification

After creating the VirtualMachine, verify it was created successfully:

Use the `resources_get` tool:
- **apiVersion**: `kubevirt.io/v1`
- **kind**: `VirtualMachine`
- **namespace**: `{{.Namespace}}`
- **name**: `{{.Name}}`

Check the resource details for any warnings or errors in the status conditions.

## Troubleshooting

If the VirtualMachine fails to create or start:

1. **Check the VM resource details and events**:
- Use `resources_get` tool with apiVersion `kubevirt.io/v1`, kind `VirtualMachine`, namespace `{{.Namespace}}`, name `{{.Name}}`
- Look for error messages in the status conditions

2. **Verify instance type exists** (if specified):
- Use `resources_get` tool with apiVersion `instancetype.kubevirt.io/v1beta1`, kind `VirtualMachineClusterInstancetype`, name `{{.Instancetype}}`

3. **Verify preference exists** (if specified):
- Use `resources_get` tool with apiVersion `instancetype.kubevirt.io/v1beta1`, kind `VirtualMachineClusterPreference`, name `{{.Preference}}`

4. **Check KubeVirt installation**:
- Use `pods_list` tool with namespace `kubevirt`
Loading