Skip to content

Commit 9455a33

Browse files
committed
feat(mcp): refactor to use go-sdk
Signed-off-by: Marc Nuri <marc@marcnuri.com>
1 parent a301b0f commit 9455a33

File tree

9 files changed

+212
-157
lines changed

9 files changed

+212
-157
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/go-jose/go-jose/v4 v4.1.3
1010
github.com/google/jsonschema-go v0.3.0
1111
github.com/mark3labs/mcp-go v0.43.0
12+
github.com/modelcontextprotocol/go-sdk v1.0.0
1213
github.com/pkg/errors v0.9.1
1314
github.com/spf13/afero v1.15.0
1415
github.com/spf13/cobra v1.10.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU
209209
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
210210
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
211211
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
212+
github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74=
213+
github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs=
212214
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
213215
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
214216
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

pkg/http/http.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, staticConfig *config.Stat
3636
Handler: wrappedMux,
3737
}
3838

39-
sseServer := mcpServer.ServeSse(staticConfig.SSEBaseURL, httpServer)
40-
streamableHttpServer := mcpServer.ServeHTTP(httpServer)
39+
sseServer := mcpServer.ServeSse()
40+
streamableHttpServer := mcpServer.ServeHTTP()
4141
mux.Handle(sseEndpoint, sseServer)
4242
mux.Handle(sseMessageEndpoint, sseServer)
4343
mux.Handle(mcpEndpoint, streamableHttpServer)

pkg/kubernetes-mcp-server/cmd/root.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,8 @@ func (m *MCPServerOptions) Run() error {
345345
return internalhttp.Serve(ctx, mcpServer, m.StaticConfig, oidcProvider, httpClient)
346346
}
347347

348-
if err := mcpServer.ServeStdio(); err != nil && !errors.Is(err, context.Canceled) {
348+
ctx := context.Background()
349+
if err := mcpServer.ServeStdio(ctx); err != nil && !errors.Is(err, context.Canceled) {
349350
return err
350351
}
351352

pkg/mcp/common_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func (s *BaseMcpSuite) InitMcpClient(options ...transport.StreamableHTTPCOption)
190190
var err error
191191
s.mcpServer, err = NewServer(Configuration{StaticConfig: s.Cfg})
192192
s.Require().NoError(err, "Expected no error creating MCP server")
193-
s.McpClient = test.NewMcpClient(s.T(), s.mcpServer.ServeHTTP(nil), options...)
193+
s.McpClient = test.NewMcpClient(s.T(), s.mcpServer.ServeHTTP(), options...)
194194
}
195195

196196
// EnvTestInOpenShift sets up the kubernetes environment to seem to be running OpenShift

pkg/mcp/gosdk.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package mcp
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"slices"
10+
11+
"github.com/containers/kubernetes-mcp-server/pkg/api"
12+
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
13+
"github.com/modelcontextprotocol/go-sdk/mcp"
14+
"k8s.io/klog/v2"
15+
"k8s.io/utils/ptr"
16+
)
17+
18+
func authHeaderPropagationMiddleware(next mcp.MethodHandler) mcp.MethodHandler {
19+
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
20+
if req.GetExtra() != nil && req.GetExtra().Header != nil {
21+
// Get the standard Authorization header (OAuth compliant)
22+
authHeader := req.GetExtra().Header.Get(string(internalk8s.OAuthAuthorizationHeader))
23+
if authHeader != "" {
24+
return next(context.WithValue(ctx, internalk8s.OAuthAuthorizationHeader, authHeader), method, req)
25+
}
26+
27+
// Fallback to custom header for backward compatibility
28+
customAuthHeader := req.GetExtra().Header.Get(string(internalk8s.CustomAuthorizationHeader))
29+
if customAuthHeader != "" {
30+
return next(context.WithValue(ctx, internalk8s.OAuthAuthorizationHeader, customAuthHeader), method, req)
31+
}
32+
}
33+
return next(ctx, method, req)
34+
}
35+
}
36+
37+
func toolCallLoggingMiddleware(next mcp.MethodHandler) mcp.MethodHandler {
38+
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
39+
switch params := req.GetParams().(type) {
40+
case *mcp.CallToolParamsRaw:
41+
toolCallRequest, _ := GoSdkToolCallParamsToToolCallRequest(params)
42+
klog.V(5).Infof("mcp tool call: %s(%v)", toolCallRequest.Name, toolCallRequest.GetArguments())
43+
if req.GetExtra() != nil && req.GetExtra().Header != nil {
44+
buffer := bytes.NewBuffer(make([]byte, 0))
45+
if err := req.GetExtra().Header.WriteSubset(buffer, map[string]bool{"Authorization": true, "authorization": true}); err == nil {
46+
klog.V(7).Infof("mcp tool call headers: %s", buffer)
47+
}
48+
}
49+
}
50+
return next(ctx, method, req)
51+
}
52+
}
53+
54+
func toolScopedAuthorizationMiddleware(next mcp.MethodHandler) mcp.MethodHandler {
55+
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
56+
scopes, ok := ctx.Value(TokenScopesContextKey).([]string)
57+
if !ok {
58+
return NewTextResult("", fmt.Errorf("authorization failed: Access denied: Tool '%s' requires scope 'mcp:%s' but no scope is available", method, method)), nil
59+
}
60+
if !slices.Contains(scopes, "mcp:"+method) && !slices.Contains(scopes, method) {
61+
return NewTextResult("", fmt.Errorf("authorization failed: Access denied: Tool '%s' requires scope 'mcp:%s' but only scopes %s are available", method, method, scopes)), nil
62+
}
63+
return next(ctx, method, req)
64+
}
65+
}
66+
67+
func ServerToolToGoSdkTool(s *Server, tool api.ServerTool) (*mcp.Tool, mcp.ToolHandler, error) {
68+
goSdkTool := &mcp.Tool{
69+
Name: tool.Tool.Name,
70+
Description: tool.Tool.Description,
71+
Title: tool.Tool.Annotations.Title,
72+
Annotations: &mcp.ToolAnnotations{
73+
Title: tool.Tool.Annotations.Title,
74+
ReadOnlyHint: ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false),
75+
DestructiveHint: tool.Tool.Annotations.DestructiveHint,
76+
IdempotentHint: ptr.Deref(tool.Tool.Annotations.IdempotentHint, false),
77+
OpenWorldHint: tool.Tool.Annotations.OpenWorldHint,
78+
},
79+
}
80+
if tool.Tool.InputSchema != nil {
81+
schema, err := json.Marshal(tool.Tool.InputSchema)
82+
if err != nil {
83+
return nil, nil, fmt.Errorf("failed to marshal tool input schema for tool %s: %v", tool.Tool.Name, err)
84+
}
85+
// TODO: temporary fix to append an empty properties object (some client have trouble parsing a schema without properties)
86+
// As opposed, Gemini had trouble for a while when properties was present but empty.
87+
// https://github.com/containers/kubernetes-mcp-server/issues/340
88+
if string(schema) == `{"type":"object"}` {
89+
schema = []byte(`{"type":"object","properties":{}}`)
90+
}
91+
92+
var fixedSchema map[string]interface{}
93+
if err := json.Unmarshal(schema, &fixedSchema); err != nil {
94+
return nil, nil, fmt.Errorf("failed to unmarshal tool input schema for tool %s: %v", tool.Tool.Name, err)
95+
}
96+
97+
goSdkTool.InputSchema = fixedSchema
98+
}
99+
goSdkHandler := func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
100+
toolCallRequest, err := GoSdkToolCallRequestToToolCallRequest(request)
101+
if err != nil {
102+
return nil, fmt.Errorf("%v for tool %s", err, tool.Tool.Name)
103+
}
104+
// get the correct derived Kubernetes client for the target specified in the request
105+
cluster := toolCallRequest.GetString(s.p.GetTargetParameterName(), s.p.GetDefaultTarget())
106+
k, err := s.p.GetDerivedKubernetes(ctx, cluster)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
result, err := tool.Handler(api.ToolHandlerParams{
112+
Context: ctx,
113+
Kubernetes: k,
114+
ToolCallRequest: toolCallRequest,
115+
ListOutput: s.configuration.ListOutput(),
116+
})
117+
if err != nil {
118+
return nil, err
119+
}
120+
return NewTextResult(result.Content, result.Error), nil
121+
}
122+
return goSdkTool, goSdkHandler, nil
123+
}
124+
125+
type ToolCallRequest struct {
126+
Name string
127+
arguments map[string]any
128+
}
129+
130+
var _ api.ToolCallRequest = (*ToolCallRequest)(nil)
131+
132+
func GoSdkToolCallRequestToToolCallRequest(request *mcp.CallToolRequest) (*ToolCallRequest, error) {
133+
toolCallParams, ok := request.GetParams().(*mcp.CallToolParamsRaw)
134+
if !ok {
135+
return nil, errors.New("invalid tool call parameters for tool call request")
136+
}
137+
return GoSdkToolCallParamsToToolCallRequest(toolCallParams)
138+
}
139+
140+
func GoSdkToolCallParamsToToolCallRequest(toolCallParams *mcp.CallToolParamsRaw) (*ToolCallRequest, error) {
141+
var arguments map[string]any
142+
if err := json.Unmarshal(toolCallParams.Arguments, &arguments); err != nil {
143+
return nil, fmt.Errorf("failed to unmarshal tool call arguments: %v", err)
144+
}
145+
return &ToolCallRequest{
146+
Name: toolCallParams.Name,
147+
arguments: arguments,
148+
}, nil
149+
}
150+
151+
func (ToolCallRequest *ToolCallRequest) GetArguments() map[string]any {
152+
return ToolCallRequest.arguments
153+
}
154+
155+
func (ToolCallRequest *ToolCallRequest) GetString(key, defaultValue string) string {
156+
if value, ok := ToolCallRequest.arguments[key]; ok {
157+
if strValue, ok := value.(string); ok {
158+
return strValue
159+
}
160+
}
161+
return defaultValue
162+
}

pkg/mcp/m3labs.go

Lines changed: 0 additions & 63 deletions
This file was deleted.

0 commit comments

Comments
 (0)