Skip to content

Commit a056981

Browse files
authored
feat(config): add "disabled" mutli cluster strategy (#360)
* feat: add 'disabled' ClusterProviderStrategy Signed-off-by: Calum Murray <[email protected]> * feat: add --disable-multi-cluster flag Signed-off-by: Calum Murray <[email protected]> * test: check that --disable-multi-cluster flag changes config Signed-off-by: Calum Murray <[email protected]> * refactor: move flag names to constants Signed-off-by: Calum Murray <[email protected]> * fix(test): correct subtest name Signed-off-by: Calum Murray <[email protected]> * fix: explicit clusterproviderstrategy is now recommended, instead of advisable Signed-off-by: Calum Murray <[email protected]> --------- Signed-off-by: Calum Murray <[email protected]>
1 parent 61eaecc commit a056981

File tree

4 files changed

+131
-62
lines changed

4 files changed

+131
-62
lines changed

pkg/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
const (
1010
ClusterProviderKubeConfig = "kubeconfig"
1111
ClusterProviderInCluster = "in-cluster"
12+
ClusterProviderDisabled = "disabled"
1213
)
1314

1415
// StaticConfig is the configuration for the server.

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

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,34 @@ kubernetes-mcp-server --port 8080
4747
4848
# start a SSE server on port 8443 with a public HTTPS host of example.com
4949
kubernetes-mcp-server --port 8443 --sse-base-url https://example.com:8443
50+
51+
# start a SSE server on port 8080 with multi-cluster tools disabled
52+
kubernetes-mcp-server --port 8080 --disable-multi-cluster
5053
`))
5154
)
5255

56+
const (
57+
flagVersion = "version"
58+
flagLogLevel = "log-level"
59+
flagConfig = "config"
60+
flagSSEPort = "sse-port"
61+
flagHttpPort = "http-port"
62+
flagPort = "port"
63+
flagSSEBaseUrl = "sse-base-url"
64+
flagKubeconfig = "kubeconfig"
65+
flagToolsets = "toolsets"
66+
flagListOutput = "list-output"
67+
flagReadOnly = "read-only"
68+
flagDisableDestructive = "disable-destructive"
69+
flagRequireOAuth = "require-oauth"
70+
flagOAuthAudience = "oauth-audience"
71+
flagValidateToken = "validate-token"
72+
flagAuthorizationURL = "authorization-url"
73+
flagServerUrl = "server-url"
74+
flagCertificateAuthority = "certificate-authority"
75+
flagDisableMultiCluster = "disable-multi-cluster"
76+
)
77+
5378
type MCPServerOptions struct {
5479
Version bool
5580
LogLevel int
@@ -68,6 +93,7 @@ type MCPServerOptions struct {
6893
AuthorizationURL string
6994
CertificateAuthority string
7095
ServerURL string
96+
DisableMultiCluster bool
7197

7298
ConfigPath string
7399
StaticConfig *config.StaticConfig
@@ -104,32 +130,33 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
104130
},
105131
}
106132

107-
cmd.Flags().BoolVar(&o.Version, "version", o.Version, "Print version information and quit")
108-
cmd.Flags().IntVar(&o.LogLevel, "log-level", o.LogLevel, "Set the log level (from 0 to 9)")
109-
cmd.Flags().StringVar(&o.ConfigPath, "config", o.ConfigPath, "Path of the config file.")
110-
cmd.Flags().IntVar(&o.SSEPort, "sse-port", o.SSEPort, "Start a SSE server on the specified port")
111-
cmd.Flag("sse-port").Deprecated = "Use --port instead"
112-
cmd.Flags().IntVar(&o.HttpPort, "http-port", o.HttpPort, "Start a streamable HTTP server on the specified port")
113-
cmd.Flag("http-port").Deprecated = "Use --port instead"
114-
cmd.Flags().StringVar(&o.Port, "port", o.Port, "Start a streamable HTTP and SSE HTTP server on the specified port (e.g. 8080)")
115-
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)")
116-
cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
117-
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, ", ")+".")
118-
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+".")
119-
cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
120-
cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled")
121-
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")
122-
_ = cmd.Flags().MarkHidden("require-oauth")
123-
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.")
124-
_ = cmd.Flags().MarkHidden("oauth-audience")
125-
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.")
126-
_ = cmd.Flags().MarkHidden("validate-token")
127-
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.")
128-
_ = cmd.Flags().MarkHidden("authorization-url")
129-
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.")
130-
_ = cmd.Flags().MarkHidden("server-url")
131-
cmd.Flags().StringVar(&o.CertificateAuthority, "certificate-authority", o.CertificateAuthority, "Certificate authority path to verify certificates. Optional. Only valid if require-oauth is enabled.")
132-
_ = cmd.Flags().MarkHidden("certificate-authority")
133+
cmd.Flags().BoolVar(&o.Version, flagVersion, o.Version, "Print version information and quit")
134+
cmd.Flags().IntVar(&o.LogLevel, flagLogLevel, o.LogLevel, "Set the log level (from 0 to 9)")
135+
cmd.Flags().StringVar(&o.ConfigPath, flagConfig, o.ConfigPath, "Path of the config file.")
136+
cmd.Flags().IntVar(&o.SSEPort, flagSSEPort, o.SSEPort, "Start a SSE server on the specified port")
137+
cmd.Flag(flagSSEPort).Deprecated = "Use --port instead"
138+
cmd.Flags().IntVar(&o.HttpPort, flagHttpPort, o.HttpPort, "Start a streamable HTTP server on the specified port")
139+
cmd.Flag(flagHttpPort).Deprecated = "Use --port instead"
140+
cmd.Flags().StringVar(&o.Port, flagPort, o.Port, "Start a streamable HTTP and SSE HTTP server on the specified port (e.g. 8080)")
141+
cmd.Flags().StringVar(&o.SSEBaseUrl, flagSSEBaseUrl, o.SSEBaseUrl, "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
142+
cmd.Flags().StringVar(&o.Kubeconfig, flagKubeconfig, o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
143+
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, ", ")+".")
144+
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+".")
145+
cmd.Flags().BoolVar(&o.ReadOnly, flagReadOnly, o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
146+
cmd.Flags().BoolVar(&o.DisableDestructive, flagDisableDestructive, o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled")
147+
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")
148+
_ = cmd.Flags().MarkHidden(flagRequireOAuth)
149+
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.")
150+
_ = cmd.Flags().MarkHidden(flagOAuthAudience)
151+
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.")
152+
_ = cmd.Flags().MarkHidden(flagValidateToken)
153+
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.")
154+
_ = cmd.Flags().MarkHidden(flagAuthorizationURL)
155+
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.")
156+
_ = cmd.Flags().MarkHidden(flagServerUrl)
157+
cmd.Flags().StringVar(&o.CertificateAuthority, flagCertificateAuthority, o.CertificateAuthority, "Certificate authority path to verify certificates. Optional. Only valid if require-oauth is enabled.")
158+
_ = cmd.Flags().MarkHidden(flagCertificateAuthority)
159+
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.")
133160

134161
return cmd
135162
}
@@ -156,52 +183,55 @@ func (m *MCPServerOptions) Complete(cmd *cobra.Command) error {
156183
}
157184

158185
func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) {
159-
if cmd.Flag("log-level").Changed {
186+
if cmd.Flag(flagLogLevel).Changed {
160187
m.StaticConfig.LogLevel = m.LogLevel
161188
}
162-
if cmd.Flag("port").Changed {
189+
if cmd.Flag(flagPort).Changed {
163190
m.StaticConfig.Port = m.Port
164-
} else if cmd.Flag("sse-port").Changed {
191+
} else if cmd.Flag(flagSSEPort).Changed {
165192
m.StaticConfig.Port = strconv.Itoa(m.SSEPort)
166-
} else if cmd.Flag("http-port").Changed {
193+
} else if cmd.Flag(flagHttpPort).Changed {
167194
m.StaticConfig.Port = strconv.Itoa(m.HttpPort)
168195
}
169-
if cmd.Flag("sse-base-url").Changed {
196+
if cmd.Flag(flagSSEBaseUrl).Changed {
170197
m.StaticConfig.SSEBaseURL = m.SSEBaseUrl
171198
}
172-
if cmd.Flag("kubeconfig").Changed {
199+
if cmd.Flag(flagKubeconfig).Changed {
173200
m.StaticConfig.KubeConfig = m.Kubeconfig
174201
}
175-
if cmd.Flag("list-output").Changed {
202+
if cmd.Flag(flagListOutput).Changed {
176203
m.StaticConfig.ListOutput = m.ListOutput
177204
}
178-
if cmd.Flag("read-only").Changed {
205+
if cmd.Flag(flagReadOnly).Changed {
179206
m.StaticConfig.ReadOnly = m.ReadOnly
180207
}
181-
if cmd.Flag("disable-destructive").Changed {
208+
if cmd.Flag(flagDisableDestructive).Changed {
182209
m.StaticConfig.DisableDestructive = m.DisableDestructive
183210
}
184-
if cmd.Flag("toolsets").Changed {
211+
if cmd.Flag(flagToolsets).Changed {
185212
m.StaticConfig.Toolsets = m.Toolsets
186213
}
187-
if cmd.Flag("require-oauth").Changed {
214+
if cmd.Flag(flagRequireOAuth).Changed {
188215
m.StaticConfig.RequireOAuth = m.RequireOAuth
189216
}
190-
if cmd.Flag("oauth-audience").Changed {
217+
if cmd.Flag(flagOAuthAudience).Changed {
191218
m.StaticConfig.OAuthAudience = m.OAuthAudience
192219
}
193-
if cmd.Flag("validate-token").Changed {
220+
if cmd.Flag(flagValidateToken).Changed {
194221
m.StaticConfig.ValidateToken = m.ValidateToken
195222
}
196-
if cmd.Flag("authorization-url").Changed {
223+
if cmd.Flag(flagAuthorizationURL).Changed {
197224
m.StaticConfig.AuthorizationURL = m.AuthorizationURL
198225
}
199-
if cmd.Flag("server-url").Changed {
226+
if cmd.Flag(flagServerUrl).Changed {
200227
m.StaticConfig.ServerURL = m.ServerURL
201228
}
202-
if cmd.Flag("certificate-authority").Changed {
229+
if cmd.Flag(flagCertificateAuthority).Changed {
203230
m.StaticConfig.CertificateAuthority = m.CertificateAuthority
204231
}
232+
if cmd.Flag(flagDisableMultiCluster).Changed && m.DisableMultiCluster {
233+
m.StaticConfig.ClusterProviderStrategy = config.ClusterProviderDisabled
234+
}
205235
}
206236

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

291+
strategy := m.StaticConfig.ClusterProviderStrategy
292+
if strategy == "" {
293+
strategy = "auto-detect (it is recommended to set this explicitly in your Config)"
294+
}
295+
296+
klog.V(1).Infof(" - ClusterProviderStrategy: %s", strategy)
297+
261298
if m.Version {
262299
_, _ = fmt.Fprintf(m.Out, "%s\n", version.Version)
263300
return nil

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,24 @@ func TestStdioLogging(t *testing.T) {
276276
assert.Containsf(t, out.String(), "Starting kubernetes-mcp-server", "Expected klog output, got %s", out.String())
277277
})
278278
}
279+
280+
func TestDisableMultiCluster(t *testing.T) {
281+
t.Run("defaults to false", func(t *testing.T) {
282+
ioStreams, out := testStream()
283+
rootCmd := NewMCPServer(ioStreams)
284+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1"})
285+
if err := rootCmd.Execute(); !strings.Contains(out.String(), " - ClusterProviderStrategy: auto-detect (it is recommended to set this explicitly in your Config)") {
286+
t.Fatalf("Expected ClusterProviderStrategy kubeconfig, got %s %v", out, err)
287+
}
288+
})
289+
t.Run("set with --disable-multi-cluster", func(t *testing.T) {
290+
ioStreams, out := testStream()
291+
rootCmd := NewMCPServer(ioStreams)
292+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--disable-multi-cluster"})
293+
_ = rootCmd.Execute()
294+
expected := `(?m)\" - ClusterProviderStrategy\: disabled\"`
295+
if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
296+
t.Fatalf("Expected ClusterProviderStrategy %s, got %s %v", expected, out.String(), err)
297+
}
298+
})
299+
}

pkg/kubernetes/provider.go

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,30 @@ type kubeConfigClusterProvider struct {
3232

3333
var _ ManagerProvider = &kubeConfigClusterProvider{}
3434

35-
type inClusterProvider struct {
36-
manager *Manager
35+
type singleClusterProvider struct {
36+
strategy string
37+
manager *Manager
3738
}
3839

39-
var _ ManagerProvider = &inClusterProvider{}
40+
var _ ManagerProvider = &singleClusterProvider{}
4041

4142
func NewManagerProvider(cfg *config.StaticConfig) (ManagerProvider, error) {
4243
m, err := NewManager(cfg)
4344
if err != nil {
4445
return nil, err
4546
}
4647

47-
switch resolveStrategy(cfg, m) {
48+
strategy := resolveStrategy(cfg, m)
49+
switch strategy {
4850
case config.ClusterProviderKubeConfig:
4951
return newKubeConfigClusterProvider(m)
50-
case config.ClusterProviderInCluster:
51-
return newInClusterProvider(m)
52+
case config.ClusterProviderInCluster, config.ClusterProviderDisabled:
53+
return newSingleClusterProvider(m, strategy)
5254
default:
53-
return nil, fmt.Errorf("invalid ClusterProviderStrategy '%s', must be 'kubeconfig' or 'in-cluster'", cfg.ClusterProviderStrategy)
55+
return nil, fmt.Errorf(
56+
"invalid ClusterProviderStrategy '%s', must be 'kubeconfig', 'in-cluster', or 'disabled'",
57+
strategy,
58+
)
5459
}
5560
}
5661

@@ -83,9 +88,14 @@ func newKubeConfigClusterProvider(m *Manager) (*kubeConfigClusterProvider, error
8388
}, nil
8489
}
8590

86-
func newInClusterProvider(m *Manager) (*inClusterProvider, error) {
87-
return &inClusterProvider{
88-
manager: m,
91+
func newSingleClusterProvider(m *Manager, strategy string) (*singleClusterProvider, error) {
92+
if strategy == config.ClusterProviderInCluster && !m.IsInCluster() {
93+
return nil, fmt.Errorf("server must be deployed in cluster for the in-cluster ClusterProviderStrategy")
94+
}
95+
96+
return &singleClusterProvider{
97+
manager: m,
98+
strategy: strategy,
8999
}, nil
90100
}
91101

@@ -141,32 +151,32 @@ func (k *kubeConfigClusterProvider) Close() {
141151
m.Close()
142152
}
143153

144-
func (i *inClusterProvider) GetTargets(ctx context.Context) ([]string, error) {
154+
func (s *singleClusterProvider) GetTargets(ctx context.Context) ([]string, error) {
145155
return []string{""}, nil
146156
}
147157

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

153-
return i.manager, nil
163+
return s.manager, nil
154164
}
155165

156-
func (i *inClusterProvider) GetDefaultTarget() string {
166+
func (s *singleClusterProvider) GetDefaultTarget() string {
157167
return ""
158168
}
159169

160-
func (i *inClusterProvider) GetTargetParameterName() string {
170+
func (s *singleClusterProvider) GetTargetParameterName() string {
161171
return ""
162172
}
163173

164-
func (i *inClusterProvider) WatchTargets(watch func() error) {
165-
i.manager.WatchKubeConfig(watch)
174+
func (s *singleClusterProvider) WatchTargets(watch func() error) {
175+
s.manager.WatchKubeConfig(watch)
166176
}
167177

168-
func (i *inClusterProvider) Close() {
169-
i.manager.Close()
178+
func (s *singleClusterProvider) Close() {
179+
s.manager.Close()
170180
}
171181

172182
func (m *Manager) newForContext(context string) (*Manager, error) {

0 commit comments

Comments
 (0)