package mcp import ( "context" "encoding/json" "io" "log/slog" "os" "os/exec" "path/filepath" "testing" "time" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "gitmanager/internal/git" "gitmanager/internal/repos" "gitmanager/internal/service" ) // TestMCPRoundTrip exercises the full path: a real temp git repo -> scanner -> // service -> MCP tools, called by an in-memory MCP client. func TestMCPRoundTrip(t *testing.T) { root := t.TempDir() repoPath := filepath.Join(root, "myrepo") if err := os.Mkdir(repoPath, 0o755); err != nil { t.Fatal(err) } runGit(t, repoPath, "init", "-b", "main") runGit(t, repoPath, "config", "user.email", "test@example.com") runGit(t, repoPath, "config", "user.name", "Test") if err := os.WriteFile(filepath.Join(repoPath, "README.md"), []byte("hi\n"), 0o644); err != nil { t.Fatal(err) } runGit(t, repoPath, "add", "-A") runGit(t, repoPath, "commit", "-m", "first commit") // Populate the index via the real scanner, then build service + MCP server. log := slog.New(slog.NewTextHandler(io.Discard, nil)) g := git.New("git") scanner := repos.NewScanner(g, log, []string{root}, 3, nil, time.Minute, false) scanner.Refresh(context.Background()) svc := service.New(g, scanner.Index) srv := NewServer(svc, "test") // Wire an in-memory client<->server session. ctx := context.Background() clientT, serverT := mcpsdk.NewInMemoryTransports() serverSession, err := srv.Connect(ctx, serverT, nil) if err != nil { t.Fatalf("server connect: %v", err) } defer serverSession.Close() client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "test", Version: "0"}, nil) cs, err := client.Connect(ctx, clientT, nil) if err != nil { t.Fatalf("client connect: %v", err) } defer cs.Close() // list_repos should find our one repo. res, err := cs.CallTool(ctx, &mcpsdk.CallToolParams{Name: "list_repos"}) if err != nil { t.Fatalf("list_repos: %v", err) } var listOut listReposOutput decodeResult(t, res, &listOut) states := listOut.Repos if len(states) != 1 { t.Fatalf("expected 1 repo, got %d: %+v", len(states), states) } if states[0].Name != "myrepo" || states[0].Branch != "main" { t.Fatalf("unexpected repo state: %+v", states[0]) } // get_repo should return detail including the commit we made. res, err = cs.CallTool(ctx, &mcpsdk.CallToolParams{ Name: "get_repo", Arguments: map[string]any{"path": states[0].Path}, }) if err != nil { t.Fatalf("get_repo: %v", err) } var detail repos.Detail decodeResult(t, res, &detail) if len(detail.Commits) != 1 || detail.Commits[0].Subject != "first commit" { t.Fatalf("unexpected detail commits: %+v", detail.Commits) } // get_repo with a bad path is a tool error, not a protocol error. res, err = cs.CallTool(ctx, &mcpsdk.CallToolParams{ Name: "get_repo", Arguments: map[string]any{"path": filepath.Join(root, "nope")}, }) if err != nil { t.Fatalf("get_repo(bad) protocol error: %v", err) } if !res.IsError { t.Fatalf("expected IsError for unknown repo, got success") } } // decodeResult unmarshals the JSON text content of a tool result into v. func decodeResult(t *testing.T, res *mcpsdk.CallToolResult, v any) { t.Helper() for _, c := range res.Content { if tc, ok := c.(*mcpsdk.TextContent); ok { if err := json.Unmarshal([]byte(tc.Text), v); err != nil { t.Fatalf("unmarshal result: %v (text=%s)", err, tc.Text) } return } } t.Fatalf("no text content in result: %+v", res.Content) } func runGit(t *testing.T, dir string, args ...string) { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } }