Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

103 lines
3.8 KiB
Go

package mcp
import (
"context"
"fmt"
"strings"
"time"
"github.com/mark3labs/mcp-go/mcp"
"github.com/zzet/gortex/internal/churn"
)
// registerEnrichChurnTool exposes the churn enricher as an MCP tool so
// agents (and the post-commit / post-merge git hook driving `gortex
// enrich churn`) can refresh per-symbol churn data without going
// through the daemon control socket. The handler runs the enricher
// in-process against s.graph, so it inherits whatever backend the
// daemon was launched with — the on-disk backend for persistence, in-memory for
// CI / one-off invocations.
//
// The accompanying `get_churn_rate` tool reads from the same
// meta.churn fields this tool writes; pre-computation here is what
// makes the read path a sub-second graph scan.
func (s *Server) registerEnrichChurnTool() {
s.addTool(
mcp.NewTool("enrich_churn",
mcp.WithDescription("Pre-compute per-file and per-symbol git churn data and stamp it on graph nodes so `get_churn_rate` can answer without a git subprocess. Walks `git log <branch>` and `git blame <branch>` once per file, then projects line-range commit counts onto every function/method node. The branch is the repository's default branch (origin/main, then origin/master, then local main/master/trunk) unless `branch` overrides. Idempotent: re-running updates the same Meta fields in place. Disk-backed daemons (sqlite) persist the result across restarts; in-memory daemons recompute on next call."),
mcp.WithString("branch", mcp.Description("Branch / tag / SHA to compute churn against. Empty means resolve the repository's default branch.")),
mcp.WithString("path", mcp.Description("Optional path or repo prefix to scope the enrichment. Multi-repo daemons enrich every tracked repo when empty.")),
mcp.WithString("format", mcp.Description("Output format: json (default), gcx, or toon")),
),
s.handleEnrichChurn,
)
}
func (s *Server) handleEnrichChurn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
if s.graph == nil {
return mcp.NewToolResultError("graph not initialized"), nil
}
branch := strings.TrimSpace(req.GetString("branch", ""))
pathArg := strings.TrimSpace(req.GetString("path", ""))
// Resolve targets: one repo root per tracked repo, optionally
// filtered by path (matched as either prefix or absolute root).
type target struct {
prefix string
root string
}
var targets []target
if s.multiIndexer != nil {
for prefix, meta := range s.multiIndexer.AllMetadata() {
if pathArg != "" && pathArg != prefix && pathArg != meta.RootPath {
continue
}
targets = append(targets, target{prefix: prefix, root: meta.RootPath})
}
}
if len(targets) == 0 {
return mcp.NewToolResultError(fmt.Sprintf("no tracked repo matches %q", pathArg)), nil
}
started := time.Now()
type perRepo struct {
Prefix string `json:"prefix"`
Branch string `json:"branch"`
HeadSHA string `json:"head_sha"`
Files int `json:"files"`
Symbols int `json:"symbols"`
Skipped string `json:"skipped,omitempty"`
}
var per []perRepo
totalFiles, totalSymbols := 0, 0
for _, t := range targets {
b := branch
if b == "" {
b = churn.DefaultBranch(t.root)
}
if b == "" {
per = append(per, perRepo{Prefix: t.prefix, Skipped: "no default branch resolvable"})
continue
}
res, err := churn.EnrichGraph(ctx, s.graph, t.root, churn.Options{Branch: b})
if err != nil {
per = append(per, perRepo{Prefix: t.prefix, Branch: b, Skipped: err.Error()})
continue
}
per = append(per, perRepo{
Prefix: t.prefix, Branch: res.Branch, HeadSHA: res.HeadSHA,
Files: res.Files, Symbols: res.Symbols,
})
totalFiles += res.Files
totalSymbols += res.Symbols
}
return s.respondJSONOrTOON(ctx, req, map[string]any{
"repos": per,
"files": totalFiles,
"symbols": totalSymbols,
"duration_ms": time.Since(started).Milliseconds(),
})
}