Skip to content
Open
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
61 changes: 61 additions & 0 deletions pkg/mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package mcp

import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"runtime"
"strings"
"sync"
"testing"

Expand Down Expand Up @@ -262,6 +265,64 @@ func (s *UserAgentPropagationSuite) TestFallsBackToServerPrefixWhenNoClientInfo(
})
}

func (s *UserAgentPropagationSuite) TestDoesNotPanicWhenClientInfoIsNil() {
// Regression test for https://github.com/containers/kubernetes-mcp-server/issues/842
// Fixed in https://github.com/containers/kubernetes-mcp-server/pull/844
//
// The MCP spec mandates that clientInfo is sent during initialization:
// https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
// However, some non-compliant clients omit it, which caused a nil pointer panic
// in the user-agent middleware. This test verifies that the server handles
// non-compliant clients gracefully by sending a raw initialize request without clientInfo.
provider, err := internalk8s.NewProvider(s.Cfg)
s.Require().NoError(err)
s.mcpServer, err = NewServer(Configuration{StaticConfig: s.Cfg}, provider)
s.Require().NoError(err)
handler := s.mcpServer.ServeHTTP()
strippedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Del("User-Agent")
handler.ServeHTTP(w, r)
})
httpServer := httptest.NewServer(strippedHandler)
defer httpServer.Close()

// Send raw initialize request without clientInfo (non-compliant client)
initBody := `{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26"}}`
Comment on lines +289 to +290
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this @manusa ! I couldn't get the SDK to send a init request without the clientInfo, but didn't think of this as another way to get the test to run...

initReq, err := http.NewRequest("POST", httpServer.URL+"/mcp", strings.NewReader(initBody))
s.Require().NoError(err)
initReq.Header.Set("Content-Type", "application/json")
initReq.Header.Set("Accept", "application/json, text/event-stream")
initResp, err := http.DefaultClient.Do(initReq)
s.Require().NoError(err)
defer func() { _ = initResp.Body.Close() }()
_, _ = io.ReadAll(initResp.Body)
sessionID := initResp.Header.Get("Mcp-Session-Id")
s.Require().NotEmpty(sessionID, "Expected session ID in response")

// Send tool call - this would panic before the fix when ClientInfo was nil
toolBody := `{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"pods_list","arguments":{}}}`
toolReq, err := http.NewRequest("POST", httpServer.URL+"/mcp", strings.NewReader(toolBody))
s.Require().NoError(err)
toolReq.Header.Set("Content-Type", "application/json")
toolReq.Header.Set("Accept", "application/json, text/event-stream")
toolReq.Header.Set("Mcp-Session-Id", sessionID)
toolResp, err := http.DefaultClient.Do(toolReq)
s.Require().NoError(err)
defer func() { _ = toolResp.Body.Close() }()

s.pathHeadersMux.Lock()
podsHeaders := s.pathHeaders["/api/v1/namespaces/default/pods"]
s.pathHeadersMux.Unlock()

s.Require().NotNil(podsHeaders, "No requests were made to /api/v1/namespaces/default/pods")
s.Run("User-Agent uses server prefix only when clientInfo is nil", func() {
s.Equal(
fmt.Sprintf("kubernetes-mcp-server/0.0.0 (%s/%s)", runtime.GOOS, runtime.GOARCH),
podsHeaders.Get("User-Agent"),
)
})
}

func TestUserAgentPropagation(t *testing.T) {
suite.Run(t, new(UserAgentPropagationSuite))
}