feat: add PR/release/branch/milestone/comment write tools

Adds six opt-in write tools (write-mode + policy + per-user permission still
enforced; no destructive or admin actions):

- create_pull_request (POST /pulls)
- create_release / edit_release (POST/PATCH /releases)
- create_branch (POST /branches; create only, no deletion)
- create_milestone (POST /milestones)
- edit_issue_comment (PATCH /issues/comments/{id})

Each: arg schema (extra=forbid, GitRef on branch/ref-like fields), Gitea client
method with url-encoded path segments, handler that surfaces auth errors, MCP
registration (write_operation=True), server wiring, docs, and success tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 20:38:25 +02:00
parent c282ffe359
commit 7837ff43ad
7 changed files with 548 additions and 0 deletions
+58
View File
@@ -20,10 +20,16 @@ from aegis_gitea_mcp.tools.read_tools import (
from aegis_gitea_mcp.tools.write_tools import (
add_labels_tool,
assign_issue_tool,
create_branch_tool,
create_issue_comment_tool,
create_issue_tool,
create_label_tool,
create_milestone_tool,
create_pr_comment_tool,
create_pull_request_tool,
create_release_tool,
edit_issue_comment_tool,
edit_release_tool,
remove_labels_tool,
update_issue_tool,
update_label_tool,
@@ -114,6 +120,28 @@ class StubGitea:
async def remove_labels(self, owner, repo, index, labels):
return []
async def create_pull_request(self, owner, repo, *, title, head, base, body=""):
return {"number": 7, "title": title, "state": "open"}
async def create_release(
self, owner, repo, *, tag_name, name="", body="", draft=False, prerelease=False, target=None
):
return {"id": 3, "tag_name": tag_name, "name": name or tag_name}
async def edit_release(
self, owner, repo, release_id, *, name=None, body=None, draft=None, prerelease=None
):
return {"id": release_id, "tag_name": "v1", "name": name or "rel"}
async def create_branch(self, owner, repo, *, new_branch_name, old_branch_name=None):
return {"name": new_branch_name, "commit": {"id": "abc"}}
async def create_milestone(self, owner, repo, *, title, description="", due_on=None):
return {"id": 4, "title": title, "state": "open"}
async def edit_issue_comment(self, owner, repo, comment_id, body):
return {"id": comment_id, "body": body}
class ErrorGitea(StubGitea):
"""Stub that raises backend errors for failure-mode coverage."""
@@ -201,6 +229,36 @@ async def test_extended_read_tools_failure_mode() -> None:
{"owner": "acme", "repo": "app", "issue_number": 1, "labels": ["bug"]},
"removed",
),
(
create_pull_request_tool,
{"owner": "acme", "repo": "app", "title": "PR", "head": "feature", "base": "main"},
"number",
),
(
create_release_tool,
{"owner": "acme", "repo": "app", "tag_name": "v1.0.0"},
"id",
),
(
edit_release_tool,
{"owner": "acme", "repo": "app", "release_id": 3, "name": "x"},
"id",
),
(
create_branch_tool,
{"owner": "acme", "repo": "app", "new_branch_name": "feature/x"},
"name",
),
(
create_milestone_tool,
{"owner": "acme", "repo": "app", "title": "M1"},
"id",
),
(
edit_issue_comment_tool,
{"owner": "acme", "repo": "app", "comment_id": 5, "body": "edited"},
"id",
),
],
)
async def test_write_tools_success(tool, args, expected_key):