mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
feat: support for listing namespaces and OpenShift projects
This commit is contained in:
18
pkg/kubernetes/namespaces.go
Normal file
18
pkg/kubernetes/namespaces.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func (k *Kubernetes) NamespacesList(ctx context.Context) (string, error) {
|
||||
return k.ResourcesList(ctx, &schema.GroupVersionKind{
|
||||
Group: "", Version: "v1", Kind: "Namespace",
|
||||
}, "")
|
||||
}
|
||||
|
||||
func (k *Kubernetes) ProjectsList(ctx context.Context) (string, error) {
|
||||
return k.ResourcesList(ctx, &schema.GroupVersionKind{
|
||||
Group: "project.openshift.io", Version: "v1", Kind: "Project",
|
||||
}, "")
|
||||
}
|
||||
@@ -192,14 +192,14 @@ func (c *mcpContext) inOpenShift() func() {
|
||||
"name": "v1","served": true,"storage": true,
|
||||
"schema": {"openAPIV3Schema": {"type": "object","x-kubernetes-preserve-unknown-fields": true}}
|
||||
}],
|
||||
"scope": "Namespaced",
|
||||
"scope": "%s",
|
||||
"names": {"plural": "%s","singular": "%s","kind": "%s"}
|
||||
}
|
||||
}`
|
||||
removeProjects := c.crdApply(fmt.Sprintf(crdTemplate, "projects.project.openshift.io", "project.openshift.io",
|
||||
"projects", "project", "Project"))
|
||||
"Cluster", "projects", "project", "Project"))
|
||||
removeRoutes := c.crdApply(fmt.Sprintf(crdTemplate, "routes.route.openshift.io", "route.openshift.io",
|
||||
"routes", "route", "Route"))
|
||||
"Namespaced", "routes", "route", "Route"))
|
||||
return func() {
|
||||
removeProjects()
|
||||
removeRoutes()
|
||||
|
||||
@@ -40,6 +40,7 @@ func (s *Server) reloadKubernetesClient() error {
|
||||
s.server.SetTools(slices.Concat(
|
||||
s.initConfiguration(),
|
||||
s.initEvents(),
|
||||
s.initNamespaces(),
|
||||
s.initPods(),
|
||||
s.initResources(),
|
||||
)...)
|
||||
|
||||
@@ -50,6 +50,7 @@ func TestTools(t *testing.T) {
|
||||
expectedNames := []string{
|
||||
"configuration_view",
|
||||
"events_list",
|
||||
"namespaces_list",
|
||||
"pods_list",
|
||||
"pods_list_in_namespace",
|
||||
"pods_get",
|
||||
@@ -87,12 +88,20 @@ 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.initNamespaces()...)
|
||||
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 contains projects_list tool", func(t *testing.T) {
|
||||
idx := slices.IndexFunc(tools.Tools, func(tool mcp.Tool) bool {
|
||||
return tool.Name == "projects_list"
|
||||
})
|
||||
if idx == -1 {
|
||||
t.Fatalf("tool projects_list not found")
|
||||
}
|
||||
})
|
||||
t.Run("ListTools has resources_list tool with OpenShift hint", func(t *testing.T) {
|
||||
@@ -101,11 +110,9 @@ func TestToolsInOpenShift(t *testing.T) {
|
||||
})
|
||||
if idx == -1 {
|
||||
t.Fatalf("tool resources_list not found")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(tools.Tools[idx].Description, ", route.openshift.io/v1 Route") {
|
||||
t.Fatalf("tool resources_list does not have OpenShift hint, got %s", tools.Tools[9].Description)
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
42
pkg/mcp/namespaces.go
Normal file
42
pkg/mcp/namespaces.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func (s *Server) initNamespaces() []server.ServerTool {
|
||||
ret := make([]server.ServerTool, 0)
|
||||
if s.k.IsOpenShift(context.Background()) {
|
||||
ret = append(ret, server.ServerTool{
|
||||
Tool: mcp.NewTool("projects_list",
|
||||
mcp.WithDescription("List all the OpenShift projects in the current cluster"),
|
||||
), Handler: s.projectsList,
|
||||
})
|
||||
} else {
|
||||
ret = append(ret, server.ServerTool{
|
||||
Tool: mcp.NewTool("namespaces_list",
|
||||
mcp.WithDescription("List all the Kubernetes namespaces in the current cluster"),
|
||||
), Handler: s.namespacesList,
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Server) namespacesList(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
ret, err := s.k.NamespacesList(ctx)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to list namespaces: %v", err)
|
||||
}
|
||||
return NewTextResult(ret, err), nil
|
||||
}
|
||||
|
||||
func (s *Server) projectsList(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
ret, err := s.k.ProjectsList(ctx)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to list projects: %v", err)
|
||||
}
|
||||
return NewTextResult(ret, err), nil
|
||||
}
|
||||
91
pkg/mcp/namespaces_test.go
Normal file
91
pkg/mcp/namespaces_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
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"
|
||||
"sigs.k8s.io/yaml"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNamespacesList(t *testing.T) {
|
||||
testCase(t, func(c *mcpContext) {
|
||||
c.withEnvTest()
|
||||
toolResult, err := c.callTool("namespaces_list", map[string]interface{}{})
|
||||
t.Run("namespaces_list returns namespace list", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
if toolResult.IsError {
|
||||
t.Fatalf("call tool failed")
|
||||
}
|
||||
})
|
||||
var decoded []unstructured.Unstructured
|
||||
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded)
|
||||
t.Run("namespaces_list has yaml content", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("invalid tool result content %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("namespaces_list returns at least 3 items", func(t *testing.T) {
|
||||
if len(decoded) < 3 {
|
||||
t.Errorf("invalid namespace count, expected at least 3, got %v", len(decoded))
|
||||
}
|
||||
for _, expectedNamespace := range []string{"default", "ns-1", "ns-2"} {
|
||||
idx := slices.IndexFunc(decoded, func(ns unstructured.Unstructured) bool {
|
||||
return ns.GetName() == expectedNamespace
|
||||
})
|
||||
if idx == -1 {
|
||||
t.Errorf("namespace %s not found in the list", expectedNamespace)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestProjectsListInOpenShift(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.initNamespaces()...)
|
||||
dynamicClient := dynamic.NewForConfigOrDie(envTestRestConfig)
|
||||
_, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "project.openshift.io", Version: "v1", Resource: "projects"}).
|
||||
Create(c.ctx, &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "project.openshift.io/v1",
|
||||
"kind": "Project",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "an-openshift-project",
|
||||
},
|
||||
}}, metav1.CreateOptions{})
|
||||
println(err)
|
||||
toolResult, err := c.callTool("projects_list", map[string]interface{}{})
|
||||
t.Run("projects_list returns project list", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
if toolResult.IsError {
|
||||
t.Fatalf("call tool failed")
|
||||
}
|
||||
})
|
||||
var decoded []unstructured.Unstructured
|
||||
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded)
|
||||
t.Run("projects_list has yaml content", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("invalid tool result content %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("projects_list returns at least 1 items", func(t *testing.T) {
|
||||
if len(decoded) < 1 {
|
||||
t.Errorf("invalid project count, expected at least 1, got %v", len(decoded))
|
||||
}
|
||||
idx := slices.IndexFunc(decoded, func(ns unstructured.Unstructured) bool {
|
||||
return ns.GetName() == "an-openshift-project"
|
||||
})
|
||||
if idx == -1 {
|
||||
t.Errorf("namespace %s not found in the list", "an-openshift-project")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user