Skip to content

Commit 2a1a3e4

Browse files
authored
feat(config): define flags in configuration file (152)
Define flags in configuration file --- Add vscode in .gitignore
1 parent b777972 commit 2a1a3e4

File tree

6 files changed

+159
-25
lines changed

6 files changed

+159
-25
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea/
2+
.vscode/
23
.docusaurus/
34
node_modules/
45

pkg/config/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ import (
88

99
type StaticConfig struct {
1010
DeniedResources []GroupVersionKind `toml:"denied_resources"`
11+
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"`
1120
}
1221

1322
type GroupVersionKind struct {

pkg/config/config_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ kind = "Role
5050

5151
func TestReadConfigValid(t *testing.T) {
5252
validConfigPath := writeConfig(t, `
53+
log_level = 1
54+
sse_port = 9999
55+
kubeconfig = "test"
56+
list_output = "yaml"
57+
read_only = true
58+
disable_destructive = false
59+
5360
[[denied_resources]]
5461
group = "apps"
5562
version = "v1"
@@ -78,6 +85,30 @@ version = "v1"
7885
config.DeniedResources[0].Kind != "Deployment" {
7986
t.Errorf("Unexpected denied resources: %v", config.DeniedResources[0])
8087
}
88+
if config.LogLevel != 1 {
89+
t.Fatalf("Unexpected log level: %v", config.LogLevel)
90+
}
91+
if config.SSEPort != 9999 {
92+
t.Fatalf("Unexpected sse_port value: %v", config.SSEPort)
93+
}
94+
if config.SSEBaseURL != "" {
95+
t.Fatalf("Unexpected sse_base_url value: %v", config.SSEBaseURL)
96+
}
97+
if config.HTTPPort != 0 {
98+
t.Fatalf("Unexpected http_port value: %v", config.HTTPPort)
99+
}
100+
if config.KubeConfig != "test" {
101+
t.Fatalf("Unexpected kubeconfig value: %v", config.KubeConfig)
102+
}
103+
if config.ListOutput != "yaml" {
104+
t.Fatalf("Unexpected list_output value: %v", config.ListOutput)
105+
}
106+
if !config.ReadOnly {
107+
t.Fatalf("Unexpected read-only mode: %v", config.ReadOnly)
108+
}
109+
if config.DisableDestructive {
110+
t.Fatalf("Unexpected disable destructive: %v", config.DisableDestructive)
111+
}
81112
})
82113
}
83114

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

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ type MCPServerOptions struct {
6262

6363
func NewMCPServerOptions(streams genericiooptions.IOStreams) *MCPServerOptions {
6464
return &MCPServerOptions{
65-
IOStreams: streams,
66-
Profile: "full",
67-
ListOutput: "table",
65+
IOStreams: streams,
66+
Profile: "full",
67+
ListOutput: "table",
68+
StaticConfig: &config.StaticConfig{},
6869
}
6970
}
7071

@@ -76,7 +77,7 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
7677
Long: long,
7778
Example: examples,
7879
RunE: func(c *cobra.Command, args []string) error {
79-
if err := o.Complete(); err != nil {
80+
if err := o.Complete(c); err != nil {
8081
return err
8182
}
8283
if err := o.Validate(); err != nil {
@@ -98,16 +99,14 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
9899
cmd.Flags().StringVar(&o.SSEBaseUrl, "sse-base-url", o.SSEBaseUrl, "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
99100
cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
100101
cmd.Flags().StringVar(&o.Profile, "profile", o.Profile, "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+")")
101-
cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+")")
102+
cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+"). Defaults to table.")
102103
cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
103104
cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled")
104105

105106
return cmd
106107
}
107108

108-
func (m *MCPServerOptions) Complete() error {
109-
m.initializeLogging()
110-
109+
func (m *MCPServerOptions) Complete(cmd *cobra.Command) error {
111110
if m.ConfigPath != "" {
112111
cnf, err := config.ReadConfig(m.ConfigPath)
113112
if err != nil {
@@ -116,16 +115,47 @@ func (m *MCPServerOptions) Complete() error {
116115
m.StaticConfig = cnf
117116
}
118117

118+
m.loadFlags(cmd)
119+
120+
m.initializeLogging()
121+
119122
return nil
120123
}
121124

125+
func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) {
126+
if cmd.Flag("log-level").Changed {
127+
m.StaticConfig.LogLevel = m.LogLevel
128+
}
129+
if cmd.Flag("sse-port").Changed {
130+
m.StaticConfig.SSEPort = m.SSEPort
131+
}
132+
if cmd.Flag("http-port").Changed {
133+
m.StaticConfig.HTTPPort = m.HttpPort
134+
}
135+
if cmd.Flag("sse-base-url").Changed {
136+
m.StaticConfig.SSEBaseURL = m.SSEBaseUrl
137+
}
138+
if cmd.Flag("kubeconfig").Changed {
139+
m.StaticConfig.KubeConfig = m.Kubeconfig
140+
}
141+
if cmd.Flag("list-output").Changed || m.StaticConfig.ListOutput == "" {
142+
m.StaticConfig.ListOutput = m.ListOutput
143+
}
144+
if cmd.Flag("read-only").Changed {
145+
m.StaticConfig.ReadOnly = m.ReadOnly
146+
}
147+
if cmd.Flag("disable-destructive").Changed {
148+
m.StaticConfig.DisableDestructive = m.DisableDestructive
149+
}
150+
}
151+
122152
func (m *MCPServerOptions) initializeLogging() {
123153
flagSet := flag.NewFlagSet("klog", flag.ContinueOnError)
124154
klog.InitFlags(flagSet)
125155
loggerOptions := []textlogger.ConfigOption{textlogger.Output(m.Out)}
126-
if m.LogLevel >= 0 {
127-
loggerOptions = append(loggerOptions, textlogger.Verbosity(m.LogLevel))
128-
_ = flagSet.Parse([]string{"--v", strconv.Itoa(m.LogLevel)})
156+
if m.StaticConfig.LogLevel >= 0 {
157+
loggerOptions = append(loggerOptions, textlogger.Verbosity(m.StaticConfig.LogLevel))
158+
_ = flagSet.Parse([]string{"--v", strconv.Itoa(m.StaticConfig.LogLevel)})
129159
}
130160
logger := textlogger.NewLogger(textlogger.NewConfig(loggerOptions...))
131161
klog.SetLoggerWithOptions(logger)
@@ -140,16 +170,16 @@ func (m *MCPServerOptions) Run() error {
140170
if profile == nil {
141171
return fmt.Errorf("Invalid profile name: %s, valid names are: %s\n", m.Profile, strings.Join(mcp.ProfileNames, ", "))
142172
}
143-
listOutput := output.FromString(m.ListOutput)
173+
listOutput := output.FromString(m.StaticConfig.ListOutput)
144174
if listOutput == nil {
145-
return fmt.Errorf("Invalid output name: %s, valid names are: %s\n", m.ListOutput, strings.Join(output.Names, ", "))
175+
return fmt.Errorf("Invalid output name: %s, valid names are: %s\n", m.StaticConfig.ListOutput, strings.Join(output.Names, ", "))
146176
}
147177
klog.V(1).Info("Starting kubernetes-mcp-server")
148178
klog.V(1).Infof(" - Config: %s", m.ConfigPath)
149179
klog.V(1).Infof(" - Profile: %s", profile.GetName())
150180
klog.V(1).Infof(" - ListOutput: %s", listOutput.GetName())
151-
klog.V(1).Infof(" - Read-only mode: %t", m.ReadOnly)
152-
klog.V(1).Infof(" - Disable destructive tools: %t", m.DisableDestructive)
181+
klog.V(1).Infof(" - Read-only mode: %t", m.StaticConfig.ReadOnly)
182+
klog.V(1).Infof(" - Disable destructive tools: %t", m.StaticConfig.DisableDestructive)
153183

154184
if m.Version {
155185
_, _ = fmt.Fprintf(m.Out, "%s\n", version.Version)
@@ -158,9 +188,9 @@ func (m *MCPServerOptions) Run() error {
158188
mcpServer, err := mcp.NewServer(mcp.Configuration{
159189
Profile: profile,
160190
ListOutput: listOutput,
161-
ReadOnly: m.ReadOnly,
162-
DisableDestructive: m.DisableDestructive,
163-
Kubeconfig: m.Kubeconfig,
191+
ReadOnly: m.StaticConfig.ReadOnly,
192+
DisableDestructive: m.StaticConfig.DisableDestructive,
193+
Kubeconfig: m.StaticConfig.KubeConfig,
164194
StaticConfig: m.StaticConfig,
165195
})
166196
if err != nil {
@@ -170,19 +200,19 @@ func (m *MCPServerOptions) Run() error {
170200

171201
ctx := context.Background()
172202

173-
if m.SSEPort > 0 {
174-
sseServer := mcpServer.ServeSse(m.SSEBaseUrl)
203+
if m.StaticConfig.SSEPort > 0 {
204+
sseServer := mcpServer.ServeSse(m.StaticConfig.SSEBaseURL)
175205
defer func() { _ = sseServer.Shutdown(ctx) }()
176-
klog.V(0).Infof("SSE server starting on port %d and path /sse", m.SSEPort)
177-
if err := sseServer.Start(fmt.Sprintf(":%d", m.SSEPort)); err != nil {
206+
klog.V(0).Infof("SSE server starting on port %d and path /sse", m.StaticConfig.SSEPort)
207+
if err := sseServer.Start(fmt.Sprintf(":%d", m.StaticConfig.SSEPort)); err != nil {
178208
return fmt.Errorf("failed to start SSE server: %w\n", err)
179209
}
180210
}
181211

182-
if m.HttpPort > 0 {
212+
if m.StaticConfig.HTTPPort > 0 {
183213
httpServer := mcpServer.ServeHTTP()
184-
klog.V(0).Infof("Streaming HTTP server starting on port %d and path /mcp", m.HttpPort)
185-
if err := httpServer.Start(fmt.Sprintf(":%d", m.HttpPort)); err != nil {
214+
klog.V(0).Infof("Streaming HTTP server starting on port %d and path /mcp", m.StaticConfig.HTTPPort)
215+
if err := httpServer.Start(fmt.Sprintf(":%d", m.StaticConfig.HTTPPort)); err != nil {
186216
return fmt.Errorf("failed to start streaming HTTP server: %w\n", err)
187217
}
188218
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,54 @@ func TestConfig(t *testing.T) {
7979
t.Fatalf("Expected error to be %s, got %s", expected, err.Error())
8080
}
8181
})
82+
t.Run("set with valid --config", func(t *testing.T) {
83+
ioStreams, out := testStream()
84+
rootCmd := NewMCPServer(ioStreams)
85+
_, file, _, _ := runtime.Caller(0)
86+
validConfigPath := filepath.Join(filepath.Dir(file), "testdata", "valid-config.toml")
87+
rootCmd.SetArgs([]string{"--version", "--config", validConfigPath})
88+
_ = rootCmd.Execute()
89+
expectedConfig := `(?m)\" - Config\:[^\"]+valid-config\.toml\"`
90+
if m, err := regexp.MatchString(expectedConfig, out.String()); !m || err != nil {
91+
t.Fatalf("Expected config to be %s, got %s %v", expectedConfig, out.String(), err)
92+
}
93+
expectedListOutput := `(?m)\" - ListOutput\: yaml"`
94+
if m, err := regexp.MatchString(expectedListOutput, out.String()); !m || err != nil {
95+
t.Fatalf("Expected config to be %s, got %s %v", expectedListOutput, out.String(), err)
96+
}
97+
expectedReadOnly := `(?m)\" - Read-only mode: true"`
98+
if m, err := regexp.MatchString(expectedReadOnly, out.String()); !m || err != nil {
99+
t.Fatalf("Expected config to be %s, got %s %v", expectedReadOnly, out.String(), err)
100+
}
101+
expectedDisableDestruction := `(?m)\" - Disable destructive tools: true"`
102+
if m, err := regexp.MatchString(expectedDisableDestruction, out.String()); !m || err != nil {
103+
t.Fatalf("Expected config to be %s, got %s %v", expectedDisableDestruction, out.String(), err)
104+
}
105+
})
106+
t.Run("set with valid --config, flags override", func(t *testing.T) {
107+
ioStreams, out := testStream()
108+
rootCmd := NewMCPServer(ioStreams)
109+
_, file, _, _ := runtime.Caller(0)
110+
validConfigPath := filepath.Join(filepath.Dir(file), "testdata", "valid-config.toml")
111+
rootCmd.SetArgs([]string{"--version", "--list-output=table", "--disable-destructive=false", "--read-only=false", "--config", validConfigPath})
112+
_ = rootCmd.Execute()
113+
expected := `(?m)\" - Config\:[^\"]+valid-config\.toml\"`
114+
if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
115+
t.Fatalf("Expected config to be %s, got %s %v", expected, out.String(), err)
116+
}
117+
expectedListOutput := `(?m)\" - ListOutput\: table"`
118+
if m, err := regexp.MatchString(expectedListOutput, out.String()); !m || err != nil {
119+
t.Fatalf("Expected config to be %s, got %s %v", expectedListOutput, out.String(), err)
120+
}
121+
expectedReadOnly := `(?m)\" - Read-only mode: false"`
122+
if m, err := regexp.MatchString(expectedReadOnly, out.String()); !m || err != nil {
123+
t.Fatalf("Expected config to be %s, got %s %v", expectedReadOnly, out.String(), err)
124+
}
125+
expectedDisableDestruction := `(?m)\" - Disable destructive tools: false"`
126+
if m, err := regexp.MatchString(expectedDisableDestruction, out.String()); !m || err != nil {
127+
t.Fatalf("Expected config to be %s, got %s %v", expectedDisableDestruction, out.String(), err)
128+
}
129+
})
82130
}
83131

84132
func TestProfile(t *testing.T) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
log_level = 1
2+
sse_port = 9999
3+
kubeconfig = "test"
4+
list_output = "yaml"
5+
read_only = true
6+
disable_destructive = true
7+
8+
[[denied_resources]]
9+
group = "apps"
10+
version = "v1"
11+
kind = "Deployment"
12+
13+
[[denied_resources]]
14+
group = "rbac.authorization.k8s.io"
15+
version = "v1"

0 commit comments

Comments
 (0)