Skip to content

Commit 06d7c87

Browse files
committed
feat(mcp): refactor to use go-sdk
Signed-off-by: Marc Nuri <[email protected]>
1 parent 09c8b23 commit 06d7c87

File tree

8 files changed

+184
-140
lines changed

8 files changed

+184
-140
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.42.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/gosdk.go

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

pkg/mcp/m3labs.go

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

0 commit comments

Comments
 (0)