feat: improved prompt efficiency for OpenShift resources

This commit is contained in:
Marc Nuri
2025-03-07 11:56:57 +01:00
parent f712653853
commit d7075f2c78
4 changed files with 67 additions and 10 deletions

View File

@@ -0,0 +1,18 @@
package kubernetes
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func (k *Kubernetes) IsOpenShift(ctx context.Context) bool {
if _, err := k.dynamicClient.Resource(schema.GroupVersionResource{
Group: "project.openshift.io",
Version: "v1",
Resource: "projects",
}).List(ctx, metav1.ListOptions{Limit: 1}); err == nil {
return true
}
return false
}

View File

@@ -174,21 +174,29 @@ func (c *mcpContext) withEnvTest() {
// inOpenShift sets up the kubernetes environment to seem to be running OpenShift
func (c *mcpContext) inOpenShift() func() {
c.withKubeConfig(envTestRestConfig)
return c.crdApply(`
crdTemplate := `
{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": {"name": "routes.route.openshift.io"},
"metadata": {"name": "%s"},
"spec": {
"group": "route.openshift.io",
"group": "%s",
"versions": [{
"name": "v1","served": true,"storage": true,
"schema": {"openAPIV3Schema": {"type": "object","x-kubernetes-preserve-unknown-fields": true}}
}],
"scope": "Namespaced",
"names": {"plural": "routes","singular": "route","kind": "Route"}
"names": {"plural": "%s","singular": "%s","kind": "%s"}
}
}`)
}`
removeProjects := c.crdApply(fmt.Sprintf(crdTemplate, "projects.project.openshift.io", "project.openshift.io",
"projects", "project", "Project"))
removeRoutes := c.crdApply(fmt.Sprintf(crdTemplate, "routes.route.openshift.io", "route.openshift.io",
"routes", "route", "Route"))
return func() {
removeProjects()
removeRoutes()
}
}
// newKubernetesClient creates a new Kubernetes client with the envTest kubeconfig
@@ -224,7 +232,7 @@ func (c *mcpContext) crdApply(resource string) func() {
}
c.crdWaitUntilReady(crd.Name)
return func() {
err = apiExtensionsV1Client.CustomResourceDefinitions().Delete(c.ctx, "routes.route.openshift.io", metav1.DeleteOptions{})
err = apiExtensionsV1Client.CustomResourceDefinitions().Delete(c.ctx, crd.Name, metav1.DeleteOptions{})
if err != nil {
panic(fmt.Errorf("failed to delete CRD %v", err))
}

View File

@@ -2,6 +2,7 @@ package mcp
import (
"github.com/mark3labs/mcp-go/mcp"
"strings"
"testing"
)
@@ -41,3 +42,28 @@ func TestTools(t *testing.T) {
}
})
}
func TestToolsInOpenShift(t *testing.T) {
testCase(t, func(c *mcpContext) {
defer c.inOpenShift()() // n.b. two sets of parentheses to invoke the first function
c.mcpServer.server.AddTools(c.mcpServer.initResources()...)
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
t.Run("ListTools returns tools", func(t *testing.T) {
if err != nil {
t.Fatalf("call ListTools failed %v", err)
return
}
})
t.Run("ListTools has resources_list tool with OpenShift hint", func(t *testing.T) {
if tools.Tools[10].Name != "resources_list" {
t.Fatalf("tool resources_list not found")
return
}
if !strings.Contains(tools.Tools[10].Description, ", route.openshift.io/v1 Route") {
t.Fatalf("tool resources_list does not have OpenShift hint, got %s", tools.Tools[9].Description)
return
}
})
})
}

View File

@@ -10,10 +10,15 @@ import (
)
func (s *Server) initResources() []server.ServerTool {
commonApiVersion := "v1 Pod, v1 Service, apps/v1 Deployment, networking.k8s.io/v1 Ingress"
if s.k.IsOpenShift(context.Background()) {
commonApiVersion += ", route.openshift.io/v1 Route"
}
commonApiVersion = fmt.Sprintf("(common apiVersion and kind include: %s)", commonApiVersion)
return []server.ServerTool{
{mcp.NewTool("resources_list",
mcp.WithDescription("List Kubernetes resources and objects in the current cluster by providing their apiVersion and kind and optionally the namespace\n"+
"(typical apiVersion and kind include: v1 Pod, v1 Service, apps/v1 Deployment, networking.k8s.io/v1 Ingress)"),
commonApiVersion),
mcp.WithString("apiVersion",
mcp.Description("apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"),
mcp.Required(),
@@ -26,7 +31,7 @@ func (s *Server) initResources() []server.ServerTool {
mcp.Description("Optional Namespace to retrieve the namespaced resources from (ignored in case of cluster scoped resources). If not provided, will list resources from all namespaces"))), s.resourcesList},
{mcp.NewTool("resources_get",
mcp.WithDescription("Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+
"(typical apiVersion and kind include: v1 Pod, v1 Service, apps/v1 Deployment, networking.k8s.io/v1 Ingress)"),
commonApiVersion),
mcp.WithString("apiVersion",
mcp.Description("apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"),
mcp.Required(),
@@ -42,7 +47,7 @@ func (s *Server) initResources() []server.ServerTool {
), s.resourcesGet},
{mcp.NewTool("resources_create_or_update",
mcp.WithDescription("Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource\n"+
"(typical apiVersion and kind include: v1 Pod, v1 Service, apps/v1 Deployment, networking.k8s.io/v1 Ingress)"),
commonApiVersion),
mcp.WithString("resource",
mcp.Description("A JSON or YAML containing a representation of the Kubernetes resource. Should include top-level fields such as apiVersion,kind,metadata, and spec"),
mcp.Required(),
@@ -50,7 +55,7 @@ func (s *Server) initResources() []server.ServerTool {
), s.resourcesCreateOrUpdate},
{mcp.NewTool("resources_delete",
mcp.WithDescription("Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+
"(typical apiVersion and kind include: v1 Pod, v1 Service, apps/v1 Deployment, networking.k8s.io/v1 Ingress)"),
commonApiVersion),
mcp.WithString("apiVersion",
mcp.Description("apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"),
mcp.Required(),