Skip to content

Commit e6b1903

Browse files
authored
feat(mcp): serve sse and streamable from a single port
1 parent 186f445 commit e6b1903

File tree

7 files changed

+46
-36
lines changed

7 files changed

+46
-36
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ RUN make build
88
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
99
WORKDIR /app
1010
COPY --from=builder /app/kubernetes-mcp-server /app/kubernetes-mcp-server
11-
ENTRYPOINT ["/app/kubernetes-mcp-server", "--sse-port", "8080"]
11+
ENTRYPOINT ["/app/kubernetes-mcp-server", "--port", "8080"]
1212

1313
EXPOSE 8080

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ uvx kubernetes-mcp-server@latest --help
158158

159159
| Option | Description |
160160
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
161-
| `--http-port` | Starts the MCP server in Streamable HTTP mode and listens on the specified port (path /mcp). |
162-
| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port (path /sse). |
161+
| `--http-port` | Starts the MCP server in Streamable HTTP mode and listens on the specified port (path /mcp). Deprecated: Please use --port. |
162+
| `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port (path /sse). Deprecated: Please use --port. |
163163
| `--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). |
164164
| `--kubeconfig` | Path to the Kubernetes configuration file. If not provided, it will try to resolve the configuration (in-cluster, default location, etc.). |
165165
| `--list-output` | Output format for resource list operations (one of: yaml, table) (default "table") |

pkg/config/config.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ type StaticConfig struct {
1212
DeniedResources []GroupVersionKind `toml:"denied_resources"`
1313

1414
LogLevel int `toml:"log_level,omitempty"`
15-
SSEPort int `toml:"sse_port,omitempty"`
16-
HTTPPort int `toml:"http_port,omitempty"`
15+
Port string `toml:"port,omitempty"`
1716
SSEBaseURL string `toml:"sse_base_url,omitempty"`
1817
KubeConfig string `toml:"kubeconfig,omitempty"`
1918
ListOutput string `toml:"list_output,omitempty"`

pkg/config/config_test.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ kind = "Role
5151
func TestReadConfigValid(t *testing.T) {
5252
validConfigPath := writeConfig(t, `
5353
log_level = 1
54-
sse_port = 9999
54+
port = "9999"
5555
kubeconfig = "test"
5656
list_output = "yaml"
5757
read_only = true
@@ -87,15 +87,12 @@ disabled_tools = ["pods_delete", "pods_top", "pods_log", "pods_run", "pods_exec"
8787
if config.LogLevel != 1 {
8888
t.Fatalf("Unexpected log level: %v", config.LogLevel)
8989
}
90-
if config.SSEPort != 9999 {
91-
t.Fatalf("Unexpected sse_port value: %v", config.SSEPort)
90+
if config.Port != "9999" {
91+
t.Fatalf("Unexpected port value: %v", config.Port)
9292
}
9393
if config.SSEBaseURL != "" {
9494
t.Fatalf("Unexpected sse_base_url value: %v", config.SSEBaseURL)
9595
}
96-
if config.HTTPPort != 0 {
97-
t.Fatalf("Unexpected http_port value: %v", config.HTTPPort)
98-
}
9996
if config.KubeConfig != "test" {
10097
t.Fatalf("Unexpected kubeconfig value: %v", config.KubeConfig)
10198
}

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

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"flag"
77
"fmt"
8+
"net/http"
89
"strconv"
910
"strings"
1011

@@ -35,16 +36,17 @@ kubernetes-mcp-server --version
3536
kubernetes-mcp-server
3637
3738
# start a SSE server on port 8080
38-
kubernetes-mcp-server --sse-port 8080
39+
kubernetes-mcp-server --port 8080
3940
4041
# start a SSE server on port 8443 with a public HTTPS host of example.com
41-
kubernetes-mcp-server --sse-port 8443 --sse-base-url https://example.com:8443
42+
kubernetes-mcp-server --port 8443 --sse-base-url https://example.com:8443
4243
`))
4344
)
4445

4546
type MCPServerOptions struct {
4647
Version bool
4748
LogLevel int
49+
Port string
4850
SSEPort int
4951
HttpPort int
5052
SSEBaseUrl string
@@ -95,7 +97,10 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
9597
cmd.Flags().IntVar(&o.LogLevel, "log-level", o.LogLevel, "Set the log level (from 0 to 9)")
9698
cmd.Flags().StringVar(&o.ConfigPath, "config", o.ConfigPath, "Path of the config file. Each profile has its set of defaults.")
9799
cmd.Flags().IntVar(&o.SSEPort, "sse-port", o.SSEPort, "Start a SSE server on the specified port")
100+
cmd.Flag("sse-port").Deprecated = "Use --port instead"
98101
cmd.Flags().IntVar(&o.HttpPort, "http-port", o.HttpPort, "Start a streamable HTTP server on the specified port")
102+
cmd.Flag("http-port").Deprecated = "Use --port instead"
103+
cmd.Flags().StringVar(&o.Port, "port", o.Port, "Start a streamable HTTP and SSE HTTP server on the specified port (e.g. 8080)")
99104
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)")
100105
cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
101106
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) {
126131
if cmd.Flag("log-level").Changed {
127132
m.StaticConfig.LogLevel = m.LogLevel
128133
}
129-
if cmd.Flag("sse-port").Changed {
130-
m.StaticConfig.SSEPort = m.SSEPort
131-
}
132-
if cmd.Flag("http-port").Changed {
133-
m.StaticConfig.HTTPPort = m.HttpPort
134+
if cmd.Flag("port").Changed {
135+
m.StaticConfig.Port = m.Port
136+
} else if cmd.Flag("sse-port").Changed {
137+
m.StaticConfig.Port = strconv.Itoa(m.SSEPort)
138+
} else if cmd.Flag("http-port").Changed {
139+
m.StaticConfig.Port = strconv.Itoa(m.HttpPort)
134140
}
135141
if cmd.Flag("sse-base-url").Changed {
136142
m.StaticConfig.SSEBaseURL = m.SSEBaseUrl
@@ -162,6 +168,9 @@ func (m *MCPServerOptions) initializeLogging() {
162168
}
163169

164170
func (m *MCPServerOptions) Validate() error {
171+
if m.Port != "" && (m.SSEPort > 0 || m.HttpPort > 0) {
172+
return fmt.Errorf("--port is mutually exclusive with deprecated --http-port and --sse-port flags")
173+
}
165174
return nil
166175
}
167176

@@ -195,23 +204,27 @@ func (m *MCPServerOptions) Run() error {
195204
}
196205
defer mcpServer.Close()
197206

198-
ctx := context.Background()
199-
200-
if m.StaticConfig.SSEPort > 0 {
201-
sseServer := mcpServer.ServeSse(m.StaticConfig.SSEBaseURL)
202-
defer func() { _ = sseServer.Shutdown(ctx) }()
203-
klog.V(0).Infof("SSE server starting on port %d and path /sse", m.StaticConfig.SSEPort)
204-
if err := sseServer.Start(fmt.Sprintf(":%d", m.StaticConfig.SSEPort)); err != nil {
205-
return fmt.Errorf("failed to start SSE server: %w\n", err)
207+
if m.StaticConfig.Port != "" {
208+
mux := http.NewServeMux()
209+
httpServer := &http.Server{
210+
Addr: ":" + m.StaticConfig.Port,
211+
Handler: mux,
206212
}
207-
}
208213

209-
if m.StaticConfig.HTTPPort > 0 {
210-
httpServer := mcpServer.ServeHTTP()
211-
klog.V(0).Infof("Streaming HTTP server starting on port %d and path /mcp", m.StaticConfig.HTTPPort)
212-
if err := httpServer.Start(fmt.Sprintf(":%d", m.StaticConfig.HTTPPort)); err != nil {
213-
return fmt.Errorf("failed to start streaming HTTP server: %w\n", err)
214+
sseServer := mcpServer.ServeSse(m.SSEBaseUrl, httpServer)
215+
streamableHttpServer := mcpServer.ServeHTTP(httpServer)
216+
mux.Handle("/sse", sseServer)
217+
mux.Handle("/message", sseServer)
218+
mux.Handle("/mcp", streamableHttpServer)
219+
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
220+
w.WriteHeader(http.StatusOK)
221+
})
222+
223+
klog.V(0).Infof("Streaming and SSE HTTP servers starting on port %s and paths /mcp, /sse, /message", m.StaticConfig.Port)
224+
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
225+
return err
214226
}
227+
return nil
215228
}
216229

217230
if err := mcpServer.ServeStdio(); err != nil && !errors.Is(err, context.Canceled) {

pkg/kubernetes-mcp-server/cmd/testdata/valid-config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
log_level = 1
2-
sse_port = 9999
2+
port = "9999"
33
kubeconfig = "test"
44
list_output = "yaml"
55
read_only = true

pkg/mcp/mcp.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,19 @@ func (s *Server) ServeStdio() error {
8585
return server.ServeStdio(s.server)
8686
}
8787

88-
func (s *Server) ServeSse(baseUrl string) *server.SSEServer {
88+
func (s *Server) ServeSse(baseUrl string, httpServer *http.Server) *server.SSEServer {
8989
options := make([]server.SSEOption, 0)
90-
options = append(options, server.WithSSEContextFunc(contextFunc))
90+
options = append(options, server.WithSSEContextFunc(contextFunc), server.WithHTTPServer(httpServer))
9191
if baseUrl != "" {
9292
options = append(options, server.WithBaseURL(baseUrl))
9393
}
9494
return server.NewSSEServer(s.server, options...)
9595
}
9696

97-
func (s *Server) ServeHTTP() *server.StreamableHTTPServer {
97+
func (s *Server) ServeHTTP(httpServer *http.Server) *server.StreamableHTTPServer {
9898
options := []server.StreamableHTTPOption{
9999
server.WithHTTPContextFunc(contextFunc),
100+
server.WithStreamableHTTPServer(httpServer),
100101
}
101102
return server.NewStreamableHTTPServer(s.server, options...)
102103
}

0 commit comments

Comments
 (0)