package server import ( "bytes" "context" "io" "net" "net/http" "testing" "time" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/stretchr/testify/assert" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" ) func TestServe(t *testing.T) { logger, _ := zap.NewDevelopment() defer func() { err := logger.Sync() if err != nil { t.Logf("logger.Sync() error: %v", err) } }() s := &Config{ Port: "50059", Logger: logger, EnableHttp: false, } go func() { err := s.Serve() time.Sleep(time.Second * 2) assert.NoError(t, err, "Serve should not return an error") }() // Wait until the server is ready to accept connections err := waitForPort("localhost:50059", 10*time.Second) assert.NoError(t, err, "Server should start without error") conn, err := grpc.Dial("localhost:50059", grpc.WithInsecure()) assert.NoError(t, err, "Should be able to dial the server") defer func() { if err := conn.Close(); err != nil { t.Logf("failed to close connection: %v", err) } }() // Test a simple gRPC reflection request cli := grpc_reflection_v1alpha.NewServerReflectionClient(conn) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := cli.ServerReflectionInfo(ctx) assert.NoError(t, err, "Should be able to get server reflection info") assert.NotNil(t, resp, "Response should not be nil") // Cleanup err = s.Shutdown() assert.NoError(t, err, "Shutdown should not return an error") } // TestMCPServerCreation tests the creation of an MCP server func TestMCPServerCreation(t *testing.T) { logger, _ := zap.NewDevelopment() defer func() { err := logger.Sync() if err != nil { t.Logf("logger.Sync() error: %v", err) } }() aiProvider := &ai.AIProvider{ Name: "test-provider", Password: "test-password", Model: "test-model", } // Test HTTP mode mcpServer, err := NewMCPServer("8088", aiProvider, true, logger) assert.NoError(t, err, "Should be able to create MCP server with HTTP transport") assert.NotNil(t, mcpServer, "MCP server should not be nil") assert.True(t, mcpServer.useHTTP, "MCP server should be in HTTP mode") assert.Equal(t, "8088", mcpServer.port, "Port should be set correctly") // Test stdio mode mcpServerStdio, err := NewMCPServer("8088", aiProvider, false, logger) assert.NoError(t, err, "Should be able to create MCP server with stdio transport") assert.NotNil(t, mcpServerStdio, "MCP server should not be nil") assert.False(t, mcpServerStdio.useHTTP, "MCP server should be in stdio mode") } // TestMCPServerBasicHTTP tests basic HTTP connectivity to the MCP server func TestMCPServerBasicHTTP(t *testing.T) { logger, _ := zap.NewDevelopment() defer func() { err := logger.Sync() if err != nil { t.Logf("logger.Sync() error: %v", err) } }() aiProvider := &ai.AIProvider{ Name: "test-provider", Password: "test-password", Model: "test-model", } mcpServer, err := NewMCPServer("8091", aiProvider, true, logger) assert.NoError(t, err, "Should be able to create MCP server") // For HTTP mode, the server is already started in NewMCPServer // No need to call Start() as it's already running in a goroutine // Wait for the server to start err = waitForPort("localhost:8091", 10*time.Second) if err != nil { t.Skipf("MCP server did not start within timeout: %v", err) } // First, initialize the session initRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {}, "resources": {}, "prompts": {} }, "clientInfo": { "name": "test-client", "version": "1.0.0" } } }` initResp, err := http.Post("http://localhost:8091/mcp", "application/json", bytes.NewBufferString(initRequest)) if err != nil { t.Logf("Initialize request failed: %v", err) return } defer func() { if err := initResp.Body.Close(); err != nil { t.Logf("Error closing init response body: %v", err) } }() // Read initialization response initBody, err := io.ReadAll(initResp.Body) if err != nil { t.Logf("Failed to read init response body: %v", err) } else { t.Logf("Init response status: %d, body: %s", initResp.StatusCode, string(initBody)) } // Extract session ID from response headers if present sessionID := initResp.Header.Get("Mcp-Session-Id") if sessionID == "" { t.Logf("No session ID in response headers") } // Now test tools/list with session ID if available headers := map[string]string{ "Content-Type": "application/json", "Accept": "application/json,text/event-stream", } if sessionID != "" { headers["Mcp-Session-Id"] = sessionID } req, err := http.NewRequest("POST", "http://localhost:8091/mcp", bytes.NewBufferString(`{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`)) if err != nil { t.Logf("Failed to create request: %v", err) return } for key, value := range headers { req.Header.Set(key, value) } client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Logf("MCP endpoint test skipped (server might not be fully ready): %v", err) return } defer func() { err := resp.Body.Close() if err != nil { t.Logf("resp.Body.Close() error: %v", err) } }() // Read response body for debugging body, err := io.ReadAll(resp.Body) if err != nil { t.Logf("Failed to read response body: %v", err) } else { t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body)) } // Accept both 200 and 404 as valid responses (404 means endpoint not implemented) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { t.Errorf("MCP endpoint returned unexpected status: %d", resp.StatusCode) } // Cleanup err = mcpServer.Close() assert.NoError(t, err, "MCP server should close without error") } // TestMCPServerToolCall tests calling a specific tool (analyze) through the MCP server func TestMCPServerToolCall(t *testing.T) { logger, _ := zap.NewDevelopment() defer func() { err := logger.Sync() if err != nil { t.Logf("logger.Sync() error: %v", err) } }() aiProvider := &ai.AIProvider{ Name: "test-provider", Password: "test-password", Model: "test-model", } mcpServer, err := NewMCPServer("8090", aiProvider, true, logger) assert.NoError(t, err, "Should be able to create MCP server") // For HTTP mode, the server is already started in NewMCPServer // No need to call Start() as it's already running in a goroutine // Wait for the server to start err = waitForPort("localhost:8090", 10*time.Second) if err != nil { t.Skipf("MCP server did not start within timeout: %v", err) } // First, initialize the session initRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {}, "resources": {}, "prompts": {} }, "clientInfo": { "name": "test-client", "version": "1.0.0" } } }` initResp, err := http.Post("http://localhost:8090/mcp", "application/json", bytes.NewBufferString(initRequest)) if err != nil { t.Logf("Initialize request failed: %v", err) return } defer func() { if err := initResp.Body.Close(); err != nil { t.Logf("Error closing init response body: %v", err) } }() // Extract session ID from response headers if present sessionID := initResp.Header.Get("Mcp-Session-Id") // Test calling the analyze tool with proper JSON-RPC format analyzeRequest := `{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "analyze", "arguments": { "namespace": "default", "backend": "openai", "language": "english", "explain": true, "maxConcurrency": 10 } } }` // Create request with session ID if available req, err := http.NewRequest("POST", "http://localhost:8090/mcp", bytes.NewBufferString(analyzeRequest)) if err != nil { t.Logf("Failed to create request: %v", err) return } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json,text/event-stream") if sessionID != "" { req.Header.Set("Mcp-Session-Id", sessionID) } client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Logf("Analyze tool call test skipped (server might not be fully ready): %v", err) return } defer func() { err := resp.Body.Close() if err != nil { t.Logf("resp.Body.Close() error: %v", err) } }() // Accept both 200 and 404 as valid responses if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { t.Errorf("Analyze tool call returned unexpected status: %d", resp.StatusCode) } // Cleanup err = mcpServer.Close() assert.NoError(t, err, "MCP server should close without error") } func waitForPort(address string, timeout time.Duration) error { start := time.Now() for { conn, err := net.Dial("tcp", address) if err == nil { _ = conn.Close() return nil } if time.Since(start) > timeout { return err } time.Sleep(100 * time.Millisecond) } }