9 new tools to enable full autonomous GitOps loop: repo_status, branch_list/delete/protection_get, pr_list/merge, dir_list, file_delete, tag_create. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.6 KiB
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/<tool_name>.go+internal/tools/<tool_name>_test.go - One client method:
internal/gitea/<domain>.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 hasBranchExists,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 branchbranch_protection_get: 404 →{protected: false}not errordir_list: file path → descriptive errorpr_merge: 405 checks failing, 409 merge conflictrepo_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