package mcp import ( "encoding/json" "strconv" "testing" "github.com/containers/kubernetes-mcp-server/internal/test" "github.com/containers/kubernetes-mcp-server/pkg/api" configuration "github.com/containers/kubernetes-mcp-server/pkg/config" "github.com/containers/kubernetes-mcp-server/pkg/toolsets" "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/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/suite" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type ToolsetsSuite struct { suite.Suite originalToolsets []api.Toolset *test.MockServer *test.McpClient Cfg *configuration.StaticConfig mcpServer *Server } func (s *ToolsetsSuite) SetupTest() { s.originalToolsets = toolsets.Toolsets() s.MockServer = test.NewMockServer() s.Cfg = configuration.Default() s.Cfg.KubeConfig = s.KubeconfigFile(s.T()) } func (s *ToolsetsSuite) TearDownTest() { toolsets.Clear() for _, toolset := range s.originalToolsets { toolsets.Register(toolset) } s.MockServer.Close() } func (s *ToolsetsSuite) TearDownSubTest() { if s.McpClient != nil { s.McpClient.Close() } if s.mcpServer != nil { s.mcpServer.Close() } } func (s *ToolsetsSuite) TestNoToolsets() { s.Run("No toolsets registered", func() { toolsets.Clear() s.Cfg.Toolsets = []string{} s.InitMcpClient() tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{}) s.Run("ListTools returns no tools", func() { s.NotNil(tools, "Expected tools from ListTools") s.NoError(err, "Expected no error from ListTools") s.Empty(tools.Tools, "Expected no tools from ListTools") }) }) } func (s *ToolsetsSuite) TestDefaultToolsetsTools() { s.Run("Default configuration toolsets", func() { s.InitMcpClient() tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{}) s.Run("ListTools returns tools", func() { s.NotNil(tools, "Expected tools from ListTools") s.NoError(err, "Expected no error from ListTools") }) s.Run("ListTools returns correct Tool metadata", func() { expectedMetadata := test.ReadFile("testdata", "toolsets-full-tools.json") metadata, err := json.MarshalIndent(tools.Tools, "", " ") s.Require().NoErrorf(err, "failed to marshal tools metadata: %v", err) s.JSONEq(expectedMetadata, string(metadata), "tools metadata does not match expected") }) }) } func (s *ToolsetsSuite) TestDefaultToolsetsToolsInOpenShift() { s.Run("Default configuration toolsets in OpenShift", func() { s.Handle(&test.InOpenShiftHandler{}) s.InitMcpClient() tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{}) s.Run("ListTools returns tools", func() { s.NotNil(tools, "Expected tools from ListTools") s.NoError(err, "Expected no error from ListTools") }) s.Run("ListTools returns correct Tool metadata", func() { expectedMetadata := test.ReadFile("testdata", "toolsets-full-tools-openshift.json") metadata, err := json.MarshalIndent(tools.Tools, "", " ") s.Require().NoErrorf(err, "failed to marshal tools metadata: %v", err) s.JSONEq(expectedMetadata, string(metadata), "tools metadata does not match expected") }) }) } func (s *ToolsetsSuite) TestDefaultToolsetsToolsInMultiCluster() { s.Run("Default configuration toolsets in multi-cluster (with 11 clusters)", func() { kubeconfig := s.Kubeconfig() for i := 0; i < 10; i++ { // Add multiple fake contexts to force multi-cluster behavior kubeconfig.Contexts[strconv.Itoa(i)] = clientcmdapi.NewContext() } s.Cfg.KubeConfig = test.KubeconfigFile(s.T(), kubeconfig) s.InitMcpClient() tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{}) s.Run("ListTools returns tools", func() { s.NotNil(tools, "Expected tools from ListTools") s.NoError(err, "Expected no error from ListTools") }) s.Run("ListTools returns correct Tool metadata", func() { expectedMetadata := test.ReadFile("testdata", "toolsets-full-tools-multicluster.json") metadata, err := json.MarshalIndent(tools.Tools, "", " ") s.Require().NoErrorf(err, "failed to marshal tools metadata: %v", err) s.JSONEq(expectedMetadata, string(metadata), "tools metadata does not match expected") }) }) } func (s *ToolsetsSuite) TestDefaultToolsetsToolsInMultiClusterEnum() { s.Run("Default configuration toolsets in multi-cluster (with 2 clusters)", func() { kubeconfig := s.Kubeconfig() // Add additional cluster to force multi-cluster behavior with enum parameter kubeconfig.Contexts["extra-cluster"] = clientcmdapi.NewContext() s.Cfg.KubeConfig = test.KubeconfigFile(s.T(), kubeconfig) s.InitMcpClient() tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{}) s.Run("ListTools returns tools", func() { s.NotNil(tools, "Expected tools from ListTools") s.NoError(err, "Expected no error from ListTools") }) s.Run("ListTools returns correct Tool metadata", func() { expectedMetadata := test.ReadFile("testdata", "toolsets-full-tools-multicluster-enum.json") metadata, err := json.MarshalIndent(tools.Tools, "", " ") s.Require().NoErrorf(err, "failed to marshal tools metadata: %v", err) s.JSONEq(expectedMetadata, string(metadata), "tools metadata does not match expected") }) }) } func (s *ToolsetsSuite) TestGranularToolsetsTools() { testCases := []api.Toolset{ &core.Toolset{}, &config.Toolset{}, &helm.Toolset{}, } for _, testCase := range testCases { s.Run("Toolset "+testCase.GetName(), func() { toolsets.Clear() toolsets.Register(testCase) s.Cfg.Toolsets = []string{testCase.GetName()} s.InitMcpClient() tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{}) s.Run("ListTools returns tools", func() { s.NotNil(tools, "Expected tools from ListTools") s.NoError(err, "Expected no error from ListTools") }) s.Run("ListTools returns correct Tool metadata", func() { expectedMetadata := test.ReadFile("testdata", "toolsets-"+testCase.GetName()+"-tools.json") metadata, err := json.MarshalIndent(tools.Tools, "", " ") s.Require().NoErrorf(err, "failed to marshal tools metadata: %v", err) s.JSONEq(expectedMetadata, string(metadata), "tools metadata does not match expected") }) }) } } func (s *ToolsetsSuite) TestInputSchemaEdgeCases() { //https://github.com/containers/kubernetes-mcp-server/issues/340 s.Run("InputSchema for no-arg tool is object with empty properties", func() { s.InitMcpClient() tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{}) s.Run("ListTools returns tools", func() { s.NotNil(tools, "Expected tools from ListTools") s.NoError(err, "Expected no error from ListTools") }) var namespacesList *mcp.Tool for _, tool := range tools.Tools { if tool.Name == "namespaces_list" { namespacesList = &tool break } } s.Require().NotNil(namespacesList, "Expected namespaces_list from ListTools") s.NotNil(namespacesList.InputSchema.Properties, "Expected namespaces_list.InputSchema.Properties not to be nil") s.Empty(namespacesList.InputSchema.Properties, "Expected namespaces_list.InputSchema.Properties to be empty") }) } func (s *ToolsetsSuite) InitMcpClient() { var err error s.mcpServer, err = NewServer(Configuration{StaticConfig: s.Cfg}) s.Require().NoError(err, "Expected no error creating MCP server") s.McpClient = test.NewMcpClient(s.T(), s.mcpServer.ServeHTTP(nil)) } func TestToolsets(t *testing.T) { suite.Run(t, new(ToolsetsSuite)) }