Skip to content

Commit 94baad6

Browse files
authored
fix(cmd): disable klog in STDIO mode (#331)
Prevents protocol issues with some clients. Kubernetes tooling uses klog to log messages. Some of these messages end up logged in the stderr or stdout which breaks some of the clients that expect jsonrpc messages. Signed-off-by: Marc Nuri <[email protected]>
1 parent e16114d commit 94baad6

File tree

3 files changed

+39
-12
lines changed

3 files changed

+39
-12
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/spf13/cobra v1.10.1
1515
github.com/spf13/pflag v1.0.10
1616
github.com/stretchr/testify v1.11.1
17+
golang.org/x/net v0.42.0
1718
golang.org/x/oauth2 v0.31.0
1819
golang.org/x/sync v0.17.0
1920
helm.sh/helm/v3 v3.18.6
@@ -122,7 +123,6 @@ require (
122123
go.yaml.in/yaml/v2 v2.4.2 // indirect
123124
go.yaml.in/yaml/v3 v3.0.4 // indirect
124125
golang.org/x/crypto v0.40.0 // indirect
125-
golang.org/x/net v0.42.0 // indirect
126126
golang.org/x/sys v0.34.0 // indirect
127127
golang.org/x/term v0.33.0 // indirect
128128
golang.org/x/text v0.28.0 // indirect

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) {
207207
func (m *MCPServerOptions) initializeLogging() {
208208
flagSet := flag.NewFlagSet("klog", flag.ContinueOnError)
209209
klog.InitFlags(flagSet)
210+
if m.StaticConfig.Port == "" {
211+
// disable klog output for stdio mode
212+
// this is needed to avoid klog writing to stderr and breaking the protocol
213+
_ = flagSet.Parse([]string{"-logtostderr=false", "-alsologtostderr=false", "-stderrthreshold=FATAL"})
214+
return
215+
}
210216
loggerOptions := []textlogger.ConfigOption{textlogger.Output(m.Out)}
211217
if m.StaticConfig.LogLevel >= 0 {
212218
loggerOptions = append(loggerOptions, textlogger.Verbosity(m.StaticConfig.LogLevel))

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

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"strings"
1111
"testing"
1212

13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
1315
"k8s.io/cli-runtime/pkg/genericiooptions"
1416
)
1517

@@ -48,7 +50,7 @@ func TestConfig(t *testing.T) {
4850
t.Run("defaults to none", func(t *testing.T) {
4951
ioStreams, out := testStream()
5052
rootCmd := NewMCPServer(ioStreams)
51-
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
53+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1"})
5254
expectedConfig := `" - Config: "`
5355
if err := rootCmd.Execute(); !strings.Contains(out.String(), expectedConfig) {
5456
t.Fatalf("Expected config to be %s, got %s %v", expectedConfig, out.String(), err)
@@ -59,7 +61,7 @@ func TestConfig(t *testing.T) {
5961
rootCmd := NewMCPServer(ioStreams)
6062
_, file, _, _ := runtime.Caller(0)
6163
emptyConfigPath := filepath.Join(filepath.Dir(file), "testdata", "empty-config.toml")
62-
rootCmd.SetArgs([]string{"--version", "--log-level=1", "--config", emptyConfigPath})
64+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config", emptyConfigPath})
6365
_ = rootCmd.Execute()
6466
expected := `(?m)\" - Config\:[^\"]+empty-config\.toml\"`
6567
if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
@@ -69,7 +71,7 @@ func TestConfig(t *testing.T) {
6971
t.Run("invalid path throws error", func(t *testing.T) {
7072
ioStreams, _ := testStream()
7173
rootCmd := NewMCPServer(ioStreams)
72-
rootCmd.SetArgs([]string{"--version", "--log-level=1", "--config", "invalid-path-to-config.toml"})
74+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config", "invalid-path-to-config.toml"})
7375
err := rootCmd.Execute()
7476
if err == nil {
7577
t.Fatal("Expected error for invalid config path, got nil")
@@ -142,15 +144,15 @@ func TestToolsets(t *testing.T) {
142144
t.Run("default", func(t *testing.T) {
143145
ioStreams, out := testStream()
144146
rootCmd := NewMCPServer(ioStreams)
145-
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
147+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1"})
146148
if err := rootCmd.Execute(); !strings.Contains(out.String(), "- Toolsets: core, config, helm") {
147149
t.Fatalf("Expected toolsets 'full', got %s %v", out, err)
148150
}
149151
})
150152
t.Run("set with --toolsets", func(t *testing.T) {
151153
ioStreams, out := testStream()
152154
rootCmd := NewMCPServer(ioStreams)
153-
rootCmd.SetArgs([]string{"--version", "--log-level=1", "--toolsets", "helm,config"})
155+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--toolsets", "helm,config"})
154156
_ = rootCmd.Execute()
155157
expected := `(?m)\" - Toolsets\: helm, config\"`
156158
if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
@@ -172,15 +174,15 @@ func TestListOutput(t *testing.T) {
172174
t.Run("defaults to table", func(t *testing.T) {
173175
ioStreams, out := testStream()
174176
rootCmd := NewMCPServer(ioStreams)
175-
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
177+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1"})
176178
if err := rootCmd.Execute(); !strings.Contains(out.String(), "- ListOutput: table") {
177179
t.Fatalf("Expected list-output 'table', got %s %v", out, err)
178180
}
179181
})
180182
t.Run("set with --list-output", func(t *testing.T) {
181183
ioStreams, out := testStream()
182184
rootCmd := NewMCPServer(ioStreams)
183-
rootCmd.SetArgs([]string{"--version", "--log-level=1", "--list-output", "yaml"})
185+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--list-output", "yaml"})
184186
_ = rootCmd.Execute()
185187
expected := `(?m)\" - ListOutput\: yaml\"`
186188
if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
@@ -193,15 +195,15 @@ func TestReadOnly(t *testing.T) {
193195
t.Run("defaults to false", func(t *testing.T) {
194196
ioStreams, out := testStream()
195197
rootCmd := NewMCPServer(ioStreams)
196-
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
198+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1"})
197199
if err := rootCmd.Execute(); !strings.Contains(out.String(), " - Read-only mode: false") {
198200
t.Fatalf("Expected read-only mode false, got %s %v", out, err)
199201
}
200202
})
201203
t.Run("set with --read-only", func(t *testing.T) {
202204
ioStreams, out := testStream()
203205
rootCmd := NewMCPServer(ioStreams)
204-
rootCmd.SetArgs([]string{"--version", "--log-level=1", "--read-only"})
206+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--read-only"})
205207
_ = rootCmd.Execute()
206208
expected := `(?m)\" - Read-only mode\: true\"`
207209
if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
@@ -214,15 +216,15 @@ func TestDisableDestructive(t *testing.T) {
214216
t.Run("defaults to false", func(t *testing.T) {
215217
ioStreams, out := testStream()
216218
rootCmd := NewMCPServer(ioStreams)
217-
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
219+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1"})
218220
if err := rootCmd.Execute(); !strings.Contains(out.String(), " - Disable destructive tools: false") {
219221
t.Fatalf("Expected disable destructive false, got %s %v", out, err)
220222
}
221223
})
222224
t.Run("set with --disable-destructive", func(t *testing.T) {
223225
ioStreams, out := testStream()
224226
rootCmd := NewMCPServer(ioStreams)
225-
rootCmd.SetArgs([]string{"--version", "--log-level=1", "--disable-destructive"})
227+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--disable-destructive"})
226228
_ = rootCmd.Execute()
227229
expected := `(?m)\" - Disable destructive tools\: true\"`
228230
if m, err := regexp.MatchString(expected, out.String()); !m || err != nil {
@@ -255,3 +257,22 @@ func TestAuthorizationURL(t *testing.T) {
255257
}
256258
})
257259
}
260+
261+
func TestStdioLogging(t *testing.T) {
262+
t.Run("stdio disables klog", func(t *testing.T) {
263+
ioStreams, out := testStream()
264+
rootCmd := NewMCPServer(ioStreams)
265+
rootCmd.SetArgs([]string{"--version", "--log-level=1"})
266+
err := rootCmd.Execute()
267+
require.NoErrorf(t, err, "Expected no error executing command, got %v", err)
268+
assert.Equalf(t, "0.0.0\n", out.String(), "Expected only version output, got %s", out.String())
269+
})
270+
t.Run("http mode enables klog", func(t *testing.T) {
271+
ioStreams, out := testStream()
272+
rootCmd := NewMCPServer(ioStreams)
273+
rootCmd.SetArgs([]string{"--version", "--log-level=1", "--port=1337"})
274+
err := rootCmd.Execute()
275+
require.NoErrorf(t, err, "Expected no error executing command, got %v", err)
276+
assert.Containsf(t, out.String(), "Starting kubernetes-mcp-server", "Expected klog output, got %s", out.String())
277+
})
278+
}

0 commit comments

Comments
 (0)