Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/tools/update-readme/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
"slices"
"strings"

"github.com/containers/kubernetes-mcp-server/pkg/api"
"github.com/containers/kubernetes-mcp-server/pkg/config"
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"

_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/config"
Expand All @@ -26,7 +26,7 @@ func (o *OpenShift) IsOpenShift(_ context.Context) bool {
return true
}

var _ internalk8s.Openshift = (*OpenShift)(nil)
var _ api.Openshift = (*OpenShift)(nil)

func main() {
// Snyk reports false positive unless we flow the args through filepath.Clean and filepath.Localize in this specific order
Expand Down
14 changes: 7 additions & 7 deletions pkg/api/config/config.go → pkg/api/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package config
package api

const (
ClusterProviderKubeConfig = "kubeconfig"
Expand All @@ -18,20 +18,20 @@ type ClusterProvider interface {
GetKubeConfigPath() string
}

// Extended is the interface that all configuration extensions must implement.
// ExtendedConfig is the interface that all configuration extensions must implement.
// Each extended config manager registers a factory function to parse its config from TOML primitives
type Extended interface {
type ExtendedConfig interface {
// Validate validates the extended configuration. Returns an error if the configuration is invalid.
Validate() error
}

type ExtendedProvider interface {
type ExtendedConfigProvider interface {
// GetProviderConfig returns the extended configuration for the given provider strategy.
// The boolean return value indicates whether the configuration was found.
GetProviderConfig(strategy string) (Extended, bool)
GetProviderConfig(strategy string) (ExtendedConfig, bool)
// GetToolsetConfig returns the extended configuration for the given toolset name.
// The boolean return value indicates whether the configuration was found.
GetToolsetConfig(name string) (Extended, bool)
GetToolsetConfig(name string) (ExtendedConfig, bool)
}

type GroupVersionKind struct {
Expand All @@ -49,5 +49,5 @@ type BaseConfig interface {
AuthProvider
ClusterProvider
DeniedResourcesProvider
ExtendedProvider
ExtendedConfigProvider
}
58 changes: 58 additions & 0 deletions pkg/api/imports_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package api

import (
"go/build"
"strings"
"testing"

"github.com/stretchr/testify/suite"
)

const modulePrefix = "github.com/containers/kubernetes-mcp-server/"

// ImportsSuite verifies that pkg/api doesn't accidentally import internal packages
// that would create cyclic dependencies.
type ImportsSuite struct {
suite.Suite
}

func (s *ImportsSuite) TestNoCyclicDependencies() {
// Whitelist of allowed internal packages that pkg/api can import.
// Any other internal import will cause the test to fail.
allowedInternalPackages := map[string]bool{
"github.com/containers/kubernetes-mcp-server/pkg/output": true,
}

s.Run("pkg/api only imports whitelisted internal packages", func() {
pkg, err := build.Import("github.com/containers/kubernetes-mcp-server/pkg/api", "", 0)
s.Require().NoError(err, "Failed to import pkg/api")

for _, imp := range pkg.Imports {
// Skip external packages (not part of this module)
if !strings.HasPrefix(imp, modulePrefix) {
continue
}

// Internal package - must be in whitelist
if !allowedInternalPackages[imp] {
s.Failf("Forbidden internal import detected",
"pkg/api imports %q which is not in the whitelist. "+
"To prevent cyclic dependencies, pkg/api can only import: %v. "+
"If this import is intentional, add it to allowedInternalPackages in this test.",
imp, keys(allowedInternalPackages))
}
}
})
}

func keys(m map[string]bool) []string {
result := make([]string, 0, len(m))
for k := range m {
result = append(result, k)
}
return result
}

func TestImports(t *testing.T) {
suite.Run(t, new(ImportsSuite))
}
129 changes: 129 additions & 0 deletions pkg/api/kubernetes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package api

import (
"context"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/metrics/pkg/apis/metrics"
metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1"
)

// Openshift provides OpenShift-specific detection capabilities.
// This interface is used by toolsets to conditionally enable OpenShift-specific tools.
type Openshift interface {
IsOpenShift(context.Context) bool
}

// ListOptions contains options for listing Kubernetes resources.
type ListOptions struct {
metav1.ListOptions
AsTable bool
}

// PodsTopOptions contains options for getting pod metrics.
type PodsTopOptions struct {
metav1.ListOptions
AllNamespaces bool
Namespace string
Name string
}

// NodesTopOptions contains options for getting node metrics.
type NodesTopOptions struct {
metav1.ListOptions
Name string
}

type KubernetesClientSet interface {
genericclioptions.RESTClientGetter
kubernetes.Interface
// NamespaceOrDefault returns the provided namespace or the default configured namespace if empty
NamespaceOrDefault(namespace string) string
RESTConfig() *rest.Config
RESTMapper() meta.ResettableRESTMapper
DiscoveryClient() discovery.CachedDiscoveryInterface
DynamicClient() dynamic.Interface
MetricsV1beta1Client() *metricsv1beta1.MetricsV1beta1Client
}

// KubernetesClient defines the interface for Kubernetes operations that tool and prompt handlers need.
// This interface abstracts the concrete Kubernetes implementation to allow for better decoupling
// and testability. The pkg/kubernetes.Kubernetes type implements this interface.
//
// For toolsets that need direct access to the Kubernetes clientset (e.g., for DynamicClient),
// they can type-assert to ClientsetProvider.
type KubernetesClient interface {
// AccessControlClientset provides access to the underlying Kubernetes clientset with access control enforced
AccessControlClientset() KubernetesClientSet

// --- Resource Operations ---

// ResourcesList lists resources of the specified GroupVersionKind
ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, options ListOptions) (runtime.Unstructured, error)
// ResourcesGet retrieves a single resource by name
ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) (*unstructured.Unstructured, error)
// ResourcesCreateOrUpdate creates or updates resources from a YAML/JSON string
ResourcesCreateOrUpdate(ctx context.Context, resource string) ([]*unstructured.Unstructured, error)
// ResourcesDelete deletes a resource by name
ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) error
// ResourcesScale gets or sets the scale of a resource
ResourcesScale(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string, desiredScale int64, shouldScale bool) (*unstructured.Unstructured, error)

// --- Namespace Operations ---

// NamespacesList lists all namespaces
NamespacesList(ctx context.Context, options ListOptions) (runtime.Unstructured, error)
// ProjectsList lists all OpenShift projects
ProjectsList(ctx context.Context, options ListOptions) (runtime.Unstructured, error)

// --- Pod Operations ---

// PodsListInAllNamespaces lists pods across all namespaces
PodsListInAllNamespaces(ctx context.Context, options ListOptions) (runtime.Unstructured, error)
// PodsListInNamespace lists pods in a specific namespace
PodsListInNamespace(ctx context.Context, namespace string, options ListOptions) (runtime.Unstructured, error)
// PodsGet retrieves a single pod by name
PodsGet(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error)
// PodsDelete deletes a pod and its managed resources
PodsDelete(ctx context.Context, namespace, name string) (string, error)
// PodsLog retrieves logs from a pod container
PodsLog(ctx context.Context, namespace, name, container string, previous bool, tail int64) (string, error)
// PodsRun creates and runs a new pod with optional service and route
PodsRun(ctx context.Context, namespace, name, image string, port int32) ([]*unstructured.Unstructured, error)
// PodsTop retrieves pod metrics
PodsTop(ctx context.Context, options PodsTopOptions) (*metrics.PodMetricsList, error)
// PodsExec executes a command in a pod container
PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error)

// --- Node Operations ---

// NodesLog retrieves logs from a node
NodesLog(ctx context.Context, name string, query string, tailLines int64) (string, error)
// NodesStatsSummary retrieves stats summary from a node
NodesStatsSummary(ctx context.Context, name string) (string, error)
// NodesTop retrieves node metrics
NodesTop(ctx context.Context, options NodesTopOptions) (*metrics.NodeMetricsList, error)

// --- Event Operations ---

// EventsList lists events in a namespace
EventsList(ctx context.Context, namespace string) ([]map[string]any, error)

// --- Configuration Operations ---

// ConfigurationContextsList returns the list of available context names
ConfigurationContextsList() (map[string]string, error)
// ConfigurationContextsDefault returns the current context name
ConfigurationContextsDefault() (string, error)
// ConfigurationView returns the kubeconfig content
ConfigurationView(minify bool) (runtime.Object, error)
}
9 changes: 3 additions & 6 deletions pkg/api/prompts.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package api

import (
"context"

internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
)
import "context"

// ServerPrompt represents a prompt that can be registered with the MCP server.
// Prompts provide pre-defined workflow templates and guidance to AI assistants.
Expand Down Expand Up @@ -88,7 +84,8 @@ func NewPromptCallResult(description string, messages []PromptMessage, err error
// PromptHandlerParams contains the parameters passed to a prompt handler
type PromptHandlerParams struct {
context.Context
*internalk8s.Kubernetes
ExtendedConfigProvider
KubernetesClient
PromptCallRequest
}

Expand Down
6 changes: 3 additions & 3 deletions pkg/api/toolsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/json"

internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/output"
"github.com/google/jsonschema-go/jsonschema"
)
Expand Down Expand Up @@ -43,7 +42,7 @@ type Toolset interface {
// GetDescription returns a human-readable description of the toolset.
// Will be used to generate documentation and help text.
GetDescription() string
GetTools(o internalk8s.Openshift) []ServerTool
GetTools(o Openshift) []ServerTool
}

type ToolCallRequest interface {
Expand All @@ -66,7 +65,8 @@ func NewToolCallResult(content string, err error) *ToolCallResult {

type ToolHandlerParams struct {
context.Context
*internalk8s.Kubernetes
ExtendedConfigProvider
KubernetesClient
ToolCallRequest
ListOutput output.Output
}
Expand Down
16 changes: 8 additions & 8 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"strings"

"github.com/BurntSushi/toml"
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
"github.com/containers/kubernetes-mcp-server/pkg/api"
"k8s.io/klog/v2"
)

Expand All @@ -21,7 +21,7 @@ const (
// StaticConfig is the configuration for the server.
// It allows to configure server specific settings and tools to be enabled or disabled.
type StaticConfig struct {
DeniedResources []configapi.GroupVersionKind `toml:"denied_resources"`
DeniedResources []api.GroupVersionKind `toml:"denied_resources"`

LogLevel int `toml:"log_level,omitzero"`
Port string `toml:"port,omitempty"`
Expand Down Expand Up @@ -82,15 +82,15 @@ type StaticConfig struct {
promptsMetadata toml.MetaData // Internal: metadata for prompts decoding

// Internal: parsed provider configs (not exposed to TOML package)
parsedClusterProviderConfigs map[string]configapi.Extended
parsedClusterProviderConfigs map[string]api.ExtendedConfig
// Internal: parsed toolset configs (not exposed to TOML package)
parsedToolsetConfigs map[string]configapi.Extended
parsedToolsetConfigs map[string]api.ExtendedConfig

// Internal: the config.toml directory, to help resolve relative file paths
configDirPath string
}

var _ configapi.BaseConfig = (*StaticConfig)(nil)
var _ api.BaseConfig = (*StaticConfig)(nil)

type ReadConfigOpt func(cfg *StaticConfig)

Expand Down Expand Up @@ -306,21 +306,21 @@ func (c *StaticConfig) GetClusterProviderStrategy() string {
return c.ClusterProviderStrategy
}

func (c *StaticConfig) GetDeniedResources() []configapi.GroupVersionKind {
func (c *StaticConfig) GetDeniedResources() []api.GroupVersionKind {
return c.DeniedResources
}

func (c *StaticConfig) GetKubeConfigPath() string {
return c.KubeConfig
}

func (c *StaticConfig) GetProviderConfig(strategy string) (configapi.Extended, bool) {
func (c *StaticConfig) GetProviderConfig(strategy string) (api.ExtendedConfig, bool) {
cfg, ok := c.parsedClusterProviderConfigs[strategy]

return cfg, ok
}

func (c *StaticConfig) GetToolsetConfig(name string) (configapi.Extended, bool) {
func (c *StaticConfig) GetToolsetConfig(name string) (api.ExtendedConfig, bool) {
cfg, ok := c.parsedToolsetConfigs[name]
return cfg, ok
}
Expand Down
Loading