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:
@@ -0,0 +1,63 @@
|
||||
// Package mcp exposes GitManager's capabilities to Claude as an MCP server over
|
||||
// Streamable HTTP (AGENT.md §8.1). The tool handlers are THIN ADAPTERS over the
|
||||
// shared service layer (§1.7) — no Git/forge logic lives here. Read tools only
|
||||
// for now; acting/destructive tools arrive with the service methods that back
|
||||
// them, carrying the §1.4 confirmation contract.
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"gitmanager/internal/repos"
|
||||
"gitmanager/internal/service"
|
||||
)
|
||||
|
||||
// getRepoInput is the argument schema for the get_repo tool.
|
||||
type getRepoInput struct {
|
||||
Path string `json:"path" jsonschema:"absolute filesystem path of the repository, exactly as returned by list_repos"`
|
||||
}
|
||||
|
||||
// NewServer builds the MCP server and registers the (currently read-only) tools.
|
||||
func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
||||
s := mcpsdk.NewServer(&mcpsdk.Implementation{
|
||||
Name: "gitmanager",
|
||||
Title: "GitManager",
|
||||
Version: version,
|
||||
Description: "Discover and inspect the user's local Git repositories.",
|
||||
}, nil)
|
||||
|
||||
// list_repos — no arguments (empty struct = object schema with no properties).
|
||||
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||
Name: "list_repos",
|
||||
Description: "List every Git repository GitManager has discovered, each with its current branch, dirty/clean state, ahead/behind counts, and remote names.",
|
||||
}, func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, []repos.State, error) {
|
||||
return nil, svc.ListRepos(), nil
|
||||
})
|
||||
|
||||
// get_repo — details for one already-discovered repository.
|
||||
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||
Name: "get_repo",
|
||||
Description: "Get details for one repository: its local branches (with upstreams), recent commits, and remote URLs. The path must be one returned by list_repos.",
|
||||
}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getRepoInput) (*mcpsdk.CallToolResult, repos.Detail, error) {
|
||||
detail, ok := svc.RepoDetail(ctx, in.Path)
|
||||
if !ok {
|
||||
return nil, repos.Detail{}, fmt.Errorf("unknown repository %q — call list_repos for valid paths", in.Path)
|
||||
}
|
||||
return nil, detail, nil
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Handler serves the MCP server over Streamable HTTP. Mount it at /mcp. Like the
|
||||
// rest of the app it is localhost-bound and unauthenticated (§8.1) — the same
|
||||
// server instance backs every session.
|
||||
func Handler(s *mcpsdk.Server) http.Handler {
|
||||
return mcpsdk.NewStreamableHTTPHandler(func(*http.Request) *mcpsdk.Server {
|
||||
return s
|
||||
}, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user