Skip to content

Commit ef66d5a

Browse files
committed
Migrate to GO SDK
Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
1 parent 33fa633 commit ef66d5a

36 files changed

Lines changed: 3773 additions & 4260 deletions

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ tidy: ## Run go mod tidy to ensure dependencies are up to date.
5858

5959
.PHONY: test
6060
test: build lint ## Run all tests with build, lint, and coverage
61-
go test -tags=test -v -cover ./pkg/... ./internal/...
61+
go test -tags=test -v -cover ./pkg/... ./internal/... ./cmd/...
6262

6363
.PHONY: test-only
6464
test-only: ## Run tests only (without build/lint for faster iteration)
65-
go test -tags=test -v -cover ./pkg/... ./internal/...
65+
go test -tags=test -v -cover ./pkg/... ./internal/... ./cmd/...
6666

6767
.PHONY: e2e
6868
e2e: test retag

cmd/main.go

Lines changed: 30 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/joho/godotenv"
1818
"github.com/kagent-dev/tools/internal/logger"
19+
mcpserver "github.com/kagent-dev/tools/internal/mcp"
1920
"github.com/kagent-dev/tools/internal/metrics"
2021
"github.com/kagent-dev/tools/internal/telemetry"
2122
"github.com/kagent-dev/tools/internal/version"
@@ -33,8 +34,7 @@ import (
3334
"go.opentelemetry.io/otel/attribute"
3435
"go.opentelemetry.io/otel/codes"
3536

36-
"github.com/mark3labs/mcp-go/mcp"
37-
"github.com/mark3labs/mcp-go/server"
37+
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
3838
)
3939

4040
var (
@@ -140,16 +140,14 @@ func run(cmd *cobra.Command, args []string) {
140140
logger.Get().Info("Running in read-only mode - write operations are disabled")
141141
}
142142

143-
mcp := server.NewMCPServer(
144-
Name,
145-
Version,
146-
)
143+
mcpSrv := sdkmcp.NewServer(&sdkmcp.Implementation{Name: Name, Version: Version}, nil)
144+
145+
// Attach a single receiving middleware that instruments every tools/call
146+
// with an OTel span and Prometheus invocation counters. Per-tool provider
147+
// labels are recorded as each provider registers its tools.
148+
mcpSrv.AddReceivingMiddleware(mcpserver.ToolMiddleware())
147149

148-
// Register tools and wrap handlers with metrics instrumentation.
149-
// registerMCP returns a map of tool_name -> tool_provider so that
150-
// wrapToolHandlersWithMetrics knows which provider each tool belongs to.
151-
toolProviders := registerMCP(mcp, tools, *kubeconfig, readOnly)
152-
wrapToolHandlersWithMetrics(mcp, toolProviders)
150+
registerMCP(mcpSrv, tools, *kubeconfig, readOnly)
153151

154152
// Create wait group for server goroutines
155153
var wg sync.WaitGroup
@@ -167,11 +165,12 @@ func run(cmd *cobra.Command, args []string) {
167165
if stdio {
168166
go func() {
169167
defer wg.Done()
170-
runStdioServer(ctx, mcp)
168+
runStdioServer(ctx, mcpSrv)
171169
}()
172170
} else {
173-
sseServer := server.NewStreamableHTTPServer(mcp,
174-
server.WithHeartbeatInterval(30*time.Second),
171+
sseServer := sdkmcp.NewStreamableHTTPHandler(
172+
func(*http.Request) *sdkmcp.Server { return mcpSrv },
173+
nil,
175174
)
176175

177176
// Create a mux to handle different routes
@@ -293,29 +292,27 @@ func writeResponse(w http.ResponseWriter, data []byte) error {
293292
return err
294293
}
295294

296-
func runStdioServer(ctx context.Context, mcp *server.MCPServer) {
295+
func runStdioServer(ctx context.Context, mcpSrv *sdkmcp.Server) {
297296
logger.Get().Info("Running KAgent Tools Server STDIO:", "tools", strings.Join(tools, ","))
298-
stdioServer := server.NewStdioServer(mcp)
299-
if err := stdioServer.Listen(ctx, os.Stdin, os.Stdout); err != nil {
297+
if err := mcpSrv.Run(ctx, &sdkmcp.StdioTransport{}); err != nil {
300298
logger.Get().Info("Stdio server stopped", "error", err)
301299
}
302300
}
303301

304-
// registerMCP registers tool providers with the MCP server and returns a mapping
305-
// of tool_name -> tool_provider. This mapping is built using the ListTools() diff
306-
// technique: we snapshot the tool list before and after each provider registers,
307-
// so we know exactly which tools belong to which provider.
308-
func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string, readOnly bool) map[string]string {
309-
// A map to hold tool providers and their registration functions
310-
toolProviderMap := map[string]func(*server.MCPServer){
311-
"argo": func(s *server.MCPServer) { argo.RegisterTools(s, readOnly) },
312-
"cilium": func(s *server.MCPServer) { cilium.RegisterTools(s, readOnly) },
313-
"helm": func(s *server.MCPServer) { helm.RegisterTools(s, readOnly) },
314-
"istio": func(s *server.MCPServer) { istio.RegisterTools(s, readOnly) },
315-
"k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig, readOnly) },
316-
"kubescape": func(s *server.MCPServer) { kubescape.RegisterTools(s, kubeconfig, readOnly) },
317-
"prometheus": func(s *server.MCPServer) { prometheus.RegisterTools(s, readOnly) },
318-
"utils": func(s *server.MCPServer) { utils.RegisterTools(s, readOnly) },
302+
// registerMCP registers the enabled tool providers with the MCP server. Each
303+
// provider's RegisterTools call records tool->provider mappings and the tool
304+
// inventory metric centrally (see internal/mcp.AddTool); invocation metrics and
305+
// tracing are applied by the receiving middleware installed in run().
306+
func registerMCP(mcpSrv *sdkmcp.Server, enabledToolProviders []string, kubeconfig string, readOnly bool) {
307+
toolProviderMap := map[string]func(*sdkmcp.Server){
308+
"argo": func(s *sdkmcp.Server) { argo.RegisterTools(s, readOnly) },
309+
"cilium": func(s *sdkmcp.Server) { cilium.RegisterTools(s, readOnly) },
310+
"helm": func(s *sdkmcp.Server) { helm.RegisterTools(s, readOnly) },
311+
"istio": func(s *sdkmcp.Server) { istio.RegisterTools(s, readOnly) },
312+
"k8s": func(s *sdkmcp.Server) { k8s.RegisterTools(s, nil, kubeconfig, readOnly) },
313+
"kubescape": func(s *sdkmcp.Server) { kubescape.RegisterTools(s, kubeconfig, readOnly) },
314+
"prometheus": func(s *sdkmcp.Server) { prometheus.RegisterTools(s, readOnly) },
315+
"utils": func(s *sdkmcp.Server) { utils.RegisterTools(s, readOnly) },
319316
}
320317

321318
// If no specific tools are specified, register all available tools.
@@ -325,82 +322,11 @@ func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfi
325322
}
326323
}
327324

328-
// toolToProvider maps each tool name to its provider (e.g., "kubectl_get" -> "k8s").
329-
// This is used later by wrapToolHandlersWithMetrics to set the correct tool_provider label.
330-
toolToProvider := make(map[string]string)
331-
332325
for _, toolProviderName := range enabledToolProviders {
333326
if registerFunc, ok := toolProviderMap[toolProviderName]; ok {
334-
// Snapshot the tool list before this provider registers its tools.
335-
// We need this because ListTools() returns ALL tools from ALL providers,
336-
// so the only way to know which tools belong to THIS provider is to compare
337-
// the list before and after registration.
338-
toolsBefore := mcp.ListTools()
339-
340-
registerFunc(mcp)
341-
342-
// Determine which tools were just registered by this provider
343-
// by finding tools that exist now but didn't exist before.
344-
// Record each one in Prometheus so we can observe the full tool inventory.
345-
for toolName := range mcp.ListTools() {
346-
if _, existed := toolsBefore[toolName]; !existed {
347-
metrics.KagentToolsMCPRegisteredTools.WithLabelValues(toolName, toolProviderName).Set(1)
348-
toolToProvider[toolName] = toolProviderName
349-
}
350-
}
327+
registerFunc(mcpSrv)
351328
} else {
352329
logger.Get().Error("Unknown tool specified", "provider", toolProviderName)
353330
}
354331
}
355-
356-
return toolToProvider
357-
}
358-
359-
// wrapToolHandlersWithMetrics applies the wrapper/middleware pattern to instrument
360-
// all registered MCP tool handlers with Prometheus invocation counters.
361-
//
362-
// How it works:
363-
// 1. Grab all registered tools from the MCP server using ListTools()
364-
// 2. For each tool, wrap its handler with a function that increments metrics
365-
// 3. Replace all tools in the MCP server using SetTools()
366-
//
367-
// The wrapper function:
368-
// - Increments kagent_tools_mcp_invocations_total on every call
369-
// - Increments kagent_tools_mcp_invocations_failure_total when the handler returns a
370-
// non-nil Go error OR when result.IsError is true (the MCP convention for tool-level
371-
// failures - handlers return NewToolResultError(...), nil, not a Go error)
372-
// - Calls the original handler unchanged - the tool's behaviour is not affected
373-
//
374-
// This uses the standard middleware/decorator pattern: the original handler and the
375-
// wrapped handler have the same function signature, so they are interchangeable.
376-
// No changes are required in any pkg/ file - all instrumentation happens centrally here.
377-
func wrapToolHandlersWithMetrics(mcpServer *server.MCPServer, toolToProvider map[string]string) {
378-
allTools := mcpServer.ListTools()
379-
wrapped := make([]server.ServerTool, 0, len(allTools))
380-
381-
for name, st := range allTools {
382-
originalHandler := st.Handler
383-
toolName := name // capture for closure
384-
provider := toolToProvider[toolName]
385-
386-
wrapped = append(wrapped, server.ServerTool{
387-
Tool: st.Tool,
388-
Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
389-
metrics.KagentToolsMCPInvocationsTotal.WithLabelValues(toolName, provider).Inc()
390-
391-
result, err := originalHandler(ctx, req)
392-
393-
// Count as failure if the Go error is non-nil OR if the tool returned
394-
// a result with IsError=true (the MCP convention for tool-level failures,
395-
// which always return nil for the Go error).
396-
if err != nil || (result != nil && result.IsError) {
397-
metrics.KagentToolsMCPInvocationsFailureTotal.WithLabelValues(toolName, provider).Inc()
398-
}
399-
400-
return result, err
401-
},
402-
})
403-
}
404-
405-
mcpServer.SetTools(wrapped...)
406332
}

cmd/metrics_wrap_test.go

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

0 commit comments

Comments
 (0)