Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ See the [DEVELOPMENT.md](DEVELOPMENT.md) file for more information.

- **Go Code**:
- Follow the [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
- Use the official MCP SDK patterns: `github.com/modelcontextprotocol/go-sdk`
- Run `make lint` before submitting your changes
- Ensure all tests pass with `make test`
- Add tests for new functionality
- Follow MCP specification for tool implementations

#### Commit Guidelines

Expand Down
30 changes: 26 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ package category

import (
"context"
"github.com/mark3labs/mcp-go/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/src/go/mcp"
)

type Tools struct {
Expand All @@ -169,11 +169,33 @@ func NewTools() *Tools {
}

func (t *Tools) RegisterTools(server *mcp.Server) {
server.RegisterTool("tool_name", t.handleTool)
tool := mcp.NewTool("tool_name",
mcp.WithDescription("Description of what this tool does"),
mcp.WithString("param1",
mcp.Required(),
mcp.Description("Description of parameter 1"),
),
mcp.WithBool("param2",
mcp.Description("Optional boolean parameter"),
),
)
server.AddTool(tool, t.handleTool)
}

func (t *Tools) handleTool(ctx context.Context, params map[string]interface{}) (*mcp.ToolResult, error) {
// implementation
func (t *Tools) handleTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Parse required parameters with type safety
param1, err := request.RequireString("param1")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Parse optional parameters
param2, _ := request.GetBool("param2")

// Tool implementation logic here
result := fmt.Sprintf("Processing %s with flag %v", param1, param2)

return mcp.NewToolResultText(result), nil
}
```

Expand Down
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,10 @@ helm-uninstall:
helm uninstall kagent --namespace kagent --kube-context kind-$(KIND_CLUSTER_NAME) --wait

.PHONY: helm-install
helm-install: helm-version
helm-install: helm-version retag
#delete first to allow testing with kagent
helm template kagent-tools ./helm/kagent-tools --namespace kagent | kubectl --namespace kagent delete -f - || :
helm $(HELM_ACTION) kagent-tools ./helm/kagent-tools \
--kube-context kind-$(KIND_CLUSTER_NAME) \
--namespace kagent \
--create-namespace \
--history-max 2 \
Expand Down Expand Up @@ -238,12 +239,12 @@ $(LOCALBIN):
mkdir -p $(LOCALBIN)

GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
GOLANGCI_LINT_VERSION ?= v1.63.4
GOLANGCI_LINT_VERSION ?= v2.5.0

.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
Expand Down
94 changes: 84 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,32 @@ For a quickstart guide on how to run KAgent tools using AgentGateway, please ref

## Architecture

The Go tools are implemented as a single MCP server that exposes all available tools through the MCP protocol.
Each tool category is implemented in its own Go file for better organization and maintainability.
The Go tools are implemented as a single MCP server that exposes all available tools through the Model Context Protocol (MCP). Built using the official `github.com/modelcontextprotocol/go-sdk`, the server provides comprehensive Kubernetes, cloud-native, and observability functionality through a unified interface.

### MCP SDK Integration

KAgent Tools leverages the official Model Context Protocol SDK:
- **Official SDK**: Uses `github.com/modelcontextprotocol/go-sdk` for MCP compliance
- **Type Safety**: Strongly-typed parameter validation and parsing
- **JSON Schema**: Automatic schema generation for tool parameters
- **Multiple Transports**: Support for stdio, HTTP, and SSE transports
- **Error Handling**: Standardized error responses following MCP specification
- **Tool Discovery**: Automatic tool registration and capability advertisement

### Package Structure

Each tool category is implemented in its own Go package under `pkg/` for better organization and maintainability:

```
pkg/
├── k8s/ # Kubernetes operations
├── helm/ # Helm package management
├── istio/ # Istio service mesh
├── argo/ # Argo Rollouts
├── cilium/ # Cilium CNI
├── prometheus/ # Prometheus monitoring
└── utils/ # Common utilities
```

## Tool Categories

Expand Down Expand Up @@ -183,10 +207,20 @@ go build -o kagent-tools .

### Running
```bash
./kagent-tools
# Run with stdio transport (default)
./kagent-tools --stdio

# Run with HTTP transport
./kagent-tools --http --port 8084

# Run with custom kubeconfig
./kagent-tools --stdio --kubeconfig ~/.kube/config
```

The server runs using sse transport for MCP communication.
The server supports multiple MCP transports:
- **Stdio**: For direct integration with MCP clients
- **HTTP**: For web-based integrations and debugging
- **SSE**: Server-Sent Events for real-time communication

### Testing
```bash
Expand All @@ -213,11 +247,13 @@ The tools use a common `runCommand` function that:
- Handles timeouts and cancellation

### MCP Integration
All tools are properly integrated with the MCP protocol:
- Use proper parameter parsing with `mcp.ParseString`, `mcp.ParseBool`, etc.
- Return results using `mcp.NewToolResultText` or `mcp.NewToolResultError`
- Include comprehensive tool descriptions and parameter documentation
- Support required and optional parameters
All tools are properly integrated with the official MCP SDK:
- Built using `github.com/modelcontextprotocol/go-sdk`
- Use type-safe parameter parsing with `request.RequireString()`, `request.RequireBool()`, etc.
- Return results using `mcp.NewToolResultText()` or `mcp.NewToolResultError()`
- Include comprehensive tool descriptions and JSON schema parameter validation
- Support required and optional parameters with proper validation
- Follow MCP specification for error handling and result formatting

## Migration from Python

Expand All @@ -239,9 +275,47 @@ This Go implementation provides feature parity with the original Python tools wh

Tools can be configured through environment variables:
- `KUBECONFIG`: Kubernetes configuration file path
- `PROMETHEUS_URL`: Default Prometheus server URL
- `PROMETHEUS_URL`: Default Prometheus server URL (default: http://localhost:9090)
- `GRAFANA_URL`: Default Grafana server URL
- `GRAFANA_API_KEY`: Default Grafana API key
- `LOG_LEVEL`: Logging level (debug, info, warn, error)

## Example Usage

### With MCP Clients

Once connected to an MCP client, you can use natural language to interact with the tools:

```
"List all pods in the default namespace"
→ Uses kubectl_get tool with resource_type="pods", namespace="default"

"Scale the nginx deployment to 3 replicas"
→ Uses kubectl_scale tool with resource_type="deployment", resource_name="nginx", replicas=3

"Show me the Prometheus query for CPU usage"
→ Uses prometheus_query tool with appropriate PromQL query

"Install the nginx helm chart"
→ Uses helm_install tool with chart="nginx"
```

### Direct HTTP API

When running with HTTP transport, you can also interact directly:

```bash
# Check server health
curl http://localhost:8084/health

# Get server metrics
curl http://localhost:8084/metrics

# List available tools (when MCP endpoint is implemented)
curl -X POST http://localhost:8084/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
```

## Error Handling and Debugging

Expand Down
78 changes: 53 additions & 25 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ import (
"github.com/kagent-dev/tools/pkg/k8s"
"github.com/kagent-dev/tools/pkg/prometheus"
"github.com/kagent-dev/tools/pkg/utils"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"

"github.com/mark3labs/mcp-go/server"
)

var (
port int
stdio bool
tools []string
kubeconfig *string
logLevel string
showVersion bool

// These variables should be set during build time using -ldflags
Expand All @@ -54,6 +54,7 @@ var rootCmd = &cobra.Command{

func init() {
rootCmd.Flags().IntVarP(&port, "port", "p", 8084, "Port to run the server on")
rootCmd.Flags().StringVarP(&logLevel, "log-level", "l", "info", "Log level")
rootCmd.Flags().BoolVar(&stdio, "stdio", false, "Use stdio for communication instead of HTTP")
rootCmd.Flags().StringSliceVar(&tools, "tools", []string{}, "List of tools to register. If empty, all tools are registered.")
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information and exit")
Expand All @@ -67,7 +68,7 @@ func init() {

func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
logger.Get().Error("Failed to execute root command", "error", err)
os.Exit(1)
}
}
Expand All @@ -89,7 +90,7 @@ func run(cmd *cobra.Command, args []string) {
return
}

logger.Init(stdio)
logger.Init(stdio, logLevel)
defer logger.Sync()

// Setup context with cancellation for graceful shutdown
Expand Down Expand Up @@ -122,13 +123,13 @@ func run(cmd *cobra.Command, args []string) {

logger.Get().Info("Starting "+Name, "version", Version, "git_commit", GitCommit, "build_date", BuildDate)

mcp := server.NewMCPServer(
Name,
Version,
)
mcpServer := mcp.NewServer(&mcp.Implementation{
Name: Name,
Version: Version,
}, nil)

// Register tools
registerMCP(mcp, tools, *kubeconfig)
registerMCP(mcpServer, tools, *kubeconfig)

// Create wait group for server goroutines
var wg sync.WaitGroup
Expand All @@ -145,12 +146,10 @@ func run(cmd *cobra.Command, args []string) {
if stdio {
go func() {
defer wg.Done()
runStdioServer(ctx, mcp)
runStdioServer(ctx, mcpServer)
}()
} else {
sseServer := server.NewStreamableHTTPServer(mcp,
server.WithHeartbeatInterval(30*time.Second),
)
// HTTP transport implemented using MCP SDK SSE handler

// Create a mux to handle different routes
mux := http.NewServeMux()
Expand All @@ -175,10 +174,14 @@ func run(cmd *cobra.Command, args []string) {
}
})

// Handle all other routes with the MCP server wrapped in telemetry middleware
mux.Handle("/", telemetry.HTTPMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sseServer.ServeHTTP(w, r)
})))
// MCP HTTP transport using SSE handler (2024-11-05 spec)
sseHandler := mcp.NewSSEHandler(func(request *http.Request) *mcp.Server {
// Return the server instance for each request
return mcpServer
}, nil) // nil options uses defaults

// Mount the MCP handler with telemetry middleware
mux.Handle("/mcp", telemetry.HTTPMiddleware(sseHandler))

httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", port),
Expand Down Expand Up @@ -276,22 +279,45 @@ func generateRuntimeMetrics() string {
return metrics.String()
}

func runStdioServer(ctx context.Context, mcp *server.MCPServer) {
func runStdioServer(ctx context.Context, mcpServer *mcp.Server) {
tracer := otel.Tracer("kagent-tools/stdio")
ctx, span := tracer.Start(ctx, "stdio.server.run")
defer span.End()

logger.Get().Info("Running KAgent Tools Server STDIO:", "tools", strings.Join(tools, ","))
stdioServer := server.NewStdioServer(mcp)
if err := stdioServer.Listen(ctx, os.Stdin, os.Stdout); err != nil {
logger.Get().Info("Stdio server stopped", "error", err)

// Create stdio transport - uses stdin/stdout for JSON-RPC communication
stdioTransport := &mcp.StdioTransport{}

span.AddEvent("stdio.transport.starting")

// Run the server on the stdio transport
// This blocks until the context is cancelled or an error occurs
if err := mcpServer.Run(ctx, stdioTransport); err != nil {
// Check if the error is due to context cancellation (normal shutdown)
if !errors.Is(err, context.Canceled) {
logger.Get().Error("Stdio server error", "error", err)
span.RecordError(err)
span.SetStatus(codes.Error, "Stdio server error")
} else {
span.AddEvent("stdio.server.cancelled")
logger.Get().Info("Stdio server cancelled")
}
return
}

span.AddEvent("stdio.server.shutdown")
logger.Get().Info("Stdio server stopped")
}

func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string) {
func registerMCP(mcpServer *mcp.Server, enabledToolProviders []string, kubeconfig string) {
// A map to hold tool providers and their registration functions
toolProviderMap := map[string]func(*server.MCPServer){
toolProviderMap := map[string]func(*mcp.Server) error{
"argo": argo.RegisterTools,
"cilium": cilium.RegisterTools,
"helm": helm.RegisterTools,
"istio": istio.RegisterTools,
"k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig) },
"k8s": func(s *mcp.Server) error { return k8s.RegisterTools(s, nil, kubeconfig) },
"prometheus": prometheus.RegisterTools,
"utils": utils.RegisterTools,
}
Expand All @@ -304,7 +330,9 @@ func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfi
}
for _, toolProviderName := range enabledToolProviders {
if registerFunc, ok := toolProviderMap[toolProviderName]; ok {
registerFunc(mcp)
if err := registerFunc(mcpServer); err != nil {
logger.Get().Error("Failed to register tool provider", "provider", toolProviderName, "error", err)
}
} else {
logger.Get().Error("Unknown tool specified", "provider", toolProviderName)
}
Expand Down
Loading
Loading