Add service layer and MCP server with read tools
Slice 1: internal/service is the one capability layer both the HTTP API and the MCP server call (AGENT.md 1.7); /api/repos and /api/repo route through it. Slice 2: internal/mcp serves an MCP server over Streamable HTTP at /mcp (go-sdk v1.8.0) with read tools list_repos and get_repo as thin adapters over the service, plus a round-trip test. Enabled air polling so hot reload works across the Docker-on-Windows bind mount. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-8
@@ -8,7 +8,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -18,8 +17,10 @@ import (
|
||||
"gitmanager/internal/config"
|
||||
"gitmanager/internal/git"
|
||||
"gitmanager/internal/logging"
|
||||
mcpserver "gitmanager/internal/mcp"
|
||||
"gitmanager/internal/render"
|
||||
"gitmanager/internal/repos"
|
||||
"gitmanager/internal/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -50,6 +51,9 @@ func main() {
|
||||
go scanner.Run(scanCtx)
|
||||
log.Info("scanner started", "roots", cfg.RepoRoots, "interval", cfg.ScanInterval.String(), "fetch", cfg.ScanFetchEnabled)
|
||||
|
||||
// The one service layer both the HTTP API and the MCP server call (§1.7).
|
||||
svc := service.New(g, scanner.Index)
|
||||
|
||||
tmpl, err := render.New("web/templates")
|
||||
if err != nil {
|
||||
log.Error("failed to parse templates", "err", err)
|
||||
@@ -79,20 +83,24 @@ func main() {
|
||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
})
|
||||
e.GET("/api/repos", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, scanner.Index.List())
|
||||
return c.JSON(http.StatusOK, svc.ListRepos())
|
||||
})
|
||||
e.GET("/api/repo", func(c echo.Context) error {
|
||||
// Only serve details for a repo we already discovered — never run git
|
||||
// against an arbitrary path supplied in the query string. Clean the
|
||||
// input so separator style (/, \) doesn't defeat the exact-match lookup.
|
||||
path := filepath.Clean(c.QueryParam("path"))
|
||||
base, ok := scanner.Index.Get(path)
|
||||
// The service only serves details for an already-discovered repo — it
|
||||
// never runs git against an arbitrary caller-supplied path (§1.3).
|
||||
detail, ok := svc.RepoDetail(c.Request().Context(), c.QueryParam("path"))
|
||||
if !ok {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "unknown repository"})
|
||||
}
|
||||
return c.JSON(http.StatusOK, repos.BuildDetail(c.Request().Context(), g, base))
|
||||
return c.JSON(http.StatusOK, detail)
|
||||
})
|
||||
|
||||
// MCP server — Claude connects here as a custom connector (§8.1). Same
|
||||
// service layer as the HTTP API (§1.7); localhost-bound like everything else.
|
||||
mcpSrv := mcpserver.NewServer(svc, "0.1.0")
|
||||
e.Any("/mcp", echo.WrapHandler(mcpserver.Handler(mcpSrv)))
|
||||
log.Info("mcp server mounted", "path", "/mcp")
|
||||
|
||||
// Serve with graceful shutdown.
|
||||
go func() {
|
||||
if err := e.Start(cfg.ListenAddr); err != nil && err != http.ErrServerClosed {
|
||||
|
||||
Reference in New Issue
Block a user