Files
spark-mcp/main.go
T
tao.chenandClaude 7f8960823b config: GIN_MODE 改为环境变量配置 + Load() 自动加载 .env
main.go 硬编码 gin.ReleaseMode 改成读 cfg.GinMode(默认 release),
方便生产/调试切换 debug/release/test。Load() 开头先调 loadDotenv
(".env"): 缺文件静默跳过,shell env 优先于 .env,跳过注释/空行/
无 '=' 行并 slog.Warn。仅用标准库,不引新依赖。

验证:
  - go build / go vet / go test 全部通过
  - shell env 覆盖 .env (LISTEN_ADDR=:19090 覆盖 .env 的 :18080)
  - 仅 .env 时正确加载 (监听 18080, GIN_MODE=debug 输出 [GIN-debug])
  - 无 .env 且无 shell env 时清晰报错 ADMIN_TOKENS is not set

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-13 13:48:35 +08:00

136 lines
3.2 KiB
Go

// Command spark-mcp-go launches the Spark MCP Server.
//
// Phase 5 scope: config + slog + Gin + /healthz + /admin/* + /mcp
// Streamable HTTP. 2 MCP Tools wired (list_clusters, spark_submit).
package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"spark-mcp-go/internal/admin"
"spark-mcp-go/internal/analyzer"
"spark-mcp-go/internal/audit"
"spark-mcp-go/internal/config"
"spark-mcp-go/internal/httpclient"
"spark-mcp-go/internal/logging"
mcpsrv "spark-mcp-go/internal/mcp"
"spark-mcp-go/internal/mcp/tools"
"spark-mcp-go/internal/middleware"
"spark-mcp-go/internal/storage"
)
func main() {
if err := run(); err != nil {
// run() handles its own logging; this is the last-chance report.
slog.Error("fatal", "err", err)
os.Exit(1)
}
}
func run() error {
cfg, err := config.Load()
if err != nil {
return err
}
logger, shutdownLog, err := logging.New(logging.Config{
Level: cfg.LogLevel,
Dir: cfg.LogDir,
Format: cfg.LogFormat,
LogToStd: true,
})
if err != nil {
return err
}
defer shutdownLog()
slog.SetDefault(logger)
if err := os.MkdirAll(cfg.LogDir+"/tools", 0o700); err != nil {
return err
}
db, err := storage.Open(cfg.SQLitePath)
if err != nil {
return err
}
defer db.Close()
logger.Info("storage.open", "path", cfg.SQLitePath)
gin.SetMode(cfg.GinMode)
r := gin.New()
r.Use(gin.Recovery())
r.GET("/healthz", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true, "version": "0.0.0"})
})
admin.Mount(r, db.Clusters(), audit.NewRepo(db), cfg.AdminTokens)
mcpHandler, err := mcpsrv.Handler(&tools.Deps{
Logger: logger.With("component", "mcp"),
ClusterRepo: db.Clusters(),
SparkSubmitTimeout: cfg.SparkSubmitTimeout,
HTTPClient: httpclient.New(httpclient.Config{
Timeout: cfg.HTTPClientTimeout,
MaxResponseBytes: cfg.MaxResponseBytes,
}),
MaxResponseBytes: cfg.MaxResponseBytes,
DataDir: cfg.DataDir,
AnalyzerThresholds: analyzer.Thresholds{
DataSkewRatio: cfg.AnalyzerDataSkewRatio,
GCPressureRatio: cfg.AnalyzerGCPressureRatio,
BottleneckShuffleGB: cfg.AnalyzerBottleneckShuffleGB,
},
})
if err != nil {
return err
}
// Wire AgentAuth for /mcp. We can't use gin.WrapH alone because
// AgentAuth is a gin middleware; compose it manually:
agentAuth := middleware.AgentAuth(cfg.AgentToken)
r.Any("/mcp", func(c *gin.Context) {
agentAuth(c)
if c.IsAborted() {
return
}
mcpHandler.ServeHTTP(c.Writer, c.Request)
})
srv := &http.Server{
Addr: cfg.ListenAddr,
Handler: r,
ReadHeaderTimeout: 10 * time.Second,
}
errCh := make(chan error, 1)
go func() {
logger.Info("server.start", "addr", cfg.ListenAddr, "data_dir", cfg.DataDir)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
select {
case err := <-errCh:
return err
case sig := <-stop:
logger.Info("server.shutdown", "signal", sig.String())
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return srv.Shutdown(ctx)
}