Phase 5 Batch 1: executor + list_clusters + spark_submit + /mcp 路由

- internal/executor: spark-submit 进程执行, exec.Command 切片形式
  (绝不 shell) + stdout 10MiB tail-cap + app_id regex
  + 5 个单测覆盖 slice-form 注入防御 + 退出码
- internal/mcp/server.go: NewServer + Streamable HTTP Handler
- internal/mcp/tools/{list_clusters, spark_submit, deps, helpers}.go
- mcp-go API 实测修正: WithArray 用 Items() 声明 item 类型;
  StreamableHTTPServer 直接实现 http.Handler, 无 .Handler() 方法
- main.go: 挂 /mcp 路由 (AgentAuth + mcpHandler 手动组合)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-10 16:50:02 +08:00
co-authored by Claude
parent 2c39091706
commit 705665745e
5 changed files with 325 additions and 2 deletions
+37 -2
View File
@@ -1,7 +1,7 @@
// Command spark-mcp-go launches the Spark MCP Server.
//
// Phase 4 scope: wire config + slog + Gin + /healthz + /admin/* cluster CRUD
// with audit log. MCP transport and Tool surface land in later phases.
// Phase 5 scope: config + slog + Gin + /healthz + /admin/* + /mcp
// Streamable HTTP. 2 MCP Tools wired (list_clusters, spark_submit).
package main
import (
@@ -17,9 +17,14 @@ import (
"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"
)
@@ -70,6 +75,36 @@ func run() error {
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,