Skip to content

Commit 669f3d8

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

File tree

10 files changed

+220
-157
lines changed

10 files changed

+220
-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.1.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.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
213+
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
9+
"github.com/containers/kubernetes-mcp-server/pkg/api"
10+
"github.com/modelcontextprotocol/go-sdk/mcp"
11+
"k8s.io/utils/ptr"
12+
)
13+
14+
func ServerToolToGoSdkTool(s *Server, tool api.ServerTool) (*mcp.Tool, mcp.ToolHandler, error) {
15+
goSdkTool := &mcp.Tool{
16+
Name: tool.Tool.Name,
17+
Description: tool.Tool.Description,
18+
Title: tool.Tool.Annotations.Title,
19+
Annotations: &mcp.ToolAnnotations{
20+
Title: tool.Tool.Annotations.Title,
21+
ReadOnlyHint: ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false),
22+
DestructiveHint: tool.Tool.Annotations.DestructiveHint,
23+
IdempotentHint: ptr.Deref(tool.Tool.Annotations.IdempotentHint, false),
24+
OpenWorldHint: tool.Tool.Annotations.OpenWorldHint,
25+
},
26+
}
27+
if tool.Tool.InputSchema != nil {
28+
schema, err := json.Marshal(tool.Tool.InputSchema)
29+
if err != nil {
30+
return nil, nil, fmt.Errorf("failed to marshal tool input schema for tool %s: %v", tool.Tool.Name, err)
31+
}
32+
// TODO: temporary fix to append an empty properties object (some client have trouble parsing a schema without properties)
33+
// As opposed, Gemini had trouble for a while when properties was present but empty.
34+
// https://github.com/containers/kubernetes-mcp-server/issues/340
35+
if string(schema) == `{"type":"object"}` {
36+
schema = []byte(`{"type":"object","properties":{}}`)
37+
}
38+
39+
var fixedSchema map[string]interface{}
40+
if err := json.Unmarshal(schema, &fixedSchema); err != nil {
41+
return nil, nil, fmt.Errorf("failed to unmarshal tool input schema for tool %s: %v", tool.Tool.Name, err)
42+
}
43+
44+
goSdkTool.InputSchema = fixedSchema
45+
}
46+
goSdkHandler := func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
47+
toolCallRequest, err := GoSdkToolCallRequestToToolCallRequest(request)
48+
if err != nil {
49+
return nil, fmt.Errorf("%v for tool %s", err, tool.Tool.Name)
50+
}
51+
// get the correct derived Kubernetes client for the target specified in the request
52+
cluster := toolCallRequest.GetString(s.p.GetTargetParameterName(), s.p.GetDefaultTarget())
53+
k, err := s.p.GetDerivedKubernetes(ctx, cluster)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
result, err := tool.Handler(api.ToolHandlerParams{
59+
Context: ctx,
60+
Kubernetes: k,
61+
ToolCallRequest: toolCallRequest,
62+
ListOutput: s.configuration.ListOutput(),
63+
})
64+
if err != nil {
65+
return nil, err
66+
}
67+
return NewTextResult(result.Content, result.Error), nil
68+
}
69+
return goSdkTool, goSdkHandler, nil
70+
}
71+
72+
type ToolCallRequest struct {
73+
Name string
74+
arguments map[string]any
75+
}
76+
77+
var _ api.ToolCallRequest = (*ToolCallRequest)(nil)
78+
79+
func GoSdkToolCallRequestToToolCallRequest(request *mcp.CallToolRequest) (*ToolCallRequest, error) {
80+
toolCallParams, ok := request.GetParams().(*mcp.CallToolParamsRaw)
81+
if !ok {
82+
return nil, errors.New("invalid tool call parameters for tool call request")
83+
}
84+
return GoSdkToolCallParamsToToolCallRequest(toolCallParams)
85+
}
86+
87+
func GoSdkToolCallParamsToToolCallRequest(toolCallParams *mcp.CallToolParamsRaw) (*ToolCallRequest, error) {
88+
var arguments map[string]any
89+
if err := json.Unmarshal(toolCallParams.Arguments, &arguments); err != nil {
90+
return nil, fmt.Errorf("failed to unmarshal tool call arguments: %v", err)
91+
}
92+
return &ToolCallRequest{
93+
Name: toolCallParams.Name,
94+
arguments: arguments,
95+
}, nil
96+
}
97+
98+
func (ToolCallRequest *ToolCallRequest) GetArguments() map[string]any {
99+
return ToolCallRequest.arguments
100+
}
101+
102+
func (ToolCallRequest *ToolCallRequest) GetString(key, defaultValue string) string {
103+
if value, ok := ToolCallRequest.arguments[key]; ok {
104+
if strValue, ok := value.(string); ok {
105+
return strValue
106+
}
107+
}
108+
return defaultValue
109+
}

pkg/mcp/m3labs.go

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

0 commit comments

Comments
 (0)