mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
feat(config): --read-only mode flag exposes only read-only annotated tools
This commit is contained in:
@@ -153,6 +153,7 @@ uvx kubernetes-mcp-server@latest --help
|
||||
| `--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. |
|
||||
|
||||
## 🛠️ Tools <a id="tools"></a>
|
||||
|
||||
|
||||
@@ -46,13 +46,16 @@ Kubernetes Model Context Protocol (MCP) server
|
||||
fmt.Printf("Invalid profile name: %s, valid names are: %s\n", viper.GetString("profile"), strings.Join(mcp.ProfileNames, ", "))
|
||||
os.Exit(1)
|
||||
}
|
||||
klog.V(1).Infof("Starting kubernetes-mcp-server with profile: %s", profile.GetName())
|
||||
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"))
|
||||
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"),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -123,5 +126,6 @@ func init() {
|
||||
rootCmd.Flags().StringP("sse-base-url", "", "", "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
|
||||
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")
|
||||
_ = viper.BindPFlags(rootCmd.Flags())
|
||||
}
|
||||
|
||||
@@ -31,7 +31,15 @@ func TestVersion(t *testing.T) {
|
||||
func TestDefaultProfile(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
|
||||
out, err := captureOutput(rootCmd.Execute)
|
||||
if !strings.Contains(out, "Starting kubernetes-mcp-server with profile: full") {
|
||||
if !strings.Contains(out, "- Profile: full") {
|
||||
t.Fatalf("Expected profile 'full', got %s %v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultReadOnly(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
|
||||
out, err := captureOutput(rootCmd.Execute)
|
||||
if !strings.Contains(out, " - Read-only mode: false") {
|
||||
t.Fatalf("Expected read-only mode false, got %s %v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
type mcpContext struct {
|
||||
profile Profile
|
||||
readOnly bool
|
||||
before func(*mcpContext)
|
||||
after func(*mcpContext)
|
||||
ctx context.Context
|
||||
@@ -116,7 +117,7 @@ func (c *mcpContext) beforeEach(t *testing.T) {
|
||||
if c.before != nil {
|
||||
c.before(c)
|
||||
}
|
||||
if c.mcpServer, err = NewSever(Configuration{Profile: c.profile}); err != nil {
|
||||
if c.mcpServer, err = NewSever(Configuration{Profile: c.profile, ReadOnly: c.readOnly}); err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Profile Profile
|
||||
Profile Profile
|
||||
// When true, expose only tools annotated with readOnlyHint=true
|
||||
ReadOnly bool
|
||||
Kubeconfig string
|
||||
}
|
||||
|
||||
@@ -43,7 +45,14 @@ func (s *Server) reloadKubernetesClient() error {
|
||||
return err
|
||||
}
|
||||
s.k = k
|
||||
s.server.SetTools(s.configuration.Profile.GetTools(s)...)
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
applicableTools = append(applicableTools, tool)
|
||||
}
|
||||
s.server.SetTools(applicableTools...)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -47,3 +47,22 @@ func TestWatchKubeConfig(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadOnly(t *testing.T) {
|
||||
readOnlyServer := func(c *mcpContext) { c.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)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user