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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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") |
Expand Down
3 changes: 1 addition & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
9 changes: 3 additions & 6 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
55 changes: 34 additions & 21 deletions pkg/kubernetes-mcp-server/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"flag"
"fmt"
"net/http"
"strconv"
"strings"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, ", ")+")")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
log_level = 1
sse_port = 9999
port = "9999"
kubeconfig = "test"
list_output = "yaml"
read_only = true
Expand Down
7 changes: 4 additions & 3 deletions pkg/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
Expand Down
Loading