feat(config): define flags in configuration file (152)

Define flags in configuration file
---
Add vscode in .gitignore
This commit is contained in:
Arda Güçlü
2025-07-01 10:39:38 +03:00
committed by GitHub
parent b777972c14
commit 2a1a3e4fbd
6 changed files with 159 additions and 25 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea/ .idea/
.vscode/
.docusaurus/ .docusaurus/
node_modules/ node_modules/

View File

@@ -8,6 +8,15 @@ import (
type StaticConfig struct { type StaticConfig struct {
DeniedResources []GroupVersionKind `toml:"denied_resources"` DeniedResources []GroupVersionKind `toml:"denied_resources"`
LogLevel int `toml:"log_level,omitempty"`
SSEPort int `toml:"sse_port,omitempty"`
HTTPPort int `toml:"http_port,omitempty"`
SSEBaseURL string `toml:"sse_base_url,omitempty"`
KubeConfig string `toml:"kubeconfig,omitempty"`
ListOutput string `toml:"list_output,omitempty"`
ReadOnly bool `toml:"read_only,omitempty"`
DisableDestructive bool `toml:"disable_destructive,omitempty"`
} }
type GroupVersionKind struct { type GroupVersionKind struct {

View File

@@ -50,6 +50,13 @@ kind = "Role
func TestReadConfigValid(t *testing.T) { func TestReadConfigValid(t *testing.T) {
validConfigPath := writeConfig(t, ` validConfigPath := writeConfig(t, `
log_level = 1
sse_port = 9999
kubeconfig = "test"
list_output = "yaml"
read_only = true
disable_destructive = false
[[denied_resources]] [[denied_resources]]
group = "apps" group = "apps"
version = "v1" version = "v1"
@@ -78,6 +85,30 @@ version = "v1"
config.DeniedResources[0].Kind != "Deployment" { config.DeniedResources[0].Kind != "Deployment" {
t.Errorf("Unexpected denied resources: %v", config.DeniedResources[0]) t.Errorf("Unexpected denied resources: %v", config.DeniedResources[0])
} }
if config.LogLevel != 1 {
t.Fatalf("Unexpected log level: %v", config.LogLevel)
}
if config.SSEPort != 9999 {
t.Fatalf("Unexpected sse_port value: %v", config.SSEPort)
}
if config.SSEBaseURL != "" {
t.Fatalf("Unexpected sse_base_url value: %v", config.SSEBaseURL)
}
if config.HTTPPort != 0 {
t.Fatalf("Unexpected http_port value: %v", config.HTTPPort)
}
if config.KubeConfig != "test" {
t.Fatalf("Unexpected kubeconfig value: %v", config.KubeConfig)
}
if config.ListOutput != "yaml" {
t.Fatalf("Unexpected list_output value: %v", config.ListOutput)
}
if !config.ReadOnly {
t.Fatalf("Unexpected read-only mode: %v", config.ReadOnly)
}
if config.DisableDestructive {
t.Fatalf("Unexpected disable destructive: %v", config.DisableDestructive)
}
}) })
} }

View File

@@ -62,9 +62,10 @@ type MCPServerOptions struct {
func NewMCPServerOptions(streams genericiooptions.IOStreams) *MCPServerOptions { func NewMCPServerOptions(streams genericiooptions.IOStreams) *MCPServerOptions {
return &MCPServerOptions{ return &MCPServerOptions{
IOStreams: streams, IOStreams: streams,
Profile: "full", Profile: "full",
ListOutput: "table", ListOutput: "table",
StaticConfig: &config.StaticConfig{},
} }
} }
@@ -76,7 +77,7 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
Long: long, Long: long,
Example: examples, Example: examples,
RunE: func(c *cobra.Command, args []string) error { RunE: func(c *cobra.Command, args []string) error {
if err := o.Complete(); err != nil { if err := o.Complete(c); err != nil {
return err return err
} }
if err := o.Validate(); err != nil { if err := o.Validate(); err != nil {
@@ -98,16 +99,14 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
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)") 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)")
cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication") cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
cmd.Flags().StringVar(&o.Profile, "profile", o.Profile, "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+")") cmd.Flags().StringVar(&o.Profile, "profile", o.Profile, "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+")")
cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+")") cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+"). Defaults to table.")
cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed") cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled") cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled")
return cmd return cmd
} }
func (m *MCPServerOptions) Complete() error { func (m *MCPServerOptions) Complete(cmd *cobra.Command) error {
m.initializeLogging()
if m.ConfigPath != "" { if m.ConfigPath != "" {
cnf, err := config.ReadConfig(m.ConfigPath) cnf, err := config.ReadConfig(m.ConfigPath)
if err != nil { if err != nil {
@@ -116,16 +115,47 @@ func (m *MCPServerOptions) Complete() error {
m.StaticConfig = cnf m.StaticConfig = cnf
} }
m.loadFlags(cmd)
m.initializeLogging()
return nil return nil
} }
func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) {
if cmd.Flag("log-level").Changed {
m.StaticConfig.LogLevel = m.LogLevel
}
if cmd.Flag("sse-port").Changed {
m.StaticConfig.SSEPort = m.SSEPort
}
if cmd.Flag("http-port").Changed {
m.StaticConfig.HTTPPort = m.HttpPort
}
if cmd.Flag("sse-base-url").Changed {
m.StaticConfig.SSEBaseURL = m.SSEBaseUrl
}
if cmd.Flag("kubeconfig").Changed {
m.StaticConfig.KubeConfig = m.Kubeconfig
}
if cmd.Flag("list-output").Changed || m.StaticConfig.ListOutput == "" {
m.StaticConfig.ListOutput = m.ListOutput
}
if cmd.Flag("read-only").Changed {
m.StaticConfig.ReadOnly = m.ReadOnly
}
if cmd.Flag("disable-destructive").Changed {
m.StaticConfig.DisableDestructive = m.DisableDestructive
}
}
func (m *MCPServerOptions) initializeLogging() { func (m *MCPServerOptions) initializeLogging() {
flagSet := flag.NewFlagSet("klog", flag.ContinueOnError) flagSet := flag.NewFlagSet("klog", flag.ContinueOnError)
klog.InitFlags(flagSet) klog.InitFlags(flagSet)
loggerOptions := []textlogger.ConfigOption{textlogger.Output(m.Out)} loggerOptions := []textlogger.ConfigOption{textlogger.Output(m.Out)}
if m.LogLevel >= 0 { if m.StaticConfig.LogLevel >= 0 {
loggerOptions = append(loggerOptions, textlogger.Verbosity(m.LogLevel)) loggerOptions = append(loggerOptions, textlogger.Verbosity(m.StaticConfig.LogLevel))
_ = flagSet.Parse([]string{"--v", strconv.Itoa(m.LogLevel)}) _ = flagSet.Parse([]string{"--v", strconv.Itoa(m.StaticConfig.LogLevel)})
} }
logger := textlogger.NewLogger(textlogger.NewConfig(loggerOptions...)) logger := textlogger.NewLogger(textlogger.NewConfig(loggerOptions...))
klog.SetLoggerWithOptions(logger) klog.SetLoggerWithOptions(logger)
@@ -140,16 +170,16 @@ func (m *MCPServerOptions) Run() error {
if profile == nil { if profile == nil {
return fmt.Errorf("Invalid profile name: %s, valid names are: %s\n", m.Profile, strings.Join(mcp.ProfileNames, ", ")) return fmt.Errorf("Invalid profile name: %s, valid names are: %s\n", m.Profile, strings.Join(mcp.ProfileNames, ", "))
} }
listOutput := output.FromString(m.ListOutput) listOutput := output.FromString(m.StaticConfig.ListOutput)
if listOutput == nil { if listOutput == nil {
return fmt.Errorf("Invalid output name: %s, valid names are: %s\n", m.ListOutput, strings.Join(output.Names, ", ")) return fmt.Errorf("Invalid output name: %s, valid names are: %s\n", m.StaticConfig.ListOutput, strings.Join(output.Names, ", "))
} }
klog.V(1).Info("Starting kubernetes-mcp-server") klog.V(1).Info("Starting kubernetes-mcp-server")
klog.V(1).Infof(" - Config: %s", m.ConfigPath) klog.V(1).Infof(" - Config: %s", m.ConfigPath)
klog.V(1).Infof(" - Profile: %s", profile.GetName()) klog.V(1).Infof(" - Profile: %s", profile.GetName())
klog.V(1).Infof(" - ListOutput: %s", listOutput.GetName()) klog.V(1).Infof(" - ListOutput: %s", listOutput.GetName())
klog.V(1).Infof(" - Read-only mode: %t", m.ReadOnly) klog.V(1).Infof(" - Read-only mode: %t", m.StaticConfig.ReadOnly)
klog.V(1).Infof(" - Disable destructive tools: %t", m.DisableDestructive) klog.V(1).Infof(" - Disable destructive tools: %t", m.StaticConfig.DisableDestructive)
if m.Version { if m.Version {
_, _ = fmt.Fprintf(m.Out, "%s\n", version.Version) _, _ = fmt.Fprintf(m.Out, "%s\n", version.Version)
@@ -158,9 +188,9 @@ func (m *MCPServerOptions) Run() error {
mcpServer, err := mcp.NewServer(mcp.Configuration{ mcpServer, err := mcp.NewServer(mcp.Configuration{
Profile: profile, Profile: profile,
ListOutput: listOutput, ListOutput: listOutput,
ReadOnly: m.ReadOnly, ReadOnly: m.StaticConfig.ReadOnly,
DisableDestructive: m.DisableDestructive, DisableDestructive: m.StaticConfig.DisableDestructive,
Kubeconfig: m.Kubeconfig, Kubeconfig: m.StaticConfig.KubeConfig,
StaticConfig: m.StaticConfig, StaticConfig: m.StaticConfig,
}) })
if err != nil { if err != nil {
@@ -170,19 +200,19 @@ func (m *MCPServerOptions) Run() error {
ctx := context.Background() ctx := context.Background()
if m.SSEPort > 0 { if m.StaticConfig.SSEPort > 0 {
sseServer := mcpServer.ServeSse(m.SSEBaseUrl) sseServer := mcpServer.ServeSse(m.StaticConfig.SSEBaseURL)
defer func() { _ = sseServer.Shutdown(ctx) }() defer func() { _ = sseServer.Shutdown(ctx) }()
klog.V(0).Infof("SSE server starting on port %d and path /sse", m.SSEPort) klog.V(0).Infof("SSE server starting on port %d and path /sse", m.StaticConfig.SSEPort)
if err := sseServer.Start(fmt.Sprintf(":%d", m.SSEPort)); err != nil { if err := sseServer.Start(fmt.Sprintf(":%d", m.StaticConfig.SSEPort)); err != nil {
return fmt.Errorf("failed to start SSE server: %w\n", err) return fmt.Errorf("failed to start SSE server: %w\n", err)
} }
} }
if m.HttpPort > 0 { if m.StaticConfig.HTTPPort > 0 {
httpServer := mcpServer.ServeHTTP() httpServer := mcpServer.ServeHTTP()
klog.V(0).Infof("Streaming HTTP server starting on port %d and path /mcp", m.HttpPort) klog.V(0).Infof("Streaming HTTP server starting on port %d and path /mcp", m.StaticConfig.HTTPPort)
if err := httpServer.Start(fmt.Sprintf(":%d", m.HttpPort)); err != nil { if err := httpServer.Start(fmt.Sprintf(":%d", m.StaticConfig.HTTPPort)); err != nil {
return fmt.Errorf("failed to start streaming HTTP server: %w\n", err) return fmt.Errorf("failed to start streaming HTTP server: %w\n", err)
} }
} }

View File

@@ -79,6 +79,54 @@ func TestConfig(t *testing.T) {
t.Fatalf("Expected error to be %s, got %s", expected, err.Error()) t.Fatalf("Expected error to be %s, got %s", expected, err.Error())
} }
}) })
t.Run("set with valid --config", func(t *testing.T) {
ioStreams, out := testStream()
rootCmd := NewMCPServer(ioStreams)
_, file, _, _ := runtime.Caller(0)
validConfigPath := filepath.Join(filepath.Dir(file), "testdata", "valid-config.toml")
rootCmd.SetArgs([]string{"--version", "--config", validConfigPath})
_ = rootCmd.Execute()
expectedConfig := `(?m)\" - Config\:[^\"]+valid-config\.toml\"`
if m, err := regexp.MatchString(expectedConfig, out.String()); !m || err != nil {
t.Fatalf("Expected config to be %s, got %s %v", expectedConfig, out.String(), err)
}
expectedListOutput := `(?m)\" - ListOutput\: yaml"`
if m, err := regexp.MatchString(expectedListOutput, out.String()); !m || err != nil {
t.Fatalf("Expected config to be %s, got %s %v", expectedListOutput, out.String(), err)
}
expectedReadOnly := `(?m)\" - Read-only mode: true"`
if m, err := regexp.MatchString(expectedReadOnly, out.String()); !m || err != nil {
t.Fatalf("Expected config to be %s, got %s %v", expectedReadOnly, out.String(), err)
}
expectedDisableDestruction := `(?m)\" - Disable destructive tools: true"`
if m, err := regexp.MatchString(expectedDisableDestruction, out.String()); !m || err != nil {
t.Fatalf("Expected config to be %s, got %s %v", expectedDisableDestruction, out.String(), err)
}
})
t.Run("set with valid --config, flags override", func(t *testing.T) {
ioStreams, out := testStream()
rootCmd := NewMCPServer(ioStreams)
_, file, _, _ := runtime.Caller(0)
validConfigPath := filepath.Join(filepath.Dir(file), "testdata", "valid-config.toml")
rootCmd.SetArgs([]string{"--version", "--list-output=table", "--disable-destructive=false", "--read-only=false", "--config", validConfigPath})
_ = rootCmd.Execute()
expected := `(?m)\" - Config\:[^\"]+valid-config\.toml\"`
if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
t.Fatalf("Expected config to be %s, got %s %v", expected, out.String(), err)
}
expectedListOutput := `(?m)\" - ListOutput\: table"`
if m, err := regexp.MatchString(expectedListOutput, out.String()); !m || err != nil {
t.Fatalf("Expected config to be %s, got %s %v", expectedListOutput, out.String(), err)
}
expectedReadOnly := `(?m)\" - Read-only mode: false"`
if m, err := regexp.MatchString(expectedReadOnly, out.String()); !m || err != nil {
t.Fatalf("Expected config to be %s, got %s %v", expectedReadOnly, out.String(), err)
}
expectedDisableDestruction := `(?m)\" - Disable destructive tools: false"`
if m, err := regexp.MatchString(expectedDisableDestruction, out.String()); !m || err != nil {
t.Fatalf("Expected config to be %s, got %s %v", expectedDisableDestruction, out.String(), err)
}
})
} }
func TestProfile(t *testing.T) { func TestProfile(t *testing.T) {

View File

@@ -0,0 +1,15 @@
log_level = 1
sse_port = 9999
kubeconfig = "test"
list_output = "yaml"
read_only = true
disable_destructive = true
[[denied_resources]]
group = "apps"
version = "v1"
kind = "Deployment"
[[denied_resources]]
group = "rbac.authorization.k8s.io"
version = "v1"