diff --git a/Dockerfile b/Dockerfile index dc34ec20..795baf6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,6 @@ RUN make build FROM registry.access.redhat.com/ubi9/ubi-minimal:latest WORKDIR /app COPY --from=builder /app/kubernetes-mcp-server /app/kubernetes-mcp-server -ENTRYPOINT ["/app/kubernetes-mcp-server", "--sse-port", "8080"] +ENTRYPOINT ["/app/kubernetes-mcp-server", "--port", "8080"] EXPOSE 8080 diff --git a/README.md b/README.md index ec8842a9..d4324e4c 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,8 @@ uvx kubernetes-mcp-server@latest --help | Option | Description | |-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--http-port` | Starts the MCP server in Streamable HTTP mode and listens on the specified port (path /mcp). | -| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port (path /sse). | +| `--http-port` | Starts the MCP server in Streamable HTTP mode and listens on the specified port (path /mcp). Deprecated: Please use --port. | +| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port (path /sse). Deprecated: Please use --port. | | `--log-level` | Sets the logging level (values [from 0-9](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md)). Similar to [kubectl logging levels](https://kubernetes.io/docs/reference/kubectl/quick-reference/#kubectl-output-verbosity-and-debugging). | | `--kubeconfig` | Path to the Kubernetes configuration file. If not provided, it will try to resolve the configuration (in-cluster, default location, etc.). | | `--list-output` | Output format for resource list operations (one of: yaml, table) (default "table") | diff --git a/pkg/config/config.go b/pkg/config/config.go index 723d17c5..334c2a2c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -12,8 +12,7 @@ type StaticConfig struct { DeniedResources []GroupVersionKind `toml:"denied_resources"` LogLevel int `toml:"log_level,omitempty"` - SSEPort int `toml:"sse_port,omitempty"` - HTTPPort int `toml:"http_port,omitempty"` + Port string `toml:"port,omitempty"` SSEBaseURL string `toml:"sse_base_url,omitempty"` KubeConfig string `toml:"kubeconfig,omitempty"` ListOutput string `toml:"list_output,omitempty"` diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 0a7be569..cbae910f 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -51,7 +51,7 @@ kind = "Role func TestReadConfigValid(t *testing.T) { validConfigPath := writeConfig(t, ` log_level = 1 -sse_port = 9999 +port = "9999" kubeconfig = "test" list_output = "yaml" read_only = true @@ -87,15 +87,12 @@ disabled_tools = ["pods_delete", "pods_top", "pods_log", "pods_run", "pods_exec" 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.Port != "9999" { + t.Fatalf("Unexpected port value: %v", config.Port) } 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) } diff --git a/pkg/kubernetes-mcp-server/cmd/root.go b/pkg/kubernetes-mcp-server/cmd/root.go index cd4837fa..ae2ecd57 100644 --- a/pkg/kubernetes-mcp-server/cmd/root.go +++ b/pkg/kubernetes-mcp-server/cmd/root.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "net/http" "strconv" "strings" @@ -35,16 +36,17 @@ kubernetes-mcp-server --version kubernetes-mcp-server # start a SSE server on port 8080 -kubernetes-mcp-server --sse-port 8080 +kubernetes-mcp-server --port 8080 # start a SSE server on port 8443 with a public HTTPS host of example.com -kubernetes-mcp-server --sse-port 8443 --sse-base-url https://example.com:8443 +kubernetes-mcp-server --port 8443 --sse-base-url https://example.com:8443 `)) ) type MCPServerOptions struct { Version bool LogLevel int + Port string SSEPort int HttpPort int SSEBaseUrl string @@ -95,7 +97,10 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command { 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. Each profile has its set of defaults.") 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().StringVar(&o.Profile, "profile", o.Profile, "MCP profile to use (one of: "+strings.Join(mcp.ProfileNames, ", ")+")") @@ -126,11 +131,12 @@ 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("port").Changed { + m.StaticConfig.Port = m.Port + } else if cmd.Flag("sse-port").Changed { + m.StaticConfig.Port = strconv.Itoa(m.SSEPort) + } else if cmd.Flag("http-port").Changed { + m.StaticConfig.Port = strconv.Itoa(m.HttpPort) } if cmd.Flag("sse-base-url").Changed { m.StaticConfig.SSEBaseURL = m.SSEBaseUrl @@ -162,6 +168,9 @@ func (m *MCPServerOptions) initializeLogging() { } func (m *MCPServerOptions) Validate() error { + if m.Port != "" && (m.SSEPort > 0 || m.HttpPort > 0) { + return fmt.Errorf("--port is mutually exclusive with deprecated --http-port and --sse-port flags") + } return nil } @@ -195,23 +204,27 @@ func (m *MCPServerOptions) Run() error { } defer mcpServer.Close() - ctx := context.Background() - - if m.StaticConfig.SSEPort > 0 { - sseServer := mcpServer.ServeSse(m.StaticConfig.SSEBaseURL) - defer func() { _ = sseServer.Shutdown(ctx) }() - klog.V(0).Infof("SSE server starting on port %d and path /sse", m.StaticConfig.SSEPort) - if err := sseServer.Start(fmt.Sprintf(":%d", m.StaticConfig.SSEPort)); err != nil { - return fmt.Errorf("failed to start SSE server: %w\n", err) + if m.StaticConfig.Port != "" { + mux := http.NewServeMux() + httpServer := &http.Server{ + Addr: ":" + m.StaticConfig.Port, + Handler: mux, } - } - if m.StaticConfig.HTTPPort > 0 { - httpServer := mcpServer.ServeHTTP() - 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.StaticConfig.HTTPPort)); err != nil { - return fmt.Errorf("failed to start streaming HTTP server: %w\n", err) + sseServer := mcpServer.ServeSse(m.SSEBaseUrl, httpServer) + streamableHttpServer := mcpServer.ServeHTTP(httpServer) + mux.Handle("/sse", sseServer) + mux.Handle("/message", sseServer) + mux.Handle("/mcp", streamableHttpServer) + mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + klog.V(0).Infof("Streaming and SSE HTTP servers starting on port %s and paths /mcp, /sse, /message", m.StaticConfig.Port) + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return err } + return nil } if err := mcpServer.ServeStdio(); err != nil && !errors.Is(err, context.Canceled) { diff --git a/pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml b/pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml index 8b46a1aa..1b8afc61 100644 --- a/pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml +++ b/pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml @@ -1,5 +1,5 @@ log_level = 1 -sse_port = 9999 +port = "9999" kubeconfig = "test" list_output = "yaml" read_only = true diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 1b4961cd..96a9898f 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -85,18 +85,19 @@ func (s *Server) ServeStdio() error { return server.ServeStdio(s.server) } -func (s *Server) ServeSse(baseUrl string) *server.SSEServer { +func (s *Server) ServeSse(baseUrl string, httpServer *http.Server) *server.SSEServer { options := make([]server.SSEOption, 0) - options = append(options, server.WithSSEContextFunc(contextFunc)) + options = append(options, server.WithSSEContextFunc(contextFunc), server.WithHTTPServer(httpServer)) if baseUrl != "" { options = append(options, server.WithBaseURL(baseUrl)) } return server.NewSSEServer(s.server, options...) } -func (s *Server) ServeHTTP() *server.StreamableHTTPServer { +func (s *Server) ServeHTTP(httpServer *http.Server) *server.StreamableHTTPServer { options := []server.StreamableHTTPOption{ server.WithHTTPContextFunc(contextFunc), + server.WithStreamableHTTPServer(httpServer), } return server.NewStreamableHTTPServer(s.server, options...) }