feat: expand MCP server tools for PR and label management, add helper CLI scripts
Closes #7
This commit is contained in:
+266
-1
@@ -17,8 +17,13 @@ import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
# Ensure the project root is on the path so gitea_auth.py can be imported.
|
||||
# Auto-execute using the project's local virtual environment Python
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
venv_python = os.path.join(PROJECT_ROOT, "venv", "bin", "python3")
|
||||
if os.path.exists(venv_python) and sys.executable != venv_python:
|
||||
os.execv(venv_python, [venv_python] + sys.argv)
|
||||
|
||||
# Ensure the project root is on the path so gitea_auth.py can be imported.
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
@@ -128,6 +133,153 @@ def gitea_create_pr(
|
||||
return {"number": data["number"], "url": data["html_url"]}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_list_prs(
|
||||
state: str = "open",
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> list[dict]:
|
||||
"""List pull requests on a Gitea repository.
|
||||
|
||||
Args:
|
||||
state: State filter — 'open', 'closed', or 'all' (default: 'open').
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
List of dicts with 'number', 'title', 'state', 'head', 'base', 'url', 'mergeable'.
|
||||
"""
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
url = f"{repo_api_url(h, o, r)}/pulls?state={state}"
|
||||
prs = api_request("GET", url, auth) or []
|
||||
return [
|
||||
{
|
||||
"number": pr["number"],
|
||||
"title": pr["title"],
|
||||
"state": pr["state"],
|
||||
"head": pr["head"]["ref"],
|
||||
"base": pr["base"]["ref"],
|
||||
"url": pr["html_url"],
|
||||
"mergeable": pr.get("mergeable"),
|
||||
}
|
||||
for pr in prs
|
||||
]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_view_pr(
|
||||
pr_number: int,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Get details of a single pull request.
|
||||
|
||||
Args:
|
||||
pr_number: The pull request index/number.
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
dict with PR details.
|
||||
"""
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
url = f"{repo_api_url(h, o, r)}/pulls/{pr_number}"
|
||||
pr = api_request("GET", url, auth)
|
||||
return {
|
||||
"number": pr["number"],
|
||||
"title": pr["title"],
|
||||
"body": pr.get("body", ""),
|
||||
"state": pr["state"],
|
||||
"head": pr["head"]["ref"],
|
||||
"base": pr["base"]["ref"],
|
||||
"url": pr["html_url"],
|
||||
"mergeable": pr.get("mergeable"),
|
||||
"user": pr.get("user", {}).get("login", ""),
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_merge_pr(
|
||||
pr_number: int,
|
||||
do: str = "merge",
|
||||
title: str | None = None,
|
||||
message: str | None = None,
|
||||
force: bool = False,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Merge a Gitea pull request.
|
||||
|
||||
Args:
|
||||
pr_number: The PR number to merge.
|
||||
do: Merge style — 'merge', 'squash', or 'rebase'.
|
||||
title: Optional merge title.
|
||||
message: Optional merge message.
|
||||
force: Force merge, ignoring status checks.
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
dict with 'success' and 'message'.
|
||||
"""
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
url = f"{repo_api_url(h, o, r)}/pulls/{pr_number}/merge"
|
||||
payload = {
|
||||
"Do": do,
|
||||
"force_merge": force,
|
||||
}
|
||||
if title:
|
||||
payload["MergeTitleField"] = title
|
||||
if message:
|
||||
payload["MergeMessageField"] = message
|
||||
api_request("POST", url, auth, payload)
|
||||
return {"success": True, "message": f"PR #{pr_number} merged via '{do}'."}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_delete_branch(
|
||||
branch: str,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Delete a remote branch from a Gitea repository.
|
||||
|
||||
Args:
|
||||
branch: The remote branch name (e.g. 'feat/branch-name').
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
dict with 'success' and 'message'.
|
||||
"""
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
import urllib.parse
|
||||
encoded_branch = urllib.parse.quote(branch, safe="")
|
||||
url = f"{repo_api_url(h, o, r)}/branches/{encoded_branch}"
|
||||
api_request("DELETE", url, auth)
|
||||
return {"success": True, "message": f"Remote branch '{branch}' deleted."}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_close_issue(
|
||||
issue_number: int,
|
||||
@@ -288,6 +440,119 @@ def gitea_mark_issue(
|
||||
return {"success": True, "message": f"Issue #{issue_number} released."}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_list_labels(
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> list:
|
||||
"""List all available labels in a Gitea repository.
|
||||
|
||||
Args:
|
||||
remote: Known Gitea instance ('dadeschools' or 'prgs').
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
list of labels.
|
||||
"""
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
base = repo_api_url(h, o, r)
|
||||
return api_request("GET", f"{base}/labels?limit=100", auth)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_create_label(
|
||||
name: str,
|
||||
color: str,
|
||||
description: str = "",
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Create a new label on a Gitea repository.
|
||||
|
||||
Args:
|
||||
name: Name of the label (e.g. 'bug', 'epic').
|
||||
color: HTML color code (hex, e.g. 'fbca04' or '#fbca04').
|
||||
description: Description of the label.
|
||||
remote: Known Gitea instance ('dadeschools' or 'prgs').
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
dict containing the created label details.
|
||||
"""
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
base = repo_api_url(h, o, r)
|
||||
|
||||
if color.startswith("#"):
|
||||
color = color[1:]
|
||||
|
||||
payload = {
|
||||
"name": name,
|
||||
"color": color,
|
||||
"description": description,
|
||||
}
|
||||
return api_request("POST", f"{base}/labels", auth, payload)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_set_issue_labels(
|
||||
issue_number: int,
|
||||
labels: list[str],
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> list:
|
||||
"""Replace all labels on a Gitea issue with a new list of label names.
|
||||
|
||||
Args:
|
||||
issue_number: The issue number.
|
||||
labels: List of label names to apply.
|
||||
remote: Known Gitea instance ('dadeschools' or 'prgs').
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
list of all labels currently applied to the issue.
|
||||
"""
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
base = repo_api_url(h, o, r)
|
||||
|
||||
# 1. Fetch existing labels on the repo to resolve names -> IDs
|
||||
existing = api_request("GET", f"{base}/labels?limit=100", auth)
|
||||
name_to_id = {lb["name"]: lb["id"] for lb in existing}
|
||||
|
||||
# 2. Check if any requested labels do not exist, and raise error
|
||||
label_ids = []
|
||||
missing_labels = []
|
||||
for name in labels:
|
||||
if name in name_to_id:
|
||||
label_ids.append(name_to_id[name])
|
||||
else:
|
||||
missing_labels.append(name)
|
||||
|
||||
if missing_labels:
|
||||
raise RuntimeError(
|
||||
f"The following labels do not exist on the repository: {missing_labels}. "
|
||||
"Please create them first using gitea_create_label."
|
||||
)
|
||||
|
||||
# 3. PUT the labels to the issue
|
||||
res = api_request("PUT", f"{base}/issues/{issue_number}/labels", auth, {"labels": label_ids})
|
||||
return res
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_mirror_refs(
|
||||
apply: bool = False,
|
||||
|
||||
Reference in New Issue
Block a user