Skip to content

Commit 87ee704

Browse files
feat:(helm) added a new tool for getting the values.yaml of the chart
Signed-off-by: Praneeth Shetty <[email protected]>
1 parent 7a3d668 commit 87ee704

File tree

5 files changed

+272
-6
lines changed

5 files changed

+272
-6
lines changed

pkg/helm/helm.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@ package helm
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"log"
8+
"time"
9+
610
"helm.sh/helm/v3/pkg/action"
711
"helm.sh/helm/v3/pkg/chart/loader"
812
"helm.sh/helm/v3/pkg/cli"
913
"helm.sh/helm/v3/pkg/registry"
1014
"helm.sh/helm/v3/pkg/release"
1115
"k8s.io/cli-runtime/pkg/genericclioptions"
12-
"log"
1316
"sigs.k8s.io/yaml"
14-
"time"
1517
)
1618

1719
type Kubernetes interface {
@@ -140,3 +142,42 @@ func simplify(release ...*release.Release) []map[string]interface{} {
140142
}
141143
return ret
142144
}
145+
146+
func (h *Helm) GetChartValues(ctx context.Context, chart string, version string) (string, error) {
147+
// Create a show action to get chart values
148+
cfg, err := h.newAction("", false)
149+
if err != nil {
150+
return "", err
151+
}
152+
153+
// Create a show action to get chart values with configuration
154+
show := action.NewShowWithConfig(action.ShowValues, cfg)
155+
show.Version = version
156+
157+
// Locate the chart
158+
chartRequested, err := show.LocateChart(chart, cli.New())
159+
if err != nil {
160+
return "", err
161+
}
162+
163+
// Load the chart
164+
chartLoaded, err := loader.Load(chartRequested)
165+
if err != nil {
166+
return "", err
167+
}
168+
169+
// Get the values from the chart
170+
values := chartLoaded.Values
171+
if values == nil {
172+
return "No values found for chart", nil
173+
}
174+
175+
// Convert values to YAML
176+
ret, err := json.MarshalIndent(values, "", " ")
177+
178+
if err != nil {
179+
return "", err
180+
}
181+
182+
return string(ret), nil
183+
}

pkg/mcp/helm.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ func (s *Server) initHelm() []server.ServerTool {
4444
mcp.WithIdempotentHintAnnotation(true),
4545
mcp.WithOpenWorldHintAnnotation(true),
4646
), Handler: s.helmUninstall},
47+
{Tool: mcp.NewTool("helm_values",
48+
mcp.WithDescription("Retrieves the default or overridden values.yaml for a specified Helm chart version. Accepts a chart reference (e.g., stable/grafana, oci://ghcr.io/nginxinc/charts/nginx-ingress) and an optional chart version. If no version is provided, the latest available version is used."),
49+
mcp.WithString("chart", mcp.Description("Chart reference to extract values from, such as stable/grafana or oci://ghcr.io/nginxinc/charts/nginx-ingress"), mcp.Required()),
50+
mcp.WithString("version", mcp.Description("Version of the Helm chart to retrieve values for. Optional; defaults to the latest version if not provided.")),
51+
// Tool annotations
52+
mcp.WithTitleAnnotation("Helm: Values"),
53+
mcp.WithReadOnlyHintAnnotation(false),
54+
mcp.WithDestructiveHintAnnotation(false),
55+
mcp.WithIdempotentHintAnnotation(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install
56+
mcp.WithOpenWorldHintAnnotation(true),
57+
), Handler: s.helmChartValues},
4758
}
4859
}
4960

@@ -116,3 +127,31 @@ func (s *Server) helmUninstall(ctx context.Context, ctr mcp.CallToolRequest) (*m
116127
}
117128
return NewTextResult(ret, err), nil
118129
}
130+
131+
func (s *Server) helmChartValues(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
132+
var chart string
133+
ok := false
134+
if chart, ok = ctr.GetArguments()["chart"].(string); !ok {
135+
return NewTextResult("", fmt.Errorf("missing required argument: chart")), nil
136+
}
137+
138+
version := ""
139+
if v, ok := ctr.GetArguments()["version"].(string); ok {
140+
version = v
141+
}
142+
143+
derived, err := s.k.Derived(ctx)
144+
if err != nil {
145+
return nil, fmt.Errorf("failed to initialize derived context: %w", err)
146+
}
147+
148+
ret, err := derived.NewHelm().GetChartValues(ctx, chart, version)
149+
if err != nil {
150+
if version != "" {
151+
return NewTextResult("", fmt.Errorf("failed to retrieve values for Helm chart '%s' (version %s): %w", chart, version, err)), nil
152+
}
153+
return NewTextResult("", fmt.Errorf("failed to retrieve values for Helm chart '%s': %w", chart, err)), nil
154+
}
155+
156+
return NewTextResult(ret, nil), nil
157+
}

pkg/mcp/helm_test.go

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ package mcp
33
import (
44
"context"
55
"encoding/base64"
6+
"path/filepath"
7+
"runtime"
8+
"strings"
9+
"testing"
10+
611
"github.com/containers/kubernetes-mcp-server/pkg/config"
712
"github.com/mark3labs/mcp-go/mcp"
813
corev1 "k8s.io/api/core/v1"
914
"k8s.io/apimachinery/pkg/api/errors"
1015
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1116
"k8s.io/client-go/kubernetes"
12-
"path/filepath"
13-
"runtime"
1417
"sigs.k8s.io/yaml"
15-
"strings"
16-
"testing"
1718
)
1819

1920
func TestHelmInstall(t *testing.T) {
@@ -254,6 +255,132 @@ func TestHelmUninstallDenied(t *testing.T) {
254255
})
255256
}
256257

258+
func TestHelmValues(t *testing.T) {
259+
testCase(t, func(c *mcpContext) {
260+
c.withEnvTest()
261+
_, file, _, _ := runtime.Caller(0)
262+
chartPath := filepath.Join(filepath.Dir(file), "testdata", "helm-chart-with-values")
263+
264+
t.Run("helm_values with local chart returns values", func(t *testing.T) {
265+
toolResult, err := c.callTool("helm_values", map[string]interface{}{
266+
"chart": chartPath,
267+
})
268+
if err != nil {
269+
t.Fatalf("call tool failed %v", err)
270+
}
271+
if toolResult.IsError {
272+
t.Fatalf("call tool failed: %v", toolResult.Content[0].(mcp.TextContent).Text)
273+
}
274+
275+
// Parse the returned YAML
276+
var values map[string]interface{}
277+
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &values)
278+
if err != nil {
279+
t.Fatalf("invalid tool result content %v", err)
280+
}
281+
282+
// Verify some expected values
283+
if values["replicaCount"] != float64(1) {
284+
t.Fatalf("expected replicaCount to be 1, got %v", values["replicaCount"])
285+
}
286+
287+
if imageMap, ok := values["image"].(map[string]interface{}); ok {
288+
if imageMap["repository"] != "nginx" {
289+
t.Fatalf("expected image.repository to be nginx, got %v", imageMap["repository"])
290+
}
291+
if imageMap["tag"] != "latest" {
292+
t.Fatalf("expected image.tag to be latest, got %v", imageMap["tag"])
293+
}
294+
} else {
295+
t.Fatalf("expected image to be a map, got %T", values["image"])
296+
}
297+
298+
if customConfig, ok := values["customConfig"].(map[string]interface{}); ok {
299+
if customConfig["debug"] != false {
300+
t.Fatalf("expected customConfig.debug to be false, got %v", customConfig["debug"])
301+
}
302+
if customConfig["logLevel"] != "info" {
303+
t.Fatalf("expected customConfig.logLevel to be info, got %v", customConfig["logLevel"])
304+
}
305+
} else {
306+
t.Fatalf("expected customConfig to be a map, got %T", values["customConfig"])
307+
}
308+
})
309+
310+
t.Run("helm_values with version parameter", func(t *testing.T) {
311+
toolResult, err := c.callTool("helm_values", map[string]interface{}{
312+
"chart": chartPath,
313+
"version": "1.0.0",
314+
})
315+
if err != nil {
316+
t.Fatalf("call tool failed %v", err)
317+
}
318+
if toolResult.IsError {
319+
t.Fatalf("call tool failed: %v", toolResult.Content[0].(mcp.TextContent).Text)
320+
}
321+
322+
// Should still return values even with version specified
323+
var values map[string]interface{}
324+
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &values)
325+
if err != nil {
326+
t.Fatalf("invalid tool result content %v", err)
327+
}
328+
329+
if values["replicaCount"] != float64(1) {
330+
t.Fatalf("expected replicaCount to be 1, got %v", values["replicaCount"])
331+
}
332+
})
333+
334+
t.Run("helm_values with chart without values returns no values message", func(t *testing.T) {
335+
chartPathNoValues := filepath.Join(filepath.Dir(file), "testdata", "helm-chart-no-op")
336+
toolResult, err := c.callTool("helm_values", map[string]interface{}{
337+
"chart": chartPathNoValues,
338+
})
339+
if err != nil {
340+
t.Fatalf("call tool failed %v", err)
341+
}
342+
if toolResult.IsError {
343+
t.Fatalf("call tool failed: %v", toolResult.Content[0].(mcp.TextContent).Text)
344+
}
345+
346+
if toolResult.Content[0].(mcp.TextContent).Text != "No values found for chart" {
347+
t.Fatalf("expected 'No values found for chart', got %v", toolResult.Content[0].(mcp.TextContent).Text)
348+
}
349+
})
350+
351+
t.Run("helm_values with missing chart argument returns error", func(t *testing.T) {
352+
toolResult, err := c.callTool("helm_values", map[string]interface{}{})
353+
if err != nil {
354+
t.Fatalf("call tool failed %v", err)
355+
}
356+
if !toolResult.IsError {
357+
t.Fatalf("expected tool to fail with missing chart argument")
358+
}
359+
360+
expectedError := "missing required argument: chart"
361+
if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, expectedError) {
362+
t.Fatalf("expected error to contain '%s', got %v", expectedError, toolResult.Content[0].(mcp.TextContent).Text)
363+
}
364+
})
365+
366+
t.Run("helm_values with invalid chart path returns error", func(t *testing.T) {
367+
toolResult, err := c.callTool("helm_values", map[string]interface{}{
368+
"chart": "/non/existent/path",
369+
})
370+
if err != nil {
371+
t.Fatalf("call tool failed %v", err)
372+
}
373+
if !toolResult.IsError {
374+
t.Fatalf("expected tool to fail with invalid chart path")
375+
}
376+
377+
if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, "failed to retrieve values for Helm chart") {
378+
t.Fatalf("expected error to contain failure message, got %v", toolResult.Content[0].(mcp.TextContent).Text)
379+
}
380+
})
381+
})
382+
}
383+
257384
func clearHelmReleases(ctx context.Context, kc *kubernetes.Clientset) {
258385
secrets, _ := kc.CoreV1().Secrets("default").List(ctx, metav1.ListOptions{})
259386
for _, secret := range secrets.Items {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
apiVersion: v2
2+
name: test-chart-with-values
3+
version: 1.0.0
4+
type: application
5+
description: Test chart with values for testing helm_values tool
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Default values for test-chart-with-values.
2+
# This is a YAML-formatted file.
3+
# Declare variables to be passed into your templates.
4+
5+
replicaCount: 1
6+
7+
image:
8+
repository: nginx
9+
pullPolicy: IfNotPresent
10+
tag: "latest"
11+
12+
nameOverride: ""
13+
fullnameOverride: ""
14+
15+
service:
16+
type: ClusterIP
17+
port: 80
18+
19+
ingress:
20+
enabled: false
21+
className: ""
22+
annotations: {}
23+
hosts:
24+
- host: chart-example.local
25+
paths:
26+
- path: /
27+
pathType: Prefix
28+
tls: []
29+
30+
resources:
31+
limits:
32+
cpu: 100m
33+
memory: 128Mi
34+
requests:
35+
cpu: 100m
36+
memory: 128Mi
37+
38+
nodeSelector: {}
39+
40+
tolerations: []
41+
42+
affinity: {}
43+
44+
# Custom configuration values
45+
customConfig:
46+
debug: false
47+
logLevel: "info"
48+
features:
49+
- authentication
50+
- authorization
51+
database:
52+
host: "localhost"
53+
port: 5432
54+
name: "myapp"

0 commit comments

Comments
 (0)