package main import ( "fmt" "net/http" "os" "os/signal" "path/filepath" "github.com/spf13/cobra" "github.com/zzet/gortex/internal/config" "github.com/zzet/gortex/internal/eval" "github.com/zzet/gortex/internal/graph" "github.com/zzet/gortex/internal/indexer" gortexmcp "github.com/zzet/gortex/internal/mcp" "github.com/zzet/gortex/internal/parser" "github.com/zzet/gortex/internal/parser/languages" "github.com/zzet/gortex/internal/platform" "github.com/zzet/gortex/internal/query" ) var ( evalPort int evalIndex string evalCacheDir string ) var evalServerCmd = &cobra.Command{ Use: "eval-server", Short: "Start the eval HTTP server for benchmarking", Long: "Starts an HTTP daemon wrapping Gortex MCP tools for evaluation. Exposes /health, /tool/{name}, /augment, and /stats endpoints.", RunE: runEvalServer, } func init() { evalServerCmd.Flags().IntVar(&evalPort, "port", 4747, "HTTP port to listen on") evalServerCmd.Flags().StringVar(&evalIndex, "index", "", "repository path to index on startup") evalServerCmd.Flags().StringVar(&evalCacheDir, "cache-dir", "", "index cache directory (default ~/.gortex-eval-cache)") rootCmd.AddCommand(evalServerCmd) } func runEvalServer(cmd *cobra.Command, args []string) error { logger := newLogger() defer func() { _ = logger.Sync() }() cfg, err := config.Load(cfgFile) if err != nil { return fmt.Errorf("loading config: %w", err) } // Build the same graph/parser/indexer/query/MCP stack as serve.go. g := graph.New() reg := parser.NewRegistry() languages.RegisterAll(reg) idx := indexer.New(g, reg, cfg.Index, logger) eng := query.NewEngine(g) eng.SetSearch(idx.Search()) gortexmcp.Version = version srv := gortexmcp.NewServer(eng, g, idx, nil, logger, cfg.Guards.Rules) srv.SetArchitecture(cfg.Architecture) srv.SetEventRules(cfg.Events.Rules) srv.SetArtifacts(cfg.Artifacts) srv.SetNamedQueries(cfg.Queries) // Index the repository if --index is provided, with cache support. if evalIndex != "" { cache, err := eval.NewCache(evalCacheDir, version) if err != nil { return fmt.Errorf("creating index cache: %w", err) } // Derive repo name from directory name and get commit hash. repoName := filepath.Base(evalIndex) commitHash := gitCommitHash(evalIndex) cached := false if commitHash != "" { if cache.Check(repoName, commitHash) && cache.Validate(repoName, commitHash) { cachePath, err := cache.Load(repoName, commitHash) if err == nil { fmt.Fprintf(os.Stderr, "[gortex] eval-server: loaded cached index from %s\n", cachePath) cached = true } else { fmt.Fprintf(os.Stderr, "[gortex] eval-server: cache load failed, will re-index: %v\n", err) } } } if !cached { fmt.Fprintf(os.Stderr, "[gortex] eval-server: indexing %s...\n", evalIndex) result, err := idx.Index(evalIndex) if err != nil { return fmt.Errorf("indexing %s: %w", evalIndex, err) } fmt.Fprintf(os.Stderr, "[gortex] eval-server: indexed %d files (%d nodes, %d edges) in %dms\n", result.FileCount, result.NodeCount, result.EdgeCount, result.DurationMs) // Store to cache for future runs. if commitHash != "" { if err := cache.Store(repoName, commitHash, evalIndex); err != nil { fmt.Fprintf(os.Stderr, "[gortex] eval-server: cache store warning: %v\n", err) } else { fmt.Fprintf(os.Stderr, "[gortex] eval-server: cached index for %s@%s\n", repoName, commitHash[:8]) } } } } // Run analysis (communities, processes) after indexing. srv.RunAnalysis() // Wire the MCP server's tool dispatch into an HTTP handler. handler := eval.NewHandler(srv.MCPServer(), g, version, logger) addr := fmt.Sprintf(":%d", evalPort) httpServer := &http.Server{ Addr: addr, Handler: handler, } fmt.Fprintf(os.Stderr, "[gortex] eval-server listening on http://localhost:%d\n", evalPort) // Start HTTP server in a goroutine. errCh := make(chan error, 1) go func() { if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { errCh <- err } }() // Handle graceful shutdown. sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, platform.ShutdownSignals()...) select { case err := <-errCh: return fmt.Errorf("eval-server: %w", err) case sig := <-sigCh: fmt.Fprintf(os.Stderr, "\n[gortex] eval-server: received %s, shutting down\n", sig) return httpServer.Close() } } // gitCommitHash is defined in git.go