mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
feat(mcp): serve sse and streamable from a single port
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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") |
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
log_level = 1
|
||||
sse_port = 9999
|
||||
port = "9999"
|
||||
kubeconfig = "test"
|
||||
list_output = "yaml"
|
||||
read_only = true
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user