From 986169f3302843f3e46ff67956b5cc74d2bfe8aa Mon Sep 17 00:00:00 2001 From: "zhoupan.jeton" Date: Mon, 8 Sep 2025 14:17:10 +0800 Subject: [PATCH 1/3] add get mcp server function --- pkg/mcp/mcp.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 54a9da89..9c4c0401 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -143,6 +143,10 @@ func (s *Server) GetEnabledTools() []string { return s.enabledTools } +func (s *Server) GetMCPServer() *server.MCPServer { + return s.server +} + func (s *Server) Close() { if s.k != nil { s.k.Close() From 03fb1c900fac00064b941d790d4885c631857cfd Mon Sep 17 00:00:00 2001 From: "zhoupan.jeton" Date: Mon, 8 Sep 2025 19:12:31 +0800 Subject: [PATCH 2/3] add endpoint parameter health_endpoint,sse_endpoint,sse_message_endpoint,streamable_http_endpoint --- pkg/config/config.go | 8 ++++++++ pkg/config/config_test.go | 25 +++++++++++++++++++++++++ pkg/http/authorization.go | 1 + pkg/http/http.go | 25 +++++++++++++++++++------ pkg/kubernetes-mcp-server/cmd/root.go | 21 +++++++++++++++++++++ pkg/mcp/mcp.go | 12 +++++++----- 6 files changed, 81 insertions(+), 11 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 26e007d1..91823a67 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -48,6 +48,14 @@ type StaticConfig struct { StsScopes []string `toml:"sts_scopes,omitempty"` CertificateAuthority string `toml:"certificate_authority,omitempty"` ServerURL string `toml:"server_url,omitempty"` + // HealthEndpoint is the health check endpoint + HealthEndpoint string `toml:"health_endpoint,omitempty"` + // SSEEndpoint is the SSE endpoint + SSEEndpoint string `toml:"sse_endpoint,omitempty"` + // SSEMessageEndpoint is the SSE message endpoint + SSEMessageEndpoint string `toml:"sse_message_endpoint,omitempty"` + // StreamableHttpEndpoint is the streamable http endpoint + StreamableHttpEndpoint string `toml:"streamable_http_endpoint,omitempty"` } type GroupVersionKind struct { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1f52361d..9e077865 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -65,6 +65,11 @@ denied_resources = [ enabled_tools = ["configuration_view", "events_list", "namespaces_list", "pods_list", "resources_list", "resources_get", "resources_create_or_update", "resources_delete"] disabled_tools = ["pods_delete", "pods_top", "pods_log", "pods_run", "pods_exec"] + +health_endpoint = "/k8s/healthz" +streamable_http_endpoint = "/k8s/mcp" +sse_endpoint = "/k8s/sse" +sse_message_endpoint = "/k8s/message" `) config, err := ReadConfig(validConfigPath) @@ -142,6 +147,26 @@ disabled_tools = ["pods_delete", "pods_top", "pods_log", "pods_run", "pods_exec" } } }) + t.Run("health_endpoint parsed correctly", func(t *testing.T) { + if config.HealthEndpoint != "/k8s/healthz" { + t.Fatalf("Unexpected health_endpoint value: %v", config.HealthEndpoint) + } + }) + t.Run("streamable_http_endpoint parsed correctly", func(t *testing.T) { + if config.StreamableHttpEndpoint != "/k8s/mcp" { + t.Fatalf("Unexpected streamable_http_endpoint value: %v", config.StreamableHttpEndpoint) + } + }) + t.Run("sse_endpoint parsed correctly", func(t *testing.T) { + if config.SSEEndpoint != "/k8s/sse" { + t.Fatalf("Unexpected sse_endpoint value: %v", config.SSEEndpoint) + } + }) + t.Run("sse_message_endpoint parsed correctly", func(t *testing.T) { + if config.SSEMessageEndpoint != "/k8s/message" { + t.Fatalf("Unexpected sse_message_endpoint value: %v", config.SSEMessageEndpoint) + } + }) } func writeConfig(t *testing.T, content string) string { diff --git a/pkg/http/authorization.go b/pkg/http/authorization.go index 39259d51..162e6846 100644 --- a/pkg/http/authorization.go +++ b/pkg/http/authorization.go @@ -65,6 +65,7 @@ type KubernetesApiTokenVerifier interface { func AuthorizationMiddleware(staticConfig *config.StaticConfig, oidcProvider *oidc.Provider, verifier KubernetesApiTokenVerifier) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + healthEndpoint := getEndpointOrDefault(staticConfig.HealthEndpoint, defaultHealthEndpoint) if r.URL.Path == healthEndpoint || slices.Contains(WellKnownEndpoints, r.URL.EscapedPath()) { next.ServeHTTP(w, r) return diff --git a/pkg/http/http.go b/pkg/http/http.go index 3f74c09f..08b36d0b 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -18,12 +18,20 @@ import ( ) const ( - healthEndpoint = "/healthz" - mcpEndpoint = "/mcp" - sseEndpoint = "/sse" - sseMessageEndpoint = "/message" + defaultHealthEndpoint = "/healthz" + defaultMcpEndpoint = "/mcp" + defaultSseEndpoint = "/sse" + defaultSseMessageEndpoint = "/message" ) +// getEndpointOrDefault returns the endpoint value, otherwise returns the default value. +func getEndpointOrDefault(configValue, defaultValue string) string { + if configValue != "" { + return configValue + } + return defaultValue +} + func Serve(ctx context.Context, mcpServer *mcp.Server, staticConfig *config.StaticConfig, oidcProvider *oidc.Provider) error { mux := http.NewServeMux() @@ -36,7 +44,12 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, staticConfig *config.Stat Handler: wrappedMux, } - sseServer := mcpServer.ServeSse(staticConfig.SSEBaseURL, httpServer) + healthEndpoint := getEndpointOrDefault(staticConfig.HealthEndpoint, defaultHealthEndpoint) + mcpEndpoint := getEndpointOrDefault(staticConfig.StreamableHttpEndpoint, defaultMcpEndpoint) + sseEndpoint := getEndpointOrDefault(staticConfig.SSEEndpoint, defaultSseEndpoint) + sseMessageEndpoint := getEndpointOrDefault(staticConfig.SSEMessageEndpoint, defaultSseMessageEndpoint) + + sseServer := mcpServer.ServeSse(staticConfig.SSEBaseURL, sseEndpoint, sseMessageEndpoint, httpServer) streamableHttpServer := mcpServer.ServeHTTP(httpServer) mux.Handle(sseEndpoint, sseServer) mux.Handle(sseMessageEndpoint, sseServer) @@ -54,7 +67,7 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, staticConfig *config.Stat serverErr := make(chan error, 1) go func() { - klog.V(0).Infof("Streaming and SSE HTTP servers starting on port %s and paths /mcp, /sse, /message", staticConfig.Port) + klog.V(0).Infof("Streaming and SSE HTTP servers starting on port %s and paths %s, %s, %s", staticConfig.Port, mcpEndpoint, sseEndpoint, sseMessageEndpoint) if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { serverErr <- err } diff --git a/pkg/kubernetes-mcp-server/cmd/root.go b/pkg/kubernetes-mcp-server/cmd/root.go index b335938b..73ae3c33 100644 --- a/pkg/kubernetes-mcp-server/cmd/root.go +++ b/pkg/kubernetes-mcp-server/cmd/root.go @@ -68,6 +68,11 @@ type MCPServerOptions struct { CertificateAuthority string ServerURL string + HealthEndpoint string + StreamableHttpEndpoint string + SseEndpoint string + SseMessageEndpoint string + ConfigPath string StaticConfig *config.StaticConfig @@ -120,6 +125,10 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command { 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().StringVar(&o.HealthEndpoint, "health-endpoint", o.HealthEndpoint, "Use for set health endpoint to use for health checks, Default is /healthz") + cmd.Flags().StringVar(&o.StreamableHttpEndpoint, "streamable-http-endpoint", o.StreamableHttpEndpoint, "Use for set streamable http requests endpoint, Default is /mcp") + cmd.Flags().StringVar(&o.SseEndpoint, "sse-endpoint", o.SseEndpoint, "Use for set sse requests endpoint, Default is /sse") + cmd.Flags().StringVar(&o.SseMessageEndpoint, "sse-message-endpoint", o.SseMessageEndpoint, "Use for set sse message requests endpoint, Default is /message") _ = 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") @@ -200,6 +209,18 @@ func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) { if cmd.Flag("certificate-authority").Changed { m.StaticConfig.CertificateAuthority = m.CertificateAuthority } + if cmd.Flag("health-endpoint").Changed { + m.StaticConfig.HealthEndpoint = m.HealthEndpoint + } + if cmd.Flag("streamable-http-endpoint").Changed { + m.StaticConfig.StreamableHttpEndpoint = m.StreamableHttpEndpoint + } + if cmd.Flag("sse-endpoint").Changed { + m.StaticConfig.SSEEndpoint = m.SseEndpoint + } + if cmd.Flag("sse-message-endpoint").Changed { + m.StaticConfig.SSEMessageEndpoint = m.SseMessageEndpoint + } } func (m *MCPServerOptions) initializeLogging() { diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 9c4c0401..cab4fbac 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -104,12 +104,18 @@ func (s *Server) ServeStdio() error { return server.ServeStdio(s.server) } -func (s *Server) ServeSse(baseUrl string, httpServer *http.Server) *server.SSEServer { +func (s *Server) ServeSse(baseUrl, sseEndpoint, sseMessageEndpoint string, httpServer *http.Server) *server.SSEServer { options := make([]server.SSEOption, 0) options = append(options, server.WithSSEContextFunc(contextFunc), server.WithHTTPServer(httpServer)) if baseUrl != "" { options = append(options, server.WithBaseURL(baseUrl)) } + if sseEndpoint != "" { + options = append(options, server.WithSSEEndpoint(sseEndpoint)) + } + if sseMessageEndpoint != "" { + options = append(options, server.WithMessageEndpoint(sseMessageEndpoint)) + } return server.NewSSEServer(s.server, options...) } @@ -143,10 +149,6 @@ func (s *Server) GetEnabledTools() []string { return s.enabledTools } -func (s *Server) GetMCPServer() *server.MCPServer { - return s.server -} - func (s *Server) Close() { if s.k != nil { s.k.Close() From b52c51a59e7f7928536d02791d357e7ce06f816c Mon Sep 17 00:00:00 2001 From: "zhoupan.jeton" Date: Mon, 8 Sep 2025 19:46:38 +0800 Subject: [PATCH 3/3] update SSEEndpoint & SSEMessageEndpoint --- pkg/kubernetes-mcp-server/cmd/root.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/kubernetes-mcp-server/cmd/root.go b/pkg/kubernetes-mcp-server/cmd/root.go index 73ae3c33..0692a919 100644 --- a/pkg/kubernetes-mcp-server/cmd/root.go +++ b/pkg/kubernetes-mcp-server/cmd/root.go @@ -70,8 +70,8 @@ type MCPServerOptions struct { HealthEndpoint string StreamableHttpEndpoint string - SseEndpoint string - SseMessageEndpoint string + SSEEndpoint string + SSEMessageEndpoint string ConfigPath string StaticConfig *config.StaticConfig @@ -125,10 +125,10 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command { 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().StringVar(&o.HealthEndpoint, "health-endpoint", o.HealthEndpoint, "Use for set health endpoint to use for health checks, Default is /healthz") - cmd.Flags().StringVar(&o.StreamableHttpEndpoint, "streamable-http-endpoint", o.StreamableHttpEndpoint, "Use for set streamable http requests endpoint, Default is /mcp") - cmd.Flags().StringVar(&o.SseEndpoint, "sse-endpoint", o.SseEndpoint, "Use for set sse requests endpoint, Default is /sse") - cmd.Flags().StringVar(&o.SseMessageEndpoint, "sse-message-endpoint", o.SseMessageEndpoint, "Use for set sse message requests endpoint, Default is /message") + cmd.Flags().StringVar(&o.HealthEndpoint, "health-endpoint", "/healthz", "Use for set health endpoint to use for health checks") + cmd.Flags().StringVar(&o.StreamableHttpEndpoint, "streamable-http-endpoint", "/mcp", "Use for set streamable http requests endpoint") + cmd.Flags().StringVar(&o.SSEEndpoint, "sse-endpoint", "/sse", "Use for set sse requests endpoint") + cmd.Flags().StringVar(&o.SSEMessageEndpoint, "sse-message-endpoint", "/message", "Use for set sse message requests endpoint") _ = 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") @@ -216,10 +216,10 @@ func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) { m.StaticConfig.StreamableHttpEndpoint = m.StreamableHttpEndpoint } if cmd.Flag("sse-endpoint").Changed { - m.StaticConfig.SSEEndpoint = m.SseEndpoint + m.StaticConfig.SSEEndpoint = m.SSEEndpoint } if cmd.Flag("sse-message-endpoint").Changed { - m.StaticConfig.SSEMessageEndpoint = m.SseMessageEndpoint + m.StaticConfig.SSEMessageEndpoint = m.SSEMessageEndpoint } }