mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
144 lines
4.7 KiB
Go
144 lines
4.7 KiB
Go
package mcp
|
|
|
|
import (
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"k8s.io/utils/ptr"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/containers/kubernetes-mcp-server/pkg/config"
|
|
)
|
|
|
|
func TestUnrestricted(t *testing.T) {
|
|
testCase(t, func(c *mcpContext) {
|
|
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)
|
|
}
|
|
})
|
|
t.Run("Destructive tools ARE NOT read only", func(t *testing.T) {
|
|
for _, tool := range tools.Tools {
|
|
readOnly := ptr.Deref(tool.Annotations.ReadOnlyHint, false)
|
|
destructive := ptr.Deref(tool.Annotations.DestructiveHint, false)
|
|
if readOnly && destructive {
|
|
t.Errorf("Tool %s is read-only and destructive, which is not allowed", tool.Name)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestReadOnly(t *testing.T) {
|
|
readOnlyServer := func(c *mcpContext) { c.staticConfig = &config.StaticConfig{ReadOnly: true} }
|
|
testCaseWithContext(t, &mcpContext{before: readOnlyServer}, func(c *mcpContext) {
|
|
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)
|
|
}
|
|
})
|
|
t.Run("ListTools returns only read-only tools", func(t *testing.T) {
|
|
for _, tool := range tools.Tools {
|
|
if tool.Annotations.ReadOnlyHint == nil || !*tool.Annotations.ReadOnlyHint {
|
|
t.Errorf("Tool %s is not read-only but should be", tool.Name)
|
|
}
|
|
if tool.Annotations.DestructiveHint != nil && *tool.Annotations.DestructiveHint {
|
|
t.Errorf("Tool %s is destructive but should not be in read-only mode", tool.Name)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestDisableDestructive(t *testing.T) {
|
|
disableDestructiveServer := func(c *mcpContext) { c.staticConfig = &config.StaticConfig{DisableDestructive: true} }
|
|
testCaseWithContext(t, &mcpContext{before: disableDestructiveServer}, func(c *mcpContext) {
|
|
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)
|
|
}
|
|
})
|
|
t.Run("ListTools does not return destructive tools", func(t *testing.T) {
|
|
for _, tool := range tools.Tools {
|
|
if tool.Annotations.DestructiveHint != nil && *tool.Annotations.DestructiveHint {
|
|
t.Errorf("Tool %s is destructive but should not be", tool.Name)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestEnabledTools(t *testing.T) {
|
|
testCaseWithContext(t, &mcpContext{
|
|
staticConfig: &config.StaticConfig{
|
|
EnabledTools: []string{"namespaces_list", "events_list"},
|
|
},
|
|
}, func(c *mcpContext) {
|
|
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)
|
|
}
|
|
})
|
|
t.Run("ListTools returns only explicitly enabled tools", func(t *testing.T) {
|
|
if len(tools.Tools) != 2 {
|
|
t.Fatalf("ListTools should return 2 tools, got %d", len(tools.Tools))
|
|
}
|
|
for _, tool := range tools.Tools {
|
|
if tool.Name != "namespaces_list" && tool.Name != "events_list" {
|
|
t.Errorf("Tool %s is not enabled but should be", tool.Name)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestDisabledTools(t *testing.T) {
|
|
testCaseWithContext(t, &mcpContext{
|
|
staticConfig: &config.StaticConfig{
|
|
DisabledTools: []string{"namespaces_list", "events_list"},
|
|
},
|
|
}, func(c *mcpContext) {
|
|
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)
|
|
}
|
|
})
|
|
t.Run("ListTools does not return disabled tools", func(t *testing.T) {
|
|
for _, tool := range tools.Tools {
|
|
if tool.Name == "namespaces_list" || tool.Name == "events_list" {
|
|
t.Errorf("Tool %s is not disabled but should be", tool.Name)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestToolCallLogging(t *testing.T) {
|
|
testCaseWithContext(t, &mcpContext{logLevel: 5}, func(c *mcpContext) {
|
|
_, _ = c.callTool("configuration_view", map[string]interface{}{
|
|
"minified": false,
|
|
})
|
|
t.Run("Logs tool name", func(t *testing.T) {
|
|
expectedLog := "mcp tool call: configuration_view("
|
|
if !strings.Contains(c.logBuffer.String(), expectedLog) {
|
|
t.Errorf("Expected log to contain '%s', got: %s", expectedLog, c.logBuffer.String())
|
|
}
|
|
})
|
|
t.Run("Logs tool call arguments", func(t *testing.T) {
|
|
expected := `"mcp tool call: configuration_view\((.+)\)"`
|
|
m := regexp.MustCompile(expected).FindStringSubmatch(c.logBuffer.String())
|
|
if len(m) != 2 {
|
|
t.Fatalf("Expected log entry to contain arguments, got %s", c.logBuffer.String())
|
|
}
|
|
if m[1] != "map[minified:false]" {
|
|
t.Errorf("Expected log arguments to be 'map[minified:false]', got %s", m[1])
|
|
}
|
|
})
|
|
})
|
|
}
|