Skip to content

Commit e6f20fc

Browse files
authored
feat(config): --disable-destructive exposes tools not annotated with destructiveHint=true
1 parent 5f279a8 commit e6f20fc

File tree

12 files changed

+80
-23
lines changed

12 files changed

+80
-23
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,13 @@ uvx kubernetes-mcp-server@latest --help
148148

149149
### Configuration Options
150150

151-
| Option | Description |
152-
|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
153-
| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port. |
154-
| `--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). |
155-
| `--kubeconfig` | Path to the Kubernetes configuration file. If not provided, it will try to resolve the configuration (in-cluster, default location, etc.). |
156-
| `--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. |
151+
| Option | Description |
152+
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
153+
| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port. |
154+
| `--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). |
155+
| `--kubeconfig` | Path to the Kubernetes configuration file. If not provided, it will try to resolve the configuration (in-cluster, default location, etc.). |
156+
| `--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. |
157+
| `--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. |
157158

158159
## 🛠️ Tools <a id="tools"></a>
159160

pkg/kubernetes-mcp-server/cmd/root.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ Kubernetes Model Context Protocol (MCP) server
4949
klog.V(1).Info("Starting kubernetes-mcp-server")
5050
klog.V(1).Infof(" - Profile: %s", profile.GetName())
5151
klog.V(1).Infof(" - Read-only mode: %t", viper.GetBool("read-only"))
52+
klog.V(1).Infof(" - Disable destructive tools: %t", viper.GetBool("disable-destructive"))
5253
if viper.GetBool("version") {
5354
fmt.Println(version.Version)
5455
return
5556
}
5657
mcpServer, err := mcp.NewSever(mcp.Configuration{
57-
Profile: profile,
58-
ReadOnly: viper.GetBool("read-only"),
59-
Kubeconfig: viper.GetString("kubeconfig"),
58+
Profile: profile,
59+
ReadOnly: viper.GetBool("read-only"),
60+
DisableDestructive: viper.GetBool("disable-destructive"),
61+
Kubeconfig: viper.GetString("kubeconfig"),
6062
})
6163
if err != nil {
6264
fmt.Printf("Failed to initialize MCP server: %v\n", err)
@@ -127,5 +129,6 @@ func init() {
127129
rootCmd.Flags().StringP("kubeconfig", "", "", "Path to the kubeconfig file to use for authentication")
128130
rootCmd.Flags().String("profile", "full", "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+")")
129131
rootCmd.Flags().Bool("read-only", false, "If true, only tools annotated with readOnlyHint=true are exposed")
132+
rootCmd.Flags().Bool("disable-destructive", false, "If true, tools annotated with destructiveHint=true are disabled")
130133
_ = viper.BindPFlags(rootCmd.Flags())
131134
}

pkg/kubernetes-mcp-server/cmd/root_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,11 @@ func TestDefaultReadOnly(t *testing.T) {
4343
t.Fatalf("Expected read-only mode false, got %s %v", out, err)
4444
}
4545
}
46+
47+
func TestDefaultDisableDestructive(t *testing.T) {
48+
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
49+
out, err := captureOutput(rootCmd.Execute)
50+
if !strings.Contains(out, " - Disable destructive tools: false") {
51+
t.Fatalf("Expected disable destructive false, got %s %v", out, err)
52+
}
53+
}

pkg/mcp/common_test.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,17 @@ func TestMain(m *testing.M) {
9494
}
9595

9696
type mcpContext struct {
97-
profile Profile
98-
readOnly bool
99-
before func(*mcpContext)
100-
after func(*mcpContext)
101-
ctx context.Context
102-
tempDir string
103-
cancel context.CancelFunc
104-
mcpServer *Server
105-
mcpHttpServer *httptest.Server
106-
mcpClient *client.Client
97+
profile Profile
98+
readOnly bool
99+
disableDestructive bool
100+
before func(*mcpContext)
101+
after func(*mcpContext)
102+
ctx context.Context
103+
tempDir string
104+
cancel context.CancelFunc
105+
mcpServer *Server
106+
mcpHttpServer *httptest.Server
107+
mcpClient *client.Client
107108
}
108109

109110
func (c *mcpContext) beforeEach(t *testing.T) {
@@ -117,7 +118,9 @@ func (c *mcpContext) beforeEach(t *testing.T) {
117118
if c.before != nil {
118119
c.before(c)
119120
}
120-
if c.mcpServer, err = NewSever(Configuration{Profile: c.profile, ReadOnly: c.readOnly}); err != nil {
121+
if c.mcpServer, err = NewSever(Configuration{
122+
Profile: c.profile, ReadOnly: c.readOnly, DisableDestructive: c.disableDestructive,
123+
}); err != nil {
121124
t.Fatal(err)
122125
return
123126
}

pkg/mcp/configuration.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func (s *Server) initConfiguration() []server.ServerTool {
1818
// Tool annotations
1919
mcp.WithTitleAnnotation("Configuration: View"),
2020
mcp.WithReadOnlyHintAnnotation(true),
21+
mcp.WithDestructiveHintAnnotation(false),
2122
mcp.WithOpenWorldHintAnnotation(true),
2223
), s.configurationView},
2324
}

pkg/mcp/events.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func (s *Server) initEvents() []server.ServerTool {
1616
// Tool annotations
1717
mcp.WithTitleAnnotation("Events: List"),
1818
mcp.WithReadOnlyHintAnnotation(true),
19+
mcp.WithDestructiveHintAnnotation(false),
1920
mcp.WithOpenWorldHintAnnotation(true),
2021
), s.eventsList},
2122
}

pkg/mcp/helm.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func (s *Server) initHelm() []server.ServerTool {
2929
// Tool annotations
3030
mcp.WithTitleAnnotation("Helm: List"),
3131
mcp.WithReadOnlyHintAnnotation(true),
32+
mcp.WithDestructiveHintAnnotation(false),
3233
mcp.WithOpenWorldHintAnnotation(true),
3334
), s.helmList},
3435
{mcp.NewTool("helm_uninstall",

pkg/mcp/mcp.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import (
1010
type Configuration struct {
1111
Profile Profile
1212
// When true, expose only tools annotated with readOnlyHint=true
13-
ReadOnly bool
14-
Kubeconfig string
13+
ReadOnly bool
14+
// When true, disable tools annotated with destructiveHint=true
15+
DisableDestructive bool
16+
Kubeconfig string
1517
}
1618

1719
type Server struct {
@@ -39,6 +41,10 @@ func NewSever(configuration Configuration) (*Server, error) {
3941
return s, nil
4042
}
4143

44+
func isFalse(value *bool) bool {
45+
return value == nil || !*value
46+
}
47+
4248
func (s *Server) reloadKubernetesClient() error {
4349
k, err := kubernetes.NewKubernetes(s.configuration.Kubeconfig)
4450
if err != nil {
@@ -47,7 +53,10 @@ func (s *Server) reloadKubernetesClient() error {
4753
s.k = k
4854
applicableTools := make([]server.ServerTool, 0)
4955
for _, tool := range s.configuration.Profile.GetTools(s) {
50-
if s.configuration.ReadOnly && (tool.Tool.Annotations.ReadOnlyHint == nil || !*tool.Tool.Annotations.ReadOnlyHint) {
56+
if s.configuration.ReadOnly && isFalse(tool.Tool.Annotations.ReadOnlyHint) {
57+
continue
58+
}
59+
if s.configuration.DisableDestructive && isFalse(tool.Tool.Annotations.ReadOnlyHint) && !isFalse(tool.Tool.Annotations.DestructiveHint) {
5160
continue
5261
}
5362
applicableTools = append(applicableTools, tool)

pkg/mcp/mcp_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ func TestReadOnly(t *testing.T) {
6262
if tool.Annotations.ReadOnlyHint == nil || !*tool.Annotations.ReadOnlyHint {
6363
t.Errorf("Tool %s is not read-only but should be", tool.Name)
6464
}
65+
if tool.Annotations.DestructiveHint != nil && *tool.Annotations.DestructiveHint {
66+
t.Errorf("Tool %s is destructive but should not be in read-only mode", tool.Name)
67+
}
68+
}
69+
})
70+
})
71+
}
72+
73+
func TestDisableDestructive(t *testing.T) {
74+
disableDestructiveServer := func(c *mcpContext) { c.disableDestructive = true }
75+
testCaseWithContext(t, &mcpContext{before: disableDestructiveServer}, func(c *mcpContext) {
76+
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
77+
t.Run("ListTools returns tools", func(t *testing.T) {
78+
if err != nil {
79+
t.Fatalf("call ListTools failed %v", err)
80+
}
81+
})
82+
t.Run("ListTools does not return destructive tools", func(t *testing.T) {
83+
for _, tool := range tools.Tools {
84+
if tool.Annotations.DestructiveHint != nil && *tool.Annotations.DestructiveHint {
85+
t.Errorf("Tool %s is destructive but should not be", tool.Name)
86+
}
6587
}
6688
})
6789
})

pkg/mcp/namespaces.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func (s *Server) initNamespaces() []server.ServerTool {
1515
// Tool annotations
1616
mcp.WithTitleAnnotation("Namespaces: List"),
1717
mcp.WithReadOnlyHintAnnotation(true),
18+
mcp.WithDestructiveHintAnnotation(false),
1819
mcp.WithOpenWorldHintAnnotation(true),
1920
), Handler: s.namespacesList,
2021
})
@@ -25,6 +26,7 @@ func (s *Server) initNamespaces() []server.ServerTool {
2526
// Tool annotations
2627
mcp.WithTitleAnnotation("Projects: List"),
2728
mcp.WithReadOnlyHintAnnotation(true),
29+
mcp.WithDestructiveHintAnnotation(false),
2830
mcp.WithOpenWorldHintAnnotation(true),
2931
), Handler: s.projectsList,
3032
})

0 commit comments

Comments
 (0)