Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
const (
ClusterProviderKubeConfig = "kubeconfig"
ClusterProviderInCluster = "in-cluster"
ClusterProviderDisabled = "disabled"
)

// StaticConfig is the configuration for the server.
Expand Down
121 changes: 79 additions & 42 deletions pkg/kubernetes-mcp-server/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,34 @@ kubernetes-mcp-server --port 8080

# start a SSE server on port 8443 with a public HTTPS host of example.com
kubernetes-mcp-server --port 8443 --sse-base-url https://example.com:8443

# start a SSE server on port 8080 with multi-cluster tools disabled
kubernetes-mcp-server --port 8080 --disable-multi-cluster
`))
)

const (
flagVersion = "version"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are nice, but I'd have perhaps done this in a separate PR before. Like a "pre factoring".

But since the change is small, this is adding not too much noise. Just as a general statement

flagLogLevel = "log-level"
flagConfig = "config"
flagSSEPort = "sse-port"
flagHttpPort = "http-port"
flagPort = "port"
flagSSEBaseUrl = "sse-base-url"
flagKubeconfig = "kubeconfig"
flagToolsets = "toolsets"
flagListOutput = "list-output"
flagReadOnly = "read-only"
flagDisableDestructive = "disable-destructive"
flagRequireOAuth = "require-oauth"
flagOAuthAudience = "oauth-audience"
flagValidateToken = "validate-token"
flagAuthorizationURL = "authorization-url"
flagServerUrl = "server-url"
flagCertificateAuthority = "certificate-authority"
flagDisableMultiCluster = "disable-multi-cluster"
)

type MCPServerOptions struct {
Version bool
LogLevel int
Expand All @@ -68,6 +93,7 @@ type MCPServerOptions struct {
AuthorizationURL string
CertificateAuthority string
ServerURL string
DisableMultiCluster bool

ConfigPath string
StaticConfig *config.StaticConfig
Expand Down Expand Up @@ -104,32 +130,33 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
},
}

cmd.Flags().BoolVar(&o.Version, "version", o.Version, "Print version information and quit")
cmd.Flags().IntVar(&o.LogLevel, "log-level", o.LogLevel, "Set the log level (from 0 to 9)")
cmd.Flags().StringVar(&o.ConfigPath, "config", o.ConfigPath, "Path of the config file.")
cmd.Flags().IntVar(&o.SSEPort, "sse-port", o.SSEPort, "Start a SSE server on the specified port")
cmd.Flag("sse-port").Deprecated = "Use --port instead"
cmd.Flags().IntVar(&o.HttpPort, "http-port", o.HttpPort, "Start a streamable HTTP server on the specified port")
cmd.Flag("http-port").Deprecated = "Use --port instead"
cmd.Flags().StringVar(&o.Port, "port", o.Port, "Start a streamable HTTP and SSE HTTP server on the specified port (e.g. 8080)")
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().StringSliceVar(&o.Toolsets, "toolsets", o.Toolsets, "Comma-separated list of MCP toolsets to use (available toolsets: "+strings.Join(toolsets.ToolsetNames(), ", ")+"). Defaults to "+strings.Join(o.StaticConfig.Toolsets, ", ")+".")
cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+"). Defaults to "+o.StaticConfig.ListOutput+".")
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.RequireOAuth, "require-oauth", o.RequireOAuth, "If true, requires OAuth authorization as defined in the Model Context Protocol (MCP) specification. This flag is ignored if transport type is stdio")
_ = cmd.Flags().MarkHidden("require-oauth")
cmd.Flags().StringVar(&o.OAuthAudience, "oauth-audience", o.OAuthAudience, "OAuth audience for token claims validation. Optional. If not set, the audience is not validated. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden("oauth-audience")
cmd.Flags().BoolVar(&o.ValidateToken, "validate-token", o.ValidateToken, "If true, validates the token against the Kubernetes API Server using TokenReview. Optional. If not set, the token is not validated. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden("validate-token")
cmd.Flags().StringVar(&o.AuthorizationURL, "authorization-url", o.AuthorizationURL, "OAuth authorization server URL for protected resource endpoint. If not provided, the Kubernetes API server host will be used. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden("authorization-url")
cmd.Flags().StringVar(&o.ServerURL, "server-url", o.ServerURL, "Server URL of this application. Optional. If set, this url will be served in protected resource metadata endpoint and tokens will be validated with this audience. If not set, expected audience is kubernetes-mcp-server. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden("server-url")
cmd.Flags().StringVar(&o.CertificateAuthority, "certificate-authority", o.CertificateAuthority, "Certificate authority path to verify certificates. Optional. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden("certificate-authority")
cmd.Flags().BoolVar(&o.Version, flagVersion, o.Version, "Print version information and quit")
cmd.Flags().IntVar(&o.LogLevel, flagLogLevel, o.LogLevel, "Set the log level (from 0 to 9)")
cmd.Flags().StringVar(&o.ConfigPath, flagConfig, o.ConfigPath, "Path of the config file.")
cmd.Flags().IntVar(&o.SSEPort, flagSSEPort, o.SSEPort, "Start a SSE server on the specified port")
cmd.Flag(flagSSEPort).Deprecated = "Use --port instead"
cmd.Flags().IntVar(&o.HttpPort, flagHttpPort, o.HttpPort, "Start a streamable HTTP server on the specified port")
cmd.Flag(flagHttpPort).Deprecated = "Use --port instead"
cmd.Flags().StringVar(&o.Port, flagPort, o.Port, "Start a streamable HTTP and SSE HTTP server on the specified port (e.g. 8080)")
cmd.Flags().StringVar(&o.SSEBaseUrl, flagSSEBaseUrl, o.SSEBaseUrl, "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
cmd.Flags().StringVar(&o.Kubeconfig, flagKubeconfig, o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
cmd.Flags().StringSliceVar(&o.Toolsets, flagToolsets, o.Toolsets, "Comma-separated list of MCP toolsets to use (available toolsets: "+strings.Join(toolsets.ToolsetNames(), ", ")+"). Defaults to "+strings.Join(o.StaticConfig.Toolsets, ", ")+".")
cmd.Flags().StringVar(&o.ListOutput, flagListOutput, o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+"). Defaults to "+o.StaticConfig.ListOutput+".")
cmd.Flags().BoolVar(&o.ReadOnly, flagReadOnly, o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
cmd.Flags().BoolVar(&o.DisableDestructive, flagDisableDestructive, o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled")
cmd.Flags().BoolVar(&o.RequireOAuth, flagRequireOAuth, o.RequireOAuth, "If true, requires OAuth authorization as defined in the Model Context Protocol (MCP) specification. This flag is ignored if transport type is stdio")
_ = cmd.Flags().MarkHidden(flagRequireOAuth)
cmd.Flags().StringVar(&o.OAuthAudience, flagOAuthAudience, o.OAuthAudience, "OAuth audience for token claims validation. Optional. If not set, the audience is not validated. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden(flagOAuthAudience)
cmd.Flags().BoolVar(&o.ValidateToken, flagValidateToken, o.ValidateToken, "If true, validates the token against the Kubernetes API Server using TokenReview. Optional. If not set, the token is not validated. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden(flagValidateToken)
cmd.Flags().StringVar(&o.AuthorizationURL, flagAuthorizationURL, o.AuthorizationURL, "OAuth authorization server URL for protected resource endpoint. If not provided, the Kubernetes API server host will be used. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden(flagAuthorizationURL)
cmd.Flags().StringVar(&o.ServerURL, flagServerUrl, o.ServerURL, "Server URL of this application. Optional. If set, this url will be served in protected resource metadata endpoint and tokens will be validated with this audience. If not set, expected audience is kubernetes-mcp-server. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden(flagServerUrl)
cmd.Flags().StringVar(&o.CertificateAuthority, flagCertificateAuthority, o.CertificateAuthority, "Certificate authority path to verify certificates. Optional. Only valid if require-oauth is enabled.")
_ = cmd.Flags().MarkHidden(flagCertificateAuthority)
cmd.Flags().BoolVar(&o.DisableMultiCluster, flagDisableMultiCluster, o.DisableMultiCluster, "Disable multi cluster tools. Optional. If true, all tools will be run against the default cluster/context.")

return cmd
}
Expand All @@ -156,52 +183,55 @@ func (m *MCPServerOptions) Complete(cmd *cobra.Command) error {
}

func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) {
if cmd.Flag("log-level").Changed {
if cmd.Flag(flagLogLevel).Changed {
m.StaticConfig.LogLevel = m.LogLevel
}
if cmd.Flag("port").Changed {
if cmd.Flag(flagPort).Changed {
m.StaticConfig.Port = m.Port
} else if cmd.Flag("sse-port").Changed {
} else if cmd.Flag(flagSSEPort).Changed {
m.StaticConfig.Port = strconv.Itoa(m.SSEPort)
} else if cmd.Flag("http-port").Changed {
} else if cmd.Flag(flagHttpPort).Changed {
m.StaticConfig.Port = strconv.Itoa(m.HttpPort)
}
if cmd.Flag("sse-base-url").Changed {
if cmd.Flag(flagSSEBaseUrl).Changed {
m.StaticConfig.SSEBaseURL = m.SSEBaseUrl
}
if cmd.Flag("kubeconfig").Changed {
if cmd.Flag(flagKubeconfig).Changed {
m.StaticConfig.KubeConfig = m.Kubeconfig
}
if cmd.Flag("list-output").Changed {
if cmd.Flag(flagListOutput).Changed {
m.StaticConfig.ListOutput = m.ListOutput
}
if cmd.Flag("read-only").Changed {
if cmd.Flag(flagReadOnly).Changed {
m.StaticConfig.ReadOnly = m.ReadOnly
}
if cmd.Flag("disable-destructive").Changed {
if cmd.Flag(flagDisableDestructive).Changed {
m.StaticConfig.DisableDestructive = m.DisableDestructive
}
if cmd.Flag("toolsets").Changed {
if cmd.Flag(flagToolsets).Changed {
m.StaticConfig.Toolsets = m.Toolsets
}
if cmd.Flag("require-oauth").Changed {
if cmd.Flag(flagRequireOAuth).Changed {
m.StaticConfig.RequireOAuth = m.RequireOAuth
}
if cmd.Flag("oauth-audience").Changed {
if cmd.Flag(flagOAuthAudience).Changed {
m.StaticConfig.OAuthAudience = m.OAuthAudience
}
if cmd.Flag("validate-token").Changed {
if cmd.Flag(flagValidateToken).Changed {
m.StaticConfig.ValidateToken = m.ValidateToken
}
if cmd.Flag("authorization-url").Changed {
if cmd.Flag(flagAuthorizationURL).Changed {
m.StaticConfig.AuthorizationURL = m.AuthorizationURL
}
if cmd.Flag("server-url").Changed {
if cmd.Flag(flagServerUrl).Changed {
m.StaticConfig.ServerURL = m.ServerURL
}
if cmd.Flag("certificate-authority").Changed {
if cmd.Flag(flagCertificateAuthority).Changed {
m.StaticConfig.CertificateAuthority = m.CertificateAuthority
}
if cmd.Flag(flagDisableMultiCluster).Changed && m.DisableMultiCluster {
m.StaticConfig.ClusterProviderStrategy = config.ClusterProviderDisabled
}
}

func (m *MCPServerOptions) initializeLogging() {
Expand Down Expand Up @@ -258,6 +288,13 @@ func (m *MCPServerOptions) Run() error {
klog.V(1).Infof(" - Read-only mode: %t", m.StaticConfig.ReadOnly)
klog.V(1).Infof(" - Disable destructive tools: %t", m.StaticConfig.DisableDestructive)

strategy := m.StaticConfig.ClusterProviderStrategy
if strategy == "" {
strategy = "auto-detect (it is recommended to set this explicitly in your Config)"
}

klog.V(1).Infof(" - ClusterProviderStrategy: %s", strategy)

if m.Version {
_, _ = fmt.Fprintf(m.Out, "%s\n", version.Version)
return nil
Expand Down
21 changes: 21 additions & 0 deletions pkg/kubernetes-mcp-server/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,24 @@ func TestStdioLogging(t *testing.T) {
assert.Containsf(t, out.String(), "Starting kubernetes-mcp-server", "Expected klog output, got %s", out.String())
})
}

func TestDisableMultiCluster(t *testing.T) {
t.Run("defaults to false", func(t *testing.T) {
ioStreams, out := testStream()
rootCmd := NewMCPServer(ioStreams)
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1"})
if err := rootCmd.Execute(); !strings.Contains(out.String(), " - ClusterProviderStrategy: auto-detect (it is recommended to set this explicitly in your Config)") {
t.Fatalf("Expected ClusterProviderStrategy kubeconfig, got %s %v", out, err)
}
})
t.Run("set with --disable-multi-cluster", func(t *testing.T) {
ioStreams, out := testStream()
rootCmd := NewMCPServer(ioStreams)
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--disable-multi-cluster"})
_ = rootCmd.Execute()
expected := `(?m)\" - ClusterProviderStrategy\: disabled\"`
if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
t.Fatalf("Expected ClusterProviderStrategy %s, got %s %v", expected, out.String(), err)
}
})
}
50 changes: 30 additions & 20 deletions pkg/kubernetes/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,30 @@ type kubeConfigClusterProvider struct {

var _ ManagerProvider = &kubeConfigClusterProvider{}

type inClusterProvider struct {
manager *Manager
type singleClusterProvider struct {
strategy string
manager *Manager
}

var _ ManagerProvider = &inClusterProvider{}
var _ ManagerProvider = &singleClusterProvider{}

func NewManagerProvider(cfg *config.StaticConfig) (ManagerProvider, error) {
m, err := NewManager(cfg)
if err != nil {
return nil, err
}

switch resolveStrategy(cfg, m) {
strategy := resolveStrategy(cfg, m)
switch strategy {
case config.ClusterProviderKubeConfig:
return newKubeConfigClusterProvider(m)
case config.ClusterProviderInCluster:
return newInClusterProvider(m)
case config.ClusterProviderInCluster, config.ClusterProviderDisabled:
return newSingleClusterProvider(m, strategy)
default:
return nil, fmt.Errorf("invalid ClusterProviderStrategy '%s', must be 'kubeconfig' or 'in-cluster'", cfg.ClusterProviderStrategy)
return nil, fmt.Errorf(
"invalid ClusterProviderStrategy '%s', must be 'kubeconfig', 'in-cluster', or 'disabled'",
strategy,
)
}
}

Expand Down Expand Up @@ -83,9 +88,14 @@ func newKubeConfigClusterProvider(m *Manager) (*kubeConfigClusterProvider, error
}, nil
}

func newInClusterProvider(m *Manager) (*inClusterProvider, error) {
return &inClusterProvider{
manager: m,
func newSingleClusterProvider(m *Manager, strategy string) (*singleClusterProvider, error) {
if strategy == config.ClusterProviderInCluster && !m.IsInCluster() {
return nil, fmt.Errorf("server must be deployed in cluster for the in-cluster ClusterProviderStrategy")
}

return &singleClusterProvider{
manager: m,
strategy: strategy,
}, nil
}

Expand Down Expand Up @@ -141,32 +151,32 @@ func (k *kubeConfigClusterProvider) Close() {
m.Close()
}

func (i *inClusterProvider) GetTargets(ctx context.Context) ([]string, error) {
func (s *singleClusterProvider) GetTargets(ctx context.Context) ([]string, error) {
return []string{""}, nil
}

func (i *inClusterProvider) GetManagerFor(ctx context.Context, target string) (*Manager, error) {
func (s *singleClusterProvider) GetManagerFor(ctx context.Context, target string) (*Manager, error) {
if target != "" {
return nil, fmt.Errorf("unable to get manager for other context/cluster with in-cluster strategy")
return nil, fmt.Errorf("unable to get manager for other context/cluster with %s strategy", s.strategy)
}

return i.manager, nil
return s.manager, nil
}

func (i *inClusterProvider) GetDefaultTarget() string {
func (s *singleClusterProvider) GetDefaultTarget() string {
return ""
}

func (i *inClusterProvider) GetTargetParameterName() string {
func (s *singleClusterProvider) GetTargetParameterName() string {
return ""
}

func (i *inClusterProvider) WatchTargets(watch func() error) {
i.manager.WatchKubeConfig(watch)
func (s *singleClusterProvider) WatchTargets(watch func() error) {
s.manager.WatchKubeConfig(watch)
}

func (i *inClusterProvider) Close() {
i.manager.Close()
func (s *singleClusterProvider) Close() {
s.manager.Close()
}

func (m *Manager) newForContext(context string) (*Manager, error) {
Expand Down
Loading