Skip to content

Commit 2bd12f8

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. To use the feature, the MCP Server still needs to be started with a basic configuration (either provided InCluster by a service account or locally by a .kube/config file) so that it's able to infer the server settings.
1 parent c8e8a30 commit 2bd12f8

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)