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>
170 lines
6.6 KiB
Markdown
170 lines
6.6 KiB
Markdown
# 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 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
|
|
```
|