Skip to content

Commit 40074b8

Browse files
committed
feat(auth): Authorize user from custom SSE header
PoC to show how we can propagate an Authorization Bearer token from the MCP client up to the Kubernetes API by passing a custom header (Kubernetes-Authorization-Bearer-Token). A new Derived client is necessary for each request due to the incompleteness of some of the client-go clients. This might add some overhead for each prompt. Ideally, the issue with the discoveryclient and others should be fixed to allow reading the authorization header from the request context.
1 parent c8e8a30 commit 40074b8

File tree

8 files changed

+84
-19
lines changed

8 files changed

+84
-19
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package kubernetes
2+
3+
import "net/http"
4+
5+
const (
6+
AuthorizationHeader = "Kubernetes-Authorization"
7+
AuthorizationBearerTokenHeader = "Kubernetes-Authorization-Bearer-Token"
8+
)
9+
10+
type impersonateRoundTripper struct {
11+
delegate http.RoundTripper
12+
}
13+
14+
func (irt *impersonateRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
15+
// TODO: Solution won't work with discoveryclient which uses context.TODO() instead of the passed-in context
16+
if v, ok := req.Context().Value(AuthorizationHeader).(string); ok {
17+
req.Header.Set("Authorization", v)
18+
}
19+
return irt.delegate.RoundTrip(req)
20+
}

pkg/kubernetes/kubernetes.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kubernetes
22

33
import (
4+
"context"
45
"github.com/fsnotify/fsnotify"
56
"github.com/manusa/kubernetes-mcp-server/pkg/helm"
67
v1 "k8s.io/api/core/v1"
@@ -14,6 +15,7 @@ import (
1415
"k8s.io/client-go/rest"
1516
"k8s.io/client-go/restmapper"
1617
"k8s.io/client-go/tools/clientcmd"
18+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
1719
"sigs.k8s.io/yaml"
1820
)
1921

@@ -41,6 +43,10 @@ func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
4143
if err := resolveKubernetesConfigurations(k8s); err != nil {
4244
return nil, err
4345
}
46+
// TODO: Won't work because not all client-go clients use the shared context (e.g. discovery client uses context.TODO())
47+
//k8s.cfg.Wrap(func(original http.RoundTripper) http.RoundTripper {
48+
// return &impersonateRoundTripper{original}
49+
//})
4450
var err error
4551
k8s.clientSet, err = kubernetes.NewForConfig(k8s.cfg)
4652
if err != nil {
@@ -115,6 +121,37 @@ func (k *Kubernetes) ToRESTMapper() (meta.RESTMapper, error) {
115121
return k.deferredDiscoveryRESTMapper, nil
116122
}
117123

124+
func (k *Kubernetes) Derived(ctx context.Context) *Kubernetes {
125+
bearerToken, ok := ctx.Value(AuthorizationBearerTokenHeader).(string)
126+
if !ok {
127+
return k
128+
}
129+
var _ error // TODO: ignored --> should be handled eventually
130+
derivedCfg := rest.CopyConfig(k.cfg)
131+
derivedCfg.BearerToken = bearerToken
132+
derivedCfg.BearerTokenFile = ""
133+
derivedCfg.AuthProvider = nil
134+
derivedCfg.Username = ""
135+
derivedCfg.Password = ""
136+
derivedCfg.Impersonate = rest.ImpersonationConfig{}
137+
clientcmdapiConfig, _ := k.clientCmdConfig.RawConfig()
138+
clientcmdapiConfig.AuthInfos = make(map[string]*clientcmdapi.AuthInfo)
139+
derived := &Kubernetes{
140+
Kubeconfig: k.Kubeconfig,
141+
clientCmdConfig: clientcmd.NewDefaultClientConfig(clientcmdapiConfig, nil),
142+
cfg: derivedCfg,
143+
}
144+
derived.clientSet, _ = kubernetes.NewForConfig(derived.cfg)
145+
discoveryClient, _ := discovery.NewDiscoveryClientForConfig(derived.cfg)
146+
derived.discoveryClient = memory.NewMemCacheClient(discoveryClient)
147+
derived.deferredDiscoveryRESTMapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(derived.discoveryClient))
148+
derived.dynamicClient, _ = dynamic.NewForConfig(derived.cfg)
149+
derived.scheme = runtime.NewScheme()
150+
derived.parameterCodec = runtime.NewParameterCodec(derived.scheme)
151+
derived.Helm = helm.NewHelm(derived)
152+
return derived
153+
}
154+
118155
func marshal(v any) (string, error) {
119156
switch t := v.(type) {
120157
case []unstructured.Unstructured:

pkg/mcp/events.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func (s *Server) eventsList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.
2727
if namespace == nil {
2828
namespace = ""
2929
}
30-
ret, err := s.k.EventsList(ctx, namespace.(string))
30+
ret, err := s.k.Derived(ctx).EventsList(ctx, namespace.(string))
3131
if err != nil {
3232
return NewTextResult("", fmt.Errorf("failed to list events in all namespaces: %v", err)), nil
3333
}

pkg/mcp/helm.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@ func (s *Server) helmInstall(ctx context.Context, ctr mcp.CallToolRequest) (*mcp
6464
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
6565
namespace = v
6666
}
67-
ret, err := s.k.Helm.Install(ctx, chart, values, name, namespace)
67+
ret, err := s.k.Derived(ctx).Helm.Install(ctx, chart, values, name, namespace)
6868
if err != nil {
6969
return NewTextResult("", fmt.Errorf("failed to install helm chart '%s': %w", chart, err)), nil
7070
}
7171
return NewTextResult(ret, err), nil
7272
}
7373

74-
func (s *Server) helmList(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
74+
func (s *Server) helmList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
7575
allNamespaces := false
7676
if v, ok := ctr.GetArguments()["all_namespaces"].(bool); ok {
7777
allNamespaces = v
@@ -80,14 +80,14 @@ func (s *Server) helmList(_ context.Context, ctr mcp.CallToolRequest) (*mcp.Call
8080
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
8181
namespace = v
8282
}
83-
ret, err := s.k.Helm.List(namespace, allNamespaces)
83+
ret, err := s.k.Derived(ctx).Helm.List(namespace, allNamespaces)
8484
if err != nil {
8585
return NewTextResult("", fmt.Errorf("failed to list helm releases in namespace '%s': %w", namespace, err)), nil
8686
}
8787
return NewTextResult(ret, err), nil
8888
}
8989

90-
func (s *Server) helmUninstall(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
90+
func (s *Server) helmUninstall(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
9191
var name string
9292
ok := false
9393
if name, ok = ctr.GetArguments()["name"].(string); !ok {
@@ -97,7 +97,7 @@ func (s *Server) helmUninstall(_ context.Context, ctr mcp.CallToolRequest) (*mcp
9797
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
9898
namespace = v
9999
}
100-
ret, err := s.k.Helm.Uninstall(name, namespace)
100+
ret, err := s.k.Derived(ctx).Helm.Uninstall(name, namespace)
101101
if err != nil {
102102
return NewTextResult("", fmt.Errorf("failed to uninstall helm chart '%s': %w", name, err)), nil
103103
}

pkg/mcp/mcp.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package mcp
22

33
import (
4+
"context"
45
"github.com/manusa/kubernetes-mcp-server/pkg/kubernetes"
56
"github.com/manusa/kubernetes-mcp-server/pkg/version"
67
"github.com/mark3labs/mcp-go/mcp"
78
"github.com/mark3labs/mcp-go/server"
9+
"net/http"
810
)
911

1012
type Configuration struct {
@@ -71,6 +73,7 @@ func (s *Server) ServeStdio() error {
7173

7274
func (s *Server) ServeSse(baseUrl string) *server.SSEServer {
7375
options := make([]server.SSEOption, 0)
76+
options = append(options, server.WithSSEContextFunc(contextFunc))
7477
if baseUrl != "" {
7578
options = append(options, server.WithBaseURL(baseUrl))
7679
}
@@ -104,3 +107,8 @@ func NewTextResult(content string, err error) *mcp.CallToolResult {
104107
},
105108
}
106109
}
110+
111+
func contextFunc(ctx context.Context, r *http.Request) context.Context {
112+
//return context.WithValue(ctx, kubernetes.AuthorizationHeader, r.Header.Get(kubernetes.AuthorizationHeader))
113+
return context.WithValue(ctx, kubernetes.AuthorizationBearerTokenHeader, r.Header.Get(kubernetes.AuthorizationBearerTokenHeader))
114+
}

pkg/mcp/namespaces.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ func (s *Server) initNamespaces() []server.ServerTool {
3535
}
3636

3737
func (s *Server) namespacesList(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
38-
ret, err := s.k.NamespacesList(ctx)
38+
ret, err := s.k.Derived(ctx).NamespacesList(ctx)
3939
if err != nil {
4040
err = fmt.Errorf("failed to list namespaces: %v", err)
4141
}
4242
return NewTextResult(ret, err), nil
4343
}
4444

4545
func (s *Server) projectsList(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
46-
ret, err := s.k.ProjectsList(ctx)
46+
ret, err := s.k.Derived(ctx).ProjectsList(ctx)
4747
if err != nil {
4848
err = fmt.Errorf("failed to list projects: %v", err)
4949
}

pkg/mcp/pods.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func (s *Server) podsListInAllNamespaces(ctx context.Context, ctr mcp.CallToolRe
110110
selector = labelSelector.(string)
111111
}
112112

113-
ret, err := s.k.PodsListInAllNamespaces(ctx, selector)
113+
ret, err := s.k.Derived(ctx).PodsListInAllNamespaces(ctx, selector)
114114
if err != nil {
115115
return NewTextResult("", fmt.Errorf("failed to list pods in all namespaces: %v", err)), nil
116116
}
@@ -127,7 +127,7 @@ func (s *Server) podsListInNamespace(ctx context.Context, ctr mcp.CallToolReques
127127
if labelSelector != nil {
128128
selector = labelSelector.(string)
129129
}
130-
ret, err := s.k.PodsListInNamespace(ctx, ns.(string), selector)
130+
ret, err := s.k.Derived(ctx).PodsListInNamespace(ctx, ns.(string), selector)
131131
if err != nil {
132132
return NewTextResult("", fmt.Errorf("failed to list pods in namespace %s: %v", ns, err)), nil
133133
}
@@ -143,7 +143,7 @@ func (s *Server) podsGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Cal
143143
if name == nil {
144144
return NewTextResult("", errors.New("failed to get pod, missing argument name")), nil
145145
}
146-
ret, err := s.k.PodsGet(ctx, ns.(string), name.(string))
146+
ret, err := s.k.Derived(ctx).PodsGet(ctx, ns.(string), name.(string))
147147
if err != nil {
148148
return NewTextResult("", fmt.Errorf("failed to get pod %s in namespace %s: %v", name, ns, err)), nil
149149
}
@@ -159,7 +159,7 @@ func (s *Server) podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.
159159
if name == nil {
160160
return NewTextResult("", errors.New("failed to delete pod, missing argument name")), nil
161161
}
162-
ret, err := s.k.PodsDelete(ctx, ns.(string), name.(string))
162+
ret, err := s.k.Derived(ctx).PodsDelete(ctx, ns.(string), name.(string))
163163
if err != nil {
164164
return NewTextResult("", fmt.Errorf("failed to delete pod %s in namespace %s: %v", name, ns, err)), nil
165165
}
@@ -190,7 +190,7 @@ func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Ca
190190
} else {
191191
return NewTextResult("", errors.New("failed to exec in pod, invalid command argument")), nil
192192
}
193-
ret, err := s.k.PodsExec(ctx, ns.(string), name.(string), container.(string), command)
193+
ret, err := s.k.Derived(ctx).PodsExec(ctx, ns.(string), name.(string), container.(string), command)
194194
if err != nil {
195195
return NewTextResult("", fmt.Errorf("failed to exec in pod %s in namespace %s: %v", name, ns, err)), nil
196196
} else if ret == "" {
@@ -212,7 +212,7 @@ func (s *Server) podsLog(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Cal
212212
if container == nil {
213213
container = ""
214214
}
215-
ret, err := s.k.PodsLog(ctx, ns.(string), name.(string), container.(string))
215+
ret, err := s.k.Derived(ctx).PodsLog(ctx, ns.(string), name.(string), container.(string))
216216
if err != nil {
217217
return NewTextResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil
218218
} else if ret == "" {
@@ -238,7 +238,7 @@ func (s *Server) podsRun(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Cal
238238
if port == nil {
239239
port = float64(0)
240240
}
241-
ret, err := s.k.PodsRun(ctx, ns.(string), name.(string), image.(string), int32(port.(float64)))
241+
ret, err := s.k.Derived(ctx).PodsRun(ctx, ns.(string), name.(string), image.(string), int32(port.(float64)))
242242
if err != nil {
243243
return NewTextResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil
244244
}

pkg/mcp/resources.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (s *Server) resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*m
111111
if err != nil {
112112
return NewTextResult("", fmt.Errorf("failed to list resources, %s", err)), nil
113113
}
114-
ret, err := s.k.ResourcesList(ctx, gvk, namespace.(string), labelSelector.(string))
114+
ret, err := s.k.Derived(ctx).ResourcesList(ctx, gvk, namespace.(string), labelSelector.(string))
115115
if err != nil {
116116
return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil
117117
}
@@ -131,7 +131,7 @@ func (s *Server) resourcesGet(ctx context.Context, ctr mcp.CallToolRequest) (*mc
131131
if name == nil {
132132
return NewTextResult("", errors.New("failed to get resource, missing argument name")), nil
133133
}
134-
ret, err := s.k.ResourcesGet(ctx, gvk, namespace.(string), name.(string))
134+
ret, err := s.k.Derived(ctx).ResourcesGet(ctx, gvk, namespace.(string), name.(string))
135135
if err != nil {
136136
return NewTextResult("", fmt.Errorf("failed to get resource: %v", err)), nil
137137
}
@@ -143,7 +143,7 @@ func (s *Server) resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRe
143143
if resource == nil || resource == "" {
144144
return NewTextResult("", errors.New("failed to create or update resources, missing argument resource")), nil
145145
}
146-
ret, err := s.k.ResourcesCreateOrUpdate(ctx, resource.(string))
146+
ret, err := s.k.Derived(ctx).ResourcesCreateOrUpdate(ctx, resource.(string))
147147
if err != nil {
148148
return NewTextResult("", fmt.Errorf("failed to create or update resources: %v", err)), nil
149149
}
@@ -163,7 +163,7 @@ func (s *Server) resourcesDelete(ctx context.Context, ctr mcp.CallToolRequest) (
163163
if name == nil {
164164
return NewTextResult("", errors.New("failed to delete resource, missing argument name")), nil
165165
}
166-
err = s.k.ResourcesDelete(ctx, gvk, namespace.(string), name.(string))
166+
err = s.k.Derived(ctx).ResourcesDelete(ctx, gvk, namespace.(string), name.(string))
167167
if err != nil {
168168
return NewTextResult("", fmt.Errorf("failed to delete resource: %v", err)), nil
169169
}

0 commit comments

Comments
 (0)