Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ config.local.json
# Build output
/bin/
/dist/
/server
/agent

# Temporary files
/tmp/
Expand Down
229 changes: 228 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,240 @@
package main

import (
"context"
"fmt"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/codeready-toolchain/cli-mcp-server/pkg/server"
"github.com/codeready-toolchain/cli-mcp-server/pkg/session"
"github.com/codeready-toolchain/cli-mcp-server/pkg/tools"
"github.com/codeready-toolchain/cli-mcp-server/pkg/version"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

const shutdownTimeout = 310 * time.Second

func main() {
fmt.Fprintf(os.Stderr, "cli-mcp-server %s (built %s)\n", version.Commit, version.BuildTime)
// TODO: Cobra root command, MCP server setup (SANDBOX-1814)

var (
address string
transport string
stateless bool
namespace string
sandboxImage string
kubeconfig string
hmacKeyFile string
idleTimeout time.Duration
warmPoolSize int
)

rootCmd := &cobra.Command{
Use: "cli-mcp-server",
Short: "Sandboxed exec environment MCP server for LLM investigation",
RunE: func(_ *cobra.Command, _ []string) error {
return runServer(runConfig{
address: address,
transport: transport,
stateless: stateless,
namespace: namespace,
sandboxImage: sandboxImage,
kubeconfig: kubeconfig,
hmacKeyFile: hmacKeyFile,
idleTimeout: idleTimeout,
warmPoolSize: warmPoolSize,
})
},
}

rootCmd.Flags().StringVarP(&address, "address", "a", "localhost:8080", "Server address (host:port)")
rootCmd.Flags().StringVarP(&transport, "transport", "t", "stdio", "Transport (stdio, http)")
rootCmd.Flags().BoolVar(&stateless, "stateless", false, "Enable stateless mode (required for HTTP)")
rootCmd.Flags().StringVar(&namespace, "namespace", "tarsy", "Namespace for sandbox pods")
rootCmd.Flags().StringVar(&sandboxImage, "sandbox-image", "", "Container image for sandbox pods (required)")
rootCmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig for sandbox pods")
rootCmd.Flags().StringVar(&hmacKeyFile, "hmac-key-file", "", "Path to HMAC shared secret file (required)")
rootCmd.Flags().DurationVar(&idleTimeout, "idle-timeout", 30*time.Minute, "Idle timeout for sandbox pods")
rootCmd.Flags().IntVar(&warmPoolSize, "warm-pool-size", 0, "Pre-warmed sandbox pods (0 = disabled)")

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

type runConfig struct {
address string
transport string
stateless bool
namespace string
sandboxImage string
kubeconfig string
hmacKeyFile string
idleTimeout time.Duration
warmPoolSize int
}

func runServer(cfg runConfig) error {
if err := server.ValidateTransportFlags(cfg.transport, cfg.stateless, cfg.address); err != nil {
return err
}
if cfg.sandboxImage == "" {
return fmt.Errorf("--sandbox-image is required")
}
hmacKey, err := loadHMACKey(cfg.hmacKeyFile)
if err != nil {
return err
}

logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true,
})).With("server", "cli-mcp-server")

clientset, err := buildClientset(cfg.kubeconfig)
if err != nil {
return fmt.Errorf("failed to create kubernetes client: %w", err)
}

sandboxCfg := session.DefaultConfig()
sandboxCfg.Image = cfg.sandboxImage
sandboxCfg.HMACKey = hmacKey
sandboxCfg.Namespace = cfg.namespace
sandboxCfg.IdleTimeout = cfg.idleTimeout
sandboxCfg.WarmPoolSize = cfg.warmPoolSize
Comment thread
coderabbitai[bot] marked this conversation as resolved.

mgr, err := session.NewSessionManager(clientset, sandboxCfg, logger)
if err != nil {
return fmt.Errorf("failed to create session manager: %w", err)
}

mcpServer := server.NewMCPServer("cli-mcp-server", cfg.stateless, logger)
tools.NewBashTool(mgr).RegisterWith(mcpServer)

// Shared context cancelled on SIGTERM/SIGINT — stops background workers for both transports.
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer cancel()

startCleanupLoop(ctx, mgr, logger)
if cfg.warmPoolSize > 0 {
mgr.StartPool(ctx)
}

switch cfg.transport {
case "http":
return serveHTTP(ctx, cancel, cfg.address, mcpServer, mgr, clientset, cfg.namespace, logger)
case "stdio":
return serveStdio(ctx, mcpServer)
default:
return fmt.Errorf("unsupported transport: %s", cfg.transport)
}
}

func serveHTTP(ctx context.Context, cancel context.CancelFunc, address string, mcpServer *mcp.Server, mgr *session.SessionManager, clientset kubernetes.Interface, namespace string, logger *slog.Logger) error {
checker := &k8sHealthChecker{clientset: clientset, namespace: namespace}
mux := server.NewMux(mcpServer, mgr, checker, logger)

srv := &http.Server{Addr: address, Handler: mux}
serverErrChan := make(chan error, 1)
go func() {
log.Printf("listening on %s (HTTP, stateless)", address)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
serverErrChan <- err
}
}()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

select {
case <-ctx.Done():
log.Println("received shutdown signal, draining...")
case err := <-serverErrChan:
cancel()
return fmt.Errorf("HTTP serve failed: %w", err)
}

cancel()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown error: %v", err)
}
log.Println("shutdown complete")
return nil
}

func serveStdio(ctx context.Context, mcpServer *mcp.Server) error {
log.Println("serving on stdio")
if err := mcpServer.Run(ctx, &mcp.StdioTransport{}); err != nil && ctx.Err() == nil {
return fmt.Errorf("stdio transport error: %w", err)
}
log.Println("shutdown complete")
return nil
}

func startCleanupLoop(ctx context.Context, mgr *session.SessionManager, logger *slog.Logger) {
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
cleaned, err := mgr.CleanupStale(ctx)
if err != nil {
logger.Error("stale cleanup failed", "error", err)
} else if cleaned > 0 {
logger.Info("cleaned stale sessions", "count", cleaned)
}
}
}
}()
}

func loadHMACKey(path string) (string, error) {
if path == "" {
return "", fmt.Errorf("--hmac-key-file is required")
}
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("failed to read HMAC key file: %w", err)
}
if len(data) == 0 {
return "", fmt.Errorf("HMAC key file is empty (zero bytes): %s", path)
}
return string(data), nil
}

func buildClientset(kubeconfigPath string) (kubernetes.Interface, error) {
var config *rest.Config
var err error
if kubeconfigPath != "" {
config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath)
} else {
config, err = rest.InClusterConfig()
}
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(config)
}

type k8sHealthChecker struct {
clientset kubernetes.Interface
namespace string
}

func (c *k8sHealthChecker) CheckHealth(ctx context.Context) error {
_, err := c.clientset.CoreV1().Namespaces().Get(ctx, c.namespace, metav1.GetOptions{})
return err
}
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ module github.com/codeready-toolchain/cli-mcp-server
go 1.25.12

require (
github.com/codeready-toolchain/mcp-common v0.0.0-20260311065550-14ceedd27660
github.com/google/uuid v1.6.0
github.com/modelcontextprotocol/go-sdk v1.4.1
github.com/prometheus/client_golang v1.22.0
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.11.1
k8s.io/api v0.33.4
k8s.io/apimachinery v0.33.4
k8s.io/client-go v0.33.4
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
Expand All @@ -23,15 +28,20 @@ require (
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/net v0.49.0 // indirect
Expand Down
28 changes: 26 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/codeready-toolchain/mcp-common v0.0.0-20260311065550-14ceedd27660 h1:j5vQQF19SBjzaFW3k4FPqLhL0EXmKJf8i+tEM3bSKuY=
github.com/codeready-toolchain/mcp-common v0.0.0-20260311065550-14ceedd27660/go.mod h1:kMt0I6nCr2uUERVj8UNmp+JjJ2rXiCREedsarmLzMQI=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -35,19 +42,25 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
Expand All @@ -67,14 +80,25 @@ github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7y
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand Down
Loading
Loading