Skip to content

Commit 186f445

Browse files
authored
feat(config): introduce enabled/disabled tool list in configuration file (155)
Introduce allow/deny tool functionality in toml config --- Remove duplicate fields that already defined in staticConfig --- Add unit tests to verify tool valid check --- Wire staticConfig to fix unit tests --- Rename to enabled/disabled instead of allowed/denied
1 parent af2a8cd commit 186f445

File tree

7 files changed

+257
-67
lines changed

7 files changed

+257
-67
lines changed

pkg/config/config.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,23 @@ import (
66
"github.com/BurntSushi/toml"
77
)
88

9+
// StaticConfig is the configuration for the server.
10+
// It allows to configure server specific settings and tools to be enabled or disabled.
911
type StaticConfig struct {
1012
DeniedResources []GroupVersionKind `toml:"denied_resources"`
1113

12-
LogLevel int `toml:"log_level,omitempty"`
13-
SSEPort int `toml:"sse_port,omitempty"`
14-
HTTPPort int `toml:"http_port,omitempty"`
15-
SSEBaseURL string `toml:"sse_base_url,omitempty"`
16-
KubeConfig string `toml:"kubeconfig,omitempty"`
17-
ListOutput string `toml:"list_output,omitempty"`
18-
ReadOnly bool `toml:"read_only,omitempty"`
19-
DisableDestructive bool `toml:"disable_destructive,omitempty"`
14+
LogLevel int `toml:"log_level,omitempty"`
15+
SSEPort int `toml:"sse_port,omitempty"`
16+
HTTPPort int `toml:"http_port,omitempty"`
17+
SSEBaseURL string `toml:"sse_base_url,omitempty"`
18+
KubeConfig string `toml:"kubeconfig,omitempty"`
19+
ListOutput string `toml:"list_output,omitempty"`
20+
// When true, expose only tools annotated with readOnlyHint=true
21+
ReadOnly bool `toml:"read_only,omitempty"`
22+
// When true, disable tools annotated with destructiveHint=true
23+
DisableDestructive bool `toml:"disable_destructive,omitempty"`
24+
EnabledTools []string `toml:"enabled_tools,omitempty"`
25+
DisabledTools []string `toml:"disabled_tools,omitempty"`
2026
}
2127

2228
type GroupVersionKind struct {
@@ -25,6 +31,7 @@ type GroupVersionKind struct {
2531
Kind string `toml:"kind,omitempty"`
2632
}
2733

34+
// ReadConfig reads the toml file and returns the StaticConfig.
2835
func ReadConfig(configPath string) (*StaticConfig, error) {
2936
configData, err := os.ReadFile(configPath)
3037
if err != nil {

pkg/config/config_test.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,13 @@ list_output = "yaml"
5757
read_only = true
5858
disable_destructive = false
5959
60-
[[denied_resources]]
61-
group = "apps"
62-
version = "v1"
63-
kind = "Deployment"
60+
denied_resources = [
61+
{group = "apps", version = "v1", kind = "Deployment"},
62+
{group = "rbac.authorization.k8s.io", version = "v1", kind = "Role"}
63+
]
6464
65-
[[denied_resources]]
66-
group = "rbac.authorization.k8s.io"
67-
version = "v1"
65+
enabled_tools = ["configuration_view", "events_list", "namespaces_list", "pods_list", "resources_list", "resources_get", "resources_create_or_update", "resources_delete"]
66+
disabled_tools = ["pods_delete", "pods_top", "pods_log", "pods_run", "pods_exec"]
6867
`)
6968

7069
config, err := ReadConfig(validConfigPath)
@@ -109,6 +108,12 @@ version = "v1"
109108
if config.DisableDestructive {
110109
t.Fatalf("Unexpected disable destructive: %v", config.DisableDestructive)
111110
}
111+
if len(config.EnabledTools) != 8 {
112+
t.Fatalf("Unexpected enabled tools: %v", config.EnabledTools)
113+
}
114+
if len(config.DisabledTools) != 5 {
115+
t.Fatalf("Unexpected disabled tools: %v", config.DisabledTools)
116+
}
112117
})
113118
}
114119

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,9 @@ func (m *MCPServerOptions) Run() error {
186186
return nil
187187
}
188188
mcpServer, err := mcp.NewServer(mcp.Configuration{
189-
Profile: profile,
190-
ListOutput: listOutput,
191-
ReadOnly: m.StaticConfig.ReadOnly,
192-
DisableDestructive: m.StaticConfig.DisableDestructive,
193-
Kubeconfig: m.StaticConfig.KubeConfig,
194-
StaticConfig: m.StaticConfig,
189+
Profile: profile,
190+
ListOutput: listOutput,
191+
StaticConfig: m.StaticConfig,
195192
})
196193
if err != nil {
197194
return fmt.Errorf("Failed to initialize MCP server: %w\n", err)

pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ list_output = "yaml"
55
read_only = true
66
disable_destructive = true
77

8-
[[denied_resources]]
9-
group = "apps"
10-
version = "v1"
11-
kind = "Deployment"
8+
denied_resources = [
9+
{group = "apps", version = "v1", kind = "Deployment"},
10+
{group = "rbac.authorization.k8s.io", version = "v1", kind = "Role"}
11+
]
12+
13+
enabled_tools = ["configuration_view", "events_list", "namespaces_list", "pods_list", "resources_list", "resources_get", "resources_create_or_update", "resources_delete"]
14+
disabled_tools = ["pods_delete", "pods_top", "pods_log", "pods_run", "pods_exec"]
1215

13-
[[denied_resources]]
14-
group = "rbac.authorization.k8s.io"
15-
version = "v1"

pkg/mcp/common_test.go

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"net/http/httptest"
8+
"os"
9+
"path/filepath"
10+
"runtime"
11+
"testing"
12+
"time"
13+
714
"github.com/manusa/kubernetes-mcp-server/pkg/config"
815
"github.com/manusa/kubernetes-mcp-server/pkg/output"
916
"github.com/mark3labs/mcp-go/client"
@@ -28,18 +35,12 @@ import (
2835
"k8s.io/client-go/tools/clientcmd/api"
2936
toolswatch "k8s.io/client-go/tools/watch"
3037
"k8s.io/utils/ptr"
31-
"net/http/httptest"
32-
"os"
33-
"path/filepath"
34-
"runtime"
3538
"sigs.k8s.io/controller-runtime/pkg/envtest"
3639
"sigs.k8s.io/controller-runtime/tools/setup-envtest/env"
3740
"sigs.k8s.io/controller-runtime/tools/setup-envtest/remote"
3841
"sigs.k8s.io/controller-runtime/tools/setup-envtest/store"
3942
"sigs.k8s.io/controller-runtime/tools/setup-envtest/versions"
4043
"sigs.k8s.io/controller-runtime/tools/setup-envtest/workflows"
41-
"testing"
42-
"time"
4344
)
4445

4546
// envTest has an expensive setup, so we only want to do it once per entire test run.
@@ -97,20 +98,19 @@ func TestMain(m *testing.M) {
9798
}
9899

99100
type mcpContext struct {
100-
profile Profile
101-
listOutput output.Output
102-
readOnly bool
103-
disableDestructive bool
104-
staticConfig *config.StaticConfig
105-
clientOptions []transport.ClientOption
106-
before func(*mcpContext)
107-
after func(*mcpContext)
108-
ctx context.Context
109-
tempDir string
110-
cancel context.CancelFunc
111-
mcpServer *Server
112-
mcpHttpServer *httptest.Server
113-
mcpClient *client.Client
101+
profile Profile
102+
listOutput output.Output
103+
104+
staticConfig *config.StaticConfig
105+
clientOptions []transport.ClientOption
106+
before func(*mcpContext)
107+
after func(*mcpContext)
108+
ctx context.Context
109+
tempDir string
110+
cancel context.CancelFunc
111+
mcpServer *Server
112+
mcpHttpServer *httptest.Server
113+
mcpClient *client.Client
114114
}
115115

116116
func (c *mcpContext) beforeEach(t *testing.T) {
@@ -125,17 +125,18 @@ func (c *mcpContext) beforeEach(t *testing.T) {
125125
c.listOutput = output.Yaml
126126
}
127127
if c.staticConfig == nil {
128-
c.staticConfig = &config.StaticConfig{}
128+
c.staticConfig = &config.StaticConfig{
129+
ReadOnly: false,
130+
DisableDestructive: false,
131+
}
129132
}
130133
if c.before != nil {
131134
c.before(c)
132135
}
133136
if c.mcpServer, err = NewServer(Configuration{
134-
Profile: c.profile,
135-
ListOutput: c.listOutput,
136-
ReadOnly: c.readOnly,
137-
DisableDestructive: c.disableDestructive,
138-
StaticConfig: c.staticConfig,
137+
Profile: c.profile,
138+
ListOutput: c.listOutput,
139+
StaticConfig: c.staticConfig,
139140
}); err != nil {
140141
t.Fatal(err)
141142
return

pkg/mcp/mcp.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ package mcp
22

33
import (
44
"context"
5-
"github.com/manusa/kubernetes-mcp-server/pkg/config"
65
"net/http"
6+
"slices"
77

88
"github.com/mark3labs/mcp-go/mcp"
99
"github.com/mark3labs/mcp-go/server"
1010
"k8s.io/utils/ptr"
1111

12+
"github.com/manusa/kubernetes-mcp-server/pkg/config"
1213
"github.com/manusa/kubernetes-mcp-server/pkg/kubernetes"
1314
"github.com/manusa/kubernetes-mcp-server/pkg/output"
1415
"github.com/manusa/kubernetes-mcp-server/pkg/version"
@@ -17,15 +18,26 @@ import (
1718
type Configuration struct {
1819
Profile Profile
1920
ListOutput output.Output
20-
// When true, expose only tools annotated with readOnlyHint=true
21-
ReadOnly bool
22-
// When true, disable tools annotated with destructiveHint=true
23-
DisableDestructive bool
24-
Kubeconfig string
2521

2622
StaticConfig *config.StaticConfig
2723
}
2824

25+
func (c *Configuration) isToolApplicable(tool server.ServerTool) bool {
26+
if c.StaticConfig.ReadOnly && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) {
27+
return false
28+
}
29+
if c.StaticConfig.DisableDestructive && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) && ptr.Deref(tool.Tool.Annotations.DestructiveHint, false) {
30+
return false
31+
}
32+
if c.StaticConfig.EnabledTools != nil && !slices.Contains(c.StaticConfig.EnabledTools, tool.Tool.Name) {
33+
return false
34+
}
35+
if c.StaticConfig.DisabledTools != nil && slices.Contains(c.StaticConfig.DisabledTools, tool.Tool.Name) {
36+
return false
37+
}
38+
return true
39+
}
40+
2941
type Server struct {
3042
configuration *Configuration
3143
server *server.MCPServer
@@ -53,17 +65,14 @@ func NewServer(configuration Configuration) (*Server, error) {
5365
}
5466

5567
func (s *Server) reloadKubernetesClient() error {
56-
k, err := kubernetes.NewManager(s.configuration.Kubeconfig, s.configuration.StaticConfig)
68+
k, err := kubernetes.NewManager(s.configuration.StaticConfig.KubeConfig, s.configuration.StaticConfig)
5769
if err != nil {
5870
return err
5971
}
6072
s.k = k
6173
applicableTools := make([]server.ServerTool, 0)
6274
for _, tool := range s.configuration.Profile.GetTools(s) {
63-
if s.configuration.ReadOnly && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) {
64-
continue
65-
}
66-
if s.configuration.DisableDestructive && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) && ptr.Deref(tool.Tool.Annotations.DestructiveHint, false) {
75+
if !s.configuration.isToolApplicable(tool) {
6776
continue
6877
}
6978
applicableTools = append(applicableTools, tool)

0 commit comments

Comments
 (0)