mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
feat(config): --disable-destructive exposes tools not annotated with destructiveHint=true
This commit is contained in:
13
README.md
13
README.md
@@ -148,12 +148,13 @@ uvx kubernetes-mcp-server@latest --help
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port. |
|
||||
| `--log-level` | Sets the logging level (values [from 0-9](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md)). Similar to [kubectl logging levels](https://kubernetes.io/docs/reference/kubectl/quick-reference/#kubectl-output-verbosity-and-debugging). |
|
||||
| `--kubeconfig` | Path to the Kubernetes configuration file. If not provided, it will try to resolve the configuration (in-cluster, default location, etc.). |
|
||||
| `--read-only` | If set, the MCP server will run in read-only mode, meaning it will not allow any write operations (create, update, delete) on the Kubernetes cluster. This is useful for debugging or inspecting the cluster without making changes. |
|
||||
| Option | Description |
|
||||
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port. |
|
||||
| `--log-level` | Sets the logging level (values [from 0-9](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md)). Similar to [kubectl logging levels](https://kubernetes.io/docs/reference/kubectl/quick-reference/#kubectl-output-verbosity-and-debugging). |
|
||||
| `--kubeconfig` | Path to the Kubernetes configuration file. If not provided, it will try to resolve the configuration (in-cluster, default location, etc.). |
|
||||
| `--read-only` | If set, the MCP server will run in read-only mode, meaning it will not allow any write operations (create, update, delete) on the Kubernetes cluster. This is useful for debugging or inspecting the cluster without making changes. |
|
||||
| `--disable-destructive` | If set, the MCP server will disable all destructive operations (delete, update, etc.) on the Kubernetes cluster. This is useful for debugging or inspecting the cluster without accidentally making changes. This option has no effect when `--read-only` is used. |
|
||||
|
||||
## 🛠️ Tools <a id="tools"></a>
|
||||
|
||||
|
||||
@@ -49,14 +49,16 @@ Kubernetes Model Context Protocol (MCP) server
|
||||
klog.V(1).Info("Starting kubernetes-mcp-server")
|
||||
klog.V(1).Infof(" - Profile: %s", profile.GetName())
|
||||
klog.V(1).Infof(" - Read-only mode: %t", viper.GetBool("read-only"))
|
||||
klog.V(1).Infof(" - Disable destructive tools: %t", viper.GetBool("disable-destructive"))
|
||||
if viper.GetBool("version") {
|
||||
fmt.Println(version.Version)
|
||||
return
|
||||
}
|
||||
mcpServer, err := mcp.NewSever(mcp.Configuration{
|
||||
Profile: profile,
|
||||
ReadOnly: viper.GetBool("read-only"),
|
||||
Kubeconfig: viper.GetString("kubeconfig"),
|
||||
Profile: profile,
|
||||
ReadOnly: viper.GetBool("read-only"),
|
||||
DisableDestructive: viper.GetBool("disable-destructive"),
|
||||
Kubeconfig: viper.GetString("kubeconfig"),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize MCP server: %v\n", err)
|
||||
@@ -127,5 +129,6 @@ func init() {
|
||||
rootCmd.Flags().StringP("kubeconfig", "", "", "Path to the kubeconfig file to use for authentication")
|
||||
rootCmd.Flags().String("profile", "full", "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+")")
|
||||
rootCmd.Flags().Bool("read-only", false, "If true, only tools annotated with readOnlyHint=true are exposed")
|
||||
rootCmd.Flags().Bool("disable-destructive", false, "If true, tools annotated with destructiveHint=true are disabled")
|
||||
_ = viper.BindPFlags(rootCmd.Flags())
|
||||
}
|
||||
|
||||
@@ -43,3 +43,11 @@ func TestDefaultReadOnly(t *testing.T) {
|
||||
t.Fatalf("Expected read-only mode false, got %s %v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDisableDestructive(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
|
||||
out, err := captureOutput(rootCmd.Execute)
|
||||
if !strings.Contains(out, " - Disable destructive tools: false") {
|
||||
t.Fatalf("Expected disable destructive false, got %s %v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,16 +94,17 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
type mcpContext struct {
|
||||
profile Profile
|
||||
readOnly bool
|
||||
before func(*mcpContext)
|
||||
after func(*mcpContext)
|
||||
ctx context.Context
|
||||
tempDir string
|
||||
cancel context.CancelFunc
|
||||
mcpServer *Server
|
||||
mcpHttpServer *httptest.Server
|
||||
mcpClient *client.Client
|
||||
profile Profile
|
||||
readOnly bool
|
||||
disableDestructive bool
|
||||
before func(*mcpContext)
|
||||
after func(*mcpContext)
|
||||
ctx context.Context
|
||||
tempDir string
|
||||
cancel context.CancelFunc
|
||||
mcpServer *Server
|
||||
mcpHttpServer *httptest.Server
|
||||
mcpClient *client.Client
|
||||
}
|
||||
|
||||
func (c *mcpContext) beforeEach(t *testing.T) {
|
||||
@@ -117,7 +118,9 @@ func (c *mcpContext) beforeEach(t *testing.T) {
|
||||
if c.before != nil {
|
||||
c.before(c)
|
||||
}
|
||||
if c.mcpServer, err = NewSever(Configuration{Profile: c.profile, ReadOnly: c.readOnly}); err != nil {
|
||||
if c.mcpServer, err = NewSever(Configuration{
|
||||
Profile: c.profile, ReadOnly: c.readOnly, DisableDestructive: c.disableDestructive,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ func (s *Server) initConfiguration() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Configuration: View"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), s.configurationView},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ func (s *Server) initEvents() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Events: List"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), s.eventsList},
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func (s *Server) initHelm() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Helm: List"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), s.helmList},
|
||||
{mcp.NewTool("helm_uninstall",
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
type Configuration struct {
|
||||
Profile Profile
|
||||
// When true, expose only tools annotated with readOnlyHint=true
|
||||
ReadOnly bool
|
||||
Kubeconfig string
|
||||
ReadOnly bool
|
||||
// When true, disable tools annotated with destructiveHint=true
|
||||
DisableDestructive bool
|
||||
Kubeconfig string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
@@ -39,6 +41,10 @@ func NewSever(configuration Configuration) (*Server, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func isFalse(value *bool) bool {
|
||||
return value == nil || !*value
|
||||
}
|
||||
|
||||
func (s *Server) reloadKubernetesClient() error {
|
||||
k, err := kubernetes.NewKubernetes(s.configuration.Kubeconfig)
|
||||
if err != nil {
|
||||
@@ -47,7 +53,10 @@ func (s *Server) reloadKubernetesClient() error {
|
||||
s.k = k
|
||||
applicableTools := make([]server.ServerTool, 0)
|
||||
for _, tool := range s.configuration.Profile.GetTools(s) {
|
||||
if s.configuration.ReadOnly && (tool.Tool.Annotations.ReadOnlyHint == nil || !*tool.Tool.Annotations.ReadOnlyHint) {
|
||||
if s.configuration.ReadOnly && isFalse(tool.Tool.Annotations.ReadOnlyHint) {
|
||||
continue
|
||||
}
|
||||
if s.configuration.DisableDestructive && isFalse(tool.Tool.Annotations.ReadOnlyHint) && !isFalse(tool.Tool.Annotations.DestructiveHint) {
|
||||
continue
|
||||
}
|
||||
applicableTools = append(applicableTools, tool)
|
||||
|
||||
@@ -62,6 +62,28 @@ func TestReadOnly(t *testing.T) {
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ func (s *Server) initNamespaces() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Namespaces: List"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.namespacesList,
|
||||
})
|
||||
@@ -25,6 +26,7 @@ func (s *Server) initNamespaces() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Projects: List"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.projectsList,
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ func (s *Server) initPods() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Pods: List"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.podsListInAllNamespaces},
|
||||
{Tool: mcp.NewTool("pods_list_in_namespace",
|
||||
@@ -26,6 +27,7 @@ func (s *Server) initPods() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Pods: List in Namespace"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.podsListInNamespace},
|
||||
{Tool: mcp.NewTool("pods_get",
|
||||
@@ -35,6 +37,7 @@ func (s *Server) initPods() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Pods: Get"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.podsGet},
|
||||
{Tool: mcp.NewTool("pods_delete",
|
||||
@@ -81,6 +84,7 @@ func (s *Server) initPods() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Pods: Log"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.podsLog},
|
||||
{Tool: mcp.NewTool("pods_run",
|
||||
|
||||
@@ -35,6 +35,7 @@ func (s *Server) initResources() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Resources: List"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.resourcesList},
|
||||
{Tool: mcp.NewTool("resources_get",
|
||||
@@ -55,6 +56,7 @@ func (s *Server) initResources() []server.ServerTool {
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Resources: Get"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.resourcesGet},
|
||||
{Tool: mcp.NewTool("resources_create_or_update",
|
||||
|
||||
Reference in New Issue
Block a user