# GitOps Agent Tools — Design Spec **Date:** 2026-05-06 **Status:** Approved ## Goal Extend the Gitea MCP server with the tools an AI agent needs to drive a full GitOps development loop autonomously — reading repo state, deciding on a branching strategy, making changes, opening and merging PRs, and tagging releases — without any local git tooling. The agent selects between feature-branch and trunk-based development based on branch protection rules it reads at runtime. --- ## New Tools (9) All tools follow the existing pattern: one file in `internal/tools/`, one Gitea client method in `internal/gitea/`, allowlist check on `owner`, table-driven tests in both packages. ### `repo_status` Convenience read tool — returns branch list, open PRs, and protection info for a target branch in a single call. Designed for the agent's first query on any repo so it can decide its strategy. **Inputs:** `owner`, `name`, `branch` (optional — defaults to repo default branch) **Output:** `{ branches: [...], open_prs: [...], protection: { protected, required_approvals, push_whitelist, merge_whitelist } }` **Implementation:** calls `ListBranches` + `ListPullRequests(state=open)` + `GetBranchProtection` internally, composes result. No new Gitea API surface. --- ### `branch_list` **Inputs:** `owner`, `name`, `page` (optional), `limit` (optional, default 30) **Output:** array of `{ name, sha }` **Gitea endpoint:** `GET /api/v1/repos/{owner}/{repo}/branches` --- ### `branch_delete` **Inputs:** `owner`, `name`, `branch` **Output:** confirmation message **Gitea endpoint:** `DELETE /api/v1/repos/{owner}/{repo}/branches/{branch}` **Error handling:** 403 from Gitea (protected branch) surfaced as a descriptive error. --- ### `branch_protection_get` **Inputs:** `owner`, `name`, `branch` **Output:** `{ protected, required_approvals, push_whitelist, merge_whitelist }` **Gitea endpoint:** `GET /api/v1/repos/{owner}/{repo}/branch_protections/{branch}` **Error handling:** 404 → return `{ protected: false }`, not an error. Allows agent to make clean boolean decisions. --- ### `pr_list` **Inputs:** `owner`, `name`, `state` (`open`/`closed`/`all`, default `open`), `head` (optional branch filter), `page`, `limit` **Output:** array of `{ number, title, state, head_branch, base_branch, draft, html_url }` **Gitea endpoint:** `GET /api/v1/repos/{owner}/{repo}/pulls` --- ### `pr_merge` **Inputs:** `owner`, `name`, `index`, `style` (`merge`/`squash`/`rebase`, default `merge`), `merge_message_title` (optional), `merge_message_field` (optional) **Output:** `{ merged: true, commit_sha }` — if Gitea returns 204 No Content (some merge styles), output is `{ merged: true }` without `commit_sha`. **Gitea endpoint:** `POST /api/v1/repos/{owner}/{repo}/pulls/{index}/merge` **Error handling:** 405 (checks failing) and 409 (merge conflict) passed through with the Gitea error message intact so the agent understands why it failed. --- ### `dir_list` **Inputs:** `owner`, `name`, `path` (empty string = repo root), `ref` (optional branch/tag/SHA) **Output:** array of `{ name, path, type (file|dir|symlink), sha, size }` **Gitea endpoint:** `GET /api/v1/repos/{owner}/{repo}/contents/{path}` **Note:** same endpoint as `file_read` but returns an array when `path` is a directory. Client detects response shape (array vs object). If called on a file path, returns a descriptive error: `"path is a file, not a directory — use file_read"`. --- ### `file_delete` **Inputs:** `owner`, `name`, `path`, `branch`, `message`, `sha` (required — current blob SHA) **Output:** `{ commit_sha, html_url }` **Gitea endpoint:** `DELETE /api/v1/repos/{owner}/{repo}/contents/{path}` --- ### `tag_create` **Inputs:** `owner`, `name`, `tag` (tag name), `target` (branch name or commit SHA), `message` (optional — creates annotated tag if set) **Output:** `{ tag, commit_sha, html_url }` **Gitea endpoint:** `POST /api/v1/repos/{owner}/{repo}/tags` --- ## Gitea Client Methods New methods on `gitea.Client`: | Method | Endpoint | HTTP verb | |--------|----------|-----------| | `ListBranches(ctx, owner, repo, page, limit)` | `/api/v1/repos/{owner}/{repo}/branches` | GET | | `DeleteBranch(ctx, owner, repo, branch)` | `/api/v1/repos/{owner}/{repo}/branches/{branch}` | DELETE | | `GetBranchProtection(ctx, owner, repo, branch)` | `/api/v1/repos/{owner}/{repo}/branch_protections/{branch}` | GET | | `ListPullRequests(ctx, owner, repo, state, head, page, limit)` | `/api/v1/repos/{owner}/{repo}/pulls` | GET | | `MergePullRequest(ctx, owner, repo, index, args)` | `/api/v1/repos/{owner}/{repo}/pulls/{index}/merge` | POST | | `ListContents(ctx, owner, repo, path, ref)` | `/api/v1/repos/{owner}/{repo}/contents/{path}` | GET | | `DeleteFile(ctx, owner, repo, path, args)` | `/api/v1/repos/{owner}/{repo}/contents/{path}` | DELETE | | `CreateTag(ctx, owner, repo, args)` | `/api/v1/repos/{owner}/{repo}/tags` | POST | --- ## Architecture No structural changes. Each new tool is: - One file: `internal/tools/.go` + `internal/tools/_test.go` - One client method: `internal/gitea/.go` (added to existing domain files where logical) - Registered in `cmd/gitea-mcp/main.go` `repo_status` is the only tool with internal composition — it calls three client methods and merges their results. It has no dedicated client method of its own. New client methods go in existing domain files: - Branch methods → `internal/gitea/files.go` (already has `BranchExists`, `CreateBranch`) - PR methods → `internal/gitea/pulls.go` - Contents (dir_list, file_delete) → `internal/gitea/files.go` - Tags → new `internal/gitea/tags.go` --- ## Testing Pattern: table-driven tests with a `httptest.NewServer` mock, same as `file_write_branch_test.go`. Each tool covers: - Happy path - 404 response - Allowlist rejection - Tool-specific edge cases: - `branch_delete`: 403 protected branch - `branch_protection_get`: 404 → `{protected: false}` not error - `dir_list`: file path → descriptive error - `pr_merge`: 405 checks failing, 409 merge conflict - `repo_status`: any one sub-call failing propagates the error --- ## Agent Decision Flow (Reference) ``` 1. repo_status(owner, name) → if branch.protected && required_approvals > 0: use feature-branch workflow → else: use trunk-based workflow Feature-branch workflow: file_write_branch (auto-creates branch) → pr_create → [wait for CI via workflow_run_status] → pr_merge → branch_delete Trunk-based workflow: file_write_branch(branch=main) → [optionally] tag_create Post-merge (either): → [optionally] tag_create to trigger deployment ```