Initial commit: Gitea MCP server with 62 tools
- Full Gitea REST API coverage for repos, issues, PRs, branches, files, releases, webhooks, orgs, notifications - Multi-instance support via config.json - pyproject.toml with pinned dependencies (mcp[cli], httpx) - config.example.json as credentials template (config.json is gitignored)
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "gitea-platform-mcp",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Full Gitea platform management — repositories, issues, PRs, branches, releases, webhooks, organizations",
|
||||||
|
"author": {
|
||||||
|
"name": "Andrey Limasov"
|
||||||
|
},
|
||||||
|
"keywords": ["gitea", "git", "self-hosted", "issues", "pull-requests", "devops"]
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Credentials — never commit real tokens
|
||||||
|
config.json
|
||||||
|
|
||||||
|
# Python
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Gitea API spec — large auto-generated file, download from your Gitea instance
|
||||||
|
gitea-api.yaml
|
||||||
|
|
||||||
|
# Claude Code local settings
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/settings.json
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"command": "uv",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"--project", "${CLAUDE_PLUGIN_ROOT}",
|
||||||
|
"${CLAUDE_PLUGIN_ROOT}/server/gitea_mcp.py",
|
||||||
|
"--config", "${CLAUDE_PLUGIN_ROOT}/config.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# gitea-platform-mcp
|
||||||
|
|
||||||
|
Full Gitea platform management — 50 tools for repositories, issues, PRs, branches, files, releases, webhooks, and organizations.
|
||||||
|
|
||||||
|
## Setup (one-time)
|
||||||
|
|
||||||
|
### 1. Edit config.json
|
||||||
|
|
||||||
|
Open `config.json` in the plugin folder and fill in your Gitea details:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"instances": {
|
||||||
|
"default": {
|
||||||
|
"url": "https://your-gitea.example.com",
|
||||||
|
"token": "your_personal_access_token"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": "default"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Gitea instances
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"instances": {
|
||||||
|
"home": {
|
||||||
|
"url": "https://gitea.home.example.com",
|
||||||
|
"token": "token_for_home"
|
||||||
|
},
|
||||||
|
"work": {
|
||||||
|
"url": "https://gitea.work.example.com",
|
||||||
|
"token": "token_for_work"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": "home"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in chat: "покажи репозитории на work" — Claude передаст `instance="work"` в инструмент.
|
||||||
|
|
||||||
|
### 2. Create a Gitea token
|
||||||
|
|
||||||
|
Gitea → Settings → Applications → Personal Access Tokens
|
||||||
|
|
||||||
|
Recommended permissions: `repo`, `issue`, `notification`, `user`, `organization`
|
||||||
|
|
||||||
|
### 3. Verify connection
|
||||||
|
|
||||||
|
After installing, ask: "проверь подключение к Gitea" — Claude вызовет `get_current_user` и `get_server_info`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `uv` installed (https://docs.astral.sh/uv/)
|
||||||
|
- Gitea instance accessible from your computer
|
||||||
|
|
||||||
|
## Tools (50)
|
||||||
|
|
||||||
|
| Category | Tools |
|
||||||
|
|----------|-------|
|
||||||
|
| Meta | list_instances, get_server_info, get_current_user |
|
||||||
|
| Repositories | list_my_repos, search_repos, get_repo, create_repo, create_org_repo, update_repo, delete_repo, fork_repo, set_repo_topics |
|
||||||
|
| Branches & Tags | list_branches, get_branch, create_branch, delete_branch, list_tags |
|
||||||
|
| Files | list_directory, get_file, create_file, update_file, delete_file |
|
||||||
|
| Issues | list_issues, get_issue, create_issue, update_issue, close_issue, reopen_issue, list_issue_comments, add_issue_comment, edit_issue_comment, delete_issue_comment |
|
||||||
|
| Labels & Milestones | list_labels, create_label, add_issue_labels, list_milestones, create_milestone |
|
||||||
|
| Pull Requests | list_pull_requests, get_pull_request, create_pull_request, update_pull_request, merge_pull_request, get_pr_diff, list_pr_reviews, create_pr_review |
|
||||||
|
| Releases | list_releases, get_latest_release, create_release, delete_release |
|
||||||
|
| Webhooks | list_repo_webhooks, create_repo_webhook, delete_repo_webhook |
|
||||||
|
| Users & Orgs | get_user, search_users, list_my_orgs, get_org, list_org_repos, list_org_members, list_org_teams |
|
||||||
|
| Commits | list_commits, compare_branches, list_repo_contributors |
|
||||||
|
| Notifications | list_notifications, mark_all_notifications_read |
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"instances": {
|
||||||
|
"default": {
|
||||||
|
"url": "https://your-gitea.example.com",
|
||||||
|
"token": "your_personal_access_token_here"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": "default"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "gitea-platform-mcp"
|
||||||
|
version = "0.2.0"
|
||||||
|
description = "Full Gitea platform management via MCP"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"mcp[cli]>=1.0",
|
||||||
|
"httpx>=0.27",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
@@ -0,0 +1,647 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Gitea MCP Server — full Gitea platform management via REST API.
|
||||||
|
Reads credentials from config.json (passed via --config argument or auto-discovered).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
import httpx
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
# ── Config loading ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def find_config() -> str:
|
||||||
|
"""Find config.json: --config arg > GITEA_CONFIG env > script dir > cwd."""
|
||||||
|
parser = argparse.ArgumentParser(add_help=False)
|
||||||
|
parser.add_argument("--config", default="")
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
if args.config:
|
||||||
|
return args.config
|
||||||
|
if os.environ.get("GITEA_CONFIG"):
|
||||||
|
return os.environ["GITEA_CONFIG"]
|
||||||
|
# Look next to the script
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
candidate = os.path.join(script_dir, "..", "config.json")
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return os.path.normpath(candidate)
|
||||||
|
return "config.json"
|
||||||
|
|
||||||
|
CONFIG_PATH = find_config()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||||
|
_config = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"ERROR: config.json not found at {CONFIG_PATH}", file=sys.stderr)
|
||||||
|
print("Create config.json next to the plugin. See README.md for format.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"ERROR: config.json is invalid JSON: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
INSTANCES: dict = _config.get("instances", {})
|
||||||
|
DEFAULT_INSTANCE: str = _config.get("default", "default")
|
||||||
|
|
||||||
|
if not INSTANCES:
|
||||||
|
print("ERROR: config.json has no 'instances' defined.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# ── HTTP helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_http = httpx.Client(timeout=30)
|
||||||
|
|
||||||
|
def _b(s: str) -> str:
|
||||||
|
"""URL-encode a branch/tag/path segment so slashes don't break the URL."""
|
||||||
|
return quote(s, safe="")
|
||||||
|
|
||||||
|
def _get_instance(instance: str) -> tuple[str, dict]:
|
||||||
|
name = instance or DEFAULT_INSTANCE
|
||||||
|
cfg = INSTANCES.get(name)
|
||||||
|
if not cfg:
|
||||||
|
available = ", ".join(INSTANCES.keys())
|
||||||
|
raise ValueError(f"Instance '{name}' not found in config.json. Available: {available}")
|
||||||
|
url = cfg["url"].rstrip("/")
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"token {cfg['token']}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
return f"{url}/api/v1", headers
|
||||||
|
|
||||||
|
def _req(method: str, api_base: str, headers: dict, path: str, **kwargs) -> Any:
|
||||||
|
url = f"{api_base}{path}"
|
||||||
|
r = _http.request(method, url, headers=headers, **kwargs)
|
||||||
|
if r.status_code == 204:
|
||||||
|
return {"success": True}
|
||||||
|
if not r.is_success:
|
||||||
|
try:
|
||||||
|
detail = r.json()
|
||||||
|
except Exception:
|
||||||
|
detail = r.text
|
||||||
|
raise RuntimeError(f"Gitea API {r.status_code}: {detail}")
|
||||||
|
if not r.text:
|
||||||
|
return {"success": True}
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def _call(method: str, instance: str, path: str, **kwargs) -> Any:
|
||||||
|
api, headers = _get_instance(instance)
|
||||||
|
return _req(method, api, headers, path, **kwargs)
|
||||||
|
|
||||||
|
def _strip(d: dict) -> dict:
|
||||||
|
return {k: v for k, v in d.items() if v is not None}
|
||||||
|
|
||||||
|
def GET(instance, path, params=None): return _call("GET", instance, path, params=params)
|
||||||
|
def POST(instance, path, body=None): return _call("POST", instance, path, json=body)
|
||||||
|
def PATCH(instance, path, body=None): return _call("PATCH", instance, path, json=body)
|
||||||
|
def PUT(instance, path, body=None): return _call("PUT", instance, path, json=body)
|
||||||
|
def DELETE(instance, path, body=None):
|
||||||
|
return _call("DELETE", instance, path, **({} if body is None else {"json": body}))
|
||||||
|
|
||||||
|
# ── MCP server ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
mcp = FastMCP("gitea")
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# META
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_instances() -> Any:
|
||||||
|
"""List all configured Gitea instances and show which one is default."""
|
||||||
|
return {
|
||||||
|
"instances": list(INSTANCES.keys()),
|
||||||
|
"default": DEFAULT_INSTANCE,
|
||||||
|
}
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_server_info(instance: str = "") -> Any:
|
||||||
|
"""Get Gitea server version and info."""
|
||||||
|
return GET(instance, "/version")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_current_user(instance: str = "") -> Any:
|
||||||
|
"""Get information about the authenticated user."""
|
||||||
|
return GET(instance, "/user")
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# REPOSITORIES
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_my_repos(limit: int = 50, page: int = 1, instance: str = "") -> Any:
|
||||||
|
"""List repositories of the authenticated user."""
|
||||||
|
return GET(instance, "/user/repos", {"limit": limit, "page": page})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def search_repos(query: str, limit: int = 20, page: int = 1, instance: str = "") -> Any:
|
||||||
|
"""Search repositories across the Gitea instance."""
|
||||||
|
return GET(instance, "/repos/search", {"q": query, "limit": limit, "page": page})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_repo(owner: str, repo: str, instance: str = "") -> Any:
|
||||||
|
"""Get details of a repository."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_repo(
|
||||||
|
name: str,
|
||||||
|
description: str = "",
|
||||||
|
private: bool = True,
|
||||||
|
auto_init: bool = True,
|
||||||
|
default_branch: str = "main",
|
||||||
|
gitignores: str = "",
|
||||||
|
license: str = "",
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Create a new repository for the authenticated user."""
|
||||||
|
return POST(instance, "/user/repos", _strip({
|
||||||
|
"name": name,
|
||||||
|
"description": description or None,
|
||||||
|
"private": private,
|
||||||
|
"auto_init": auto_init,
|
||||||
|
"default_branch": default_branch,
|
||||||
|
"gitignores": gitignores or None,
|
||||||
|
"license": license or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_org_repo(
|
||||||
|
org: str,
|
||||||
|
name: str,
|
||||||
|
description: str = "",
|
||||||
|
private: bool = True,
|
||||||
|
auto_init: bool = True,
|
||||||
|
default_branch: str = "main",
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Create a new repository in an organization."""
|
||||||
|
return POST(instance, f"/orgs/{org}/repos", _strip({
|
||||||
|
"name": name,
|
||||||
|
"description": description or None,
|
||||||
|
"private": private,
|
||||||
|
"auto_init": auto_init,
|
||||||
|
"default_branch": default_branch,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def update_repo(
|
||||||
|
owner: str, repo: str,
|
||||||
|
description: str = "", private: bool | None = None, website: str = "",
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Update repository settings."""
|
||||||
|
return PATCH(instance, f"/repos/{owner}/{repo}", _strip({
|
||||||
|
"description": description or None,
|
||||||
|
"private": private,
|
||||||
|
"website": website or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def delete_repo(owner: str, repo: str, instance: str = "") -> Any:
|
||||||
|
"""Delete a repository permanently."""
|
||||||
|
return DELETE(instance, f"/repos/{owner}/{repo}")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def fork_repo(owner: str, repo: str, organization: str = "", new_repo_name: str = "", instance: str = "") -> Any:
|
||||||
|
"""Fork a repository."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/forks", _strip({
|
||||||
|
"organization": organization or None,
|
||||||
|
"name": new_repo_name or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def set_repo_topics(owner: str, repo: str, topics: list[str], instance: str = "") -> Any:
|
||||||
|
"""Replace all topics of a repository."""
|
||||||
|
return PUT(instance, f"/repos/{owner}/{repo}/topics", {"topics": topics})
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# BRANCHES & TAGS
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_branches(owner: str, repo: str, limit: int = 50, instance: str = "") -> Any:
|
||||||
|
"""List all branches in a repository."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/branches", {"limit": limit})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_branch(owner: str, repo: str, branch: str, instance: str = "") -> Any:
|
||||||
|
"""Get details of a branch including its latest commit."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/branches/{_b(branch)}")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_branch(owner: str, repo: str, new_branch_name: str, old_branch_name: str = "", instance: str = "") -> Any:
|
||||||
|
"""Create a branch. old_branch_name defaults to the repo's default branch."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/branches", _strip({
|
||||||
|
"new_branch_name": new_branch_name,
|
||||||
|
"old_branch_name": old_branch_name or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def delete_branch(owner: str, repo: str, branch: str, instance: str = "") -> Any:
|
||||||
|
"""Delete a branch."""
|
||||||
|
return DELETE(instance, f"/repos/{owner}/{repo}/branches/{_b(branch)}")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_tags(owner: str, repo: str, limit: int = 20, instance: str = "") -> Any:
|
||||||
|
"""List tags in a repository."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/tags", {"limit": limit})
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# FILES & CONTENTS
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_directory(owner: str, repo: str, path: str = "", ref: str = "", instance: str = "") -> Any:
|
||||||
|
"""List files and directories at a path. ref can be branch/tag/commit."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/contents/{path}", _strip({"ref": ref or None}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_file(owner: str, repo: str, filepath: str, ref: str = "", instance: str = "") -> Any:
|
||||||
|
"""Get file content and metadata. Returns base64-encoded content + SHA."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/contents/{filepath}", _strip({"ref": ref or None}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_file(
|
||||||
|
owner: str, repo: str, filepath: str,
|
||||||
|
content: str, message: str,
|
||||||
|
branch: str = "", instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Create a new text file. content is plain text — encoding handled automatically."""
|
||||||
|
encoded = base64.b64encode(content.encode()).decode()
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/contents/{filepath}", _strip({
|
||||||
|
"content": encoded,
|
||||||
|
"message": message,
|
||||||
|
"branch": branch or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def update_file(
|
||||||
|
owner: str, repo: str, filepath: str,
|
||||||
|
content: str, message: str, sha: str,
|
||||||
|
branch: str = "", instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Update an existing file. sha is the current blob SHA (from get_file). content is plain text."""
|
||||||
|
encoded = base64.b64encode(content.encode()).decode()
|
||||||
|
return PUT(instance, f"/repos/{owner}/{repo}/contents/{filepath}", _strip({
|
||||||
|
"content": encoded,
|
||||||
|
"message": message,
|
||||||
|
"sha": sha,
|
||||||
|
"branch": branch or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def delete_file(
|
||||||
|
owner: str, repo: str, filepath: str,
|
||||||
|
message: str, sha: str,
|
||||||
|
branch: str = "", instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Delete a file. sha is the current blob SHA (from get_file)."""
|
||||||
|
return DELETE(instance, f"/repos/{owner}/{repo}/contents/{filepath}", _strip({
|
||||||
|
"message": message, "sha": sha, "branch": branch or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# ISSUES
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_issues(
|
||||||
|
owner: str, repo: str,
|
||||||
|
state: str = "open", issue_type: str = "issues",
|
||||||
|
labels: str = "", assignee: str = "",
|
||||||
|
limit: int = 20, page: int = 1,
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""List issues or PRs. state: open|closed|all. issue_type: issues|pulls."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/issues", _strip({
|
||||||
|
"state": state, "type": issue_type,
|
||||||
|
"labels": labels or None, "assignee": assignee or None,
|
||||||
|
"limit": limit, "page": page,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_issue(owner: str, repo: str, index: int, instance: str = "") -> Any:
|
||||||
|
"""Get a single issue by number."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/issues/{index}")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_issue(
|
||||||
|
owner: str, repo: str, title: str, body: str = "",
|
||||||
|
assignees: list[str] | None = None, labels: list[int] | None = None, milestone: int = 0,
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Create a new issue."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/issues", _strip({
|
||||||
|
"title": title,
|
||||||
|
"body": body or None,
|
||||||
|
"assignees": assignees or None,
|
||||||
|
"labels": labels or None,
|
||||||
|
"milestone": milestone or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def update_issue(
|
||||||
|
owner: str, repo: str, index: int,
|
||||||
|
title: str = "", body: str = "", state: str = "",
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Update an issue. state: open|closed."""
|
||||||
|
return PATCH(instance, f"/repos/{owner}/{repo}/issues/{index}", _strip({
|
||||||
|
"title": title or None, "body": body or None, "state": state or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def close_issue(owner: str, repo: str, index: int, instance: str = "") -> Any:
|
||||||
|
"""Close an issue."""
|
||||||
|
return PATCH(instance, f"/repos/{owner}/{repo}/issues/{index}", {"state": "closed"})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def reopen_issue(owner: str, repo: str, index: int, instance: str = "") -> Any:
|
||||||
|
"""Reopen a closed issue."""
|
||||||
|
return PATCH(instance, f"/repos/{owner}/{repo}/issues/{index}", {"state": "open"})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_issue_comments(owner: str, repo: str, index: int, instance: str = "") -> Any:
|
||||||
|
"""List comments on an issue."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/issues/{index}/comments")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def add_issue_comment(owner: str, repo: str, index: int, body: str, instance: str = "") -> Any:
|
||||||
|
"""Add a comment to an issue or PR."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/issues/{index}/comments", {"body": body})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def edit_issue_comment(owner: str, repo: str, comment_id: int, body: str, instance: str = "") -> Any:
|
||||||
|
"""Edit an existing comment."""
|
||||||
|
return PATCH(instance, f"/repos/{owner}/{repo}/issues/comments/{comment_id}", {"body": body})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def delete_issue_comment(owner: str, repo: str, comment_id: int, instance: str = "") -> Any:
|
||||||
|
"""Delete a comment."""
|
||||||
|
return DELETE(instance, f"/repos/{owner}/{repo}/issues/comments/{comment_id}")
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# LABELS & MILESTONES
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_labels(owner: str, repo: str, instance: str = "") -> Any:
|
||||||
|
"""List all labels in a repository."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/labels")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_label(owner: str, repo: str, name: str, color: str, description: str = "", instance: str = "") -> Any:
|
||||||
|
"""Create a label. color is hex like #ff0000."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/labels", _strip({
|
||||||
|
"name": name, "color": color, "description": description or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def add_issue_labels(owner: str, repo: str, index: int, label_ids: list[int], instance: str = "") -> Any:
|
||||||
|
"""Add labels to an issue."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/issues/{index}/labels", {"labels": label_ids})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_milestones(owner: str, repo: str, state: str = "open", instance: str = "") -> Any:
|
||||||
|
"""List milestones. state: open|closed|all."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/milestones", {"state": state})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_milestone(owner: str, repo: str, title: str, description: str = "", due_on: str = "", instance: str = "") -> Any:
|
||||||
|
"""Create a milestone. due_on: 2024-12-31T00:00:00Z"""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/milestones", _strip({
|
||||||
|
"title": title, "description": description or None, "due_on": due_on or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# PULL REQUESTS
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_pull_requests(owner: str, repo: str, state: str = "open", limit: int = 20, instance: str = "") -> Any:
|
||||||
|
"""List pull requests. state: open|closed|all."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/pulls", {"state": state, "limit": limit})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_pull_request(owner: str, repo: str, index: int, instance: str = "") -> Any:
|
||||||
|
"""Get a pull request by index."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/pulls/{index}")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_pull_request(
|
||||||
|
owner: str, repo: str, title: str, head: str, base: str,
|
||||||
|
body: str = "", assignees: list[str] | None = None, labels: list[int] | None = None,
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Create a pull request. head = source branch, base = target branch."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/pulls", _strip({
|
||||||
|
"title": title, "head": head, "base": base,
|
||||||
|
"body": body or None,
|
||||||
|
"assignees": assignees or None,
|
||||||
|
"labels": labels or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def update_pull_request(
|
||||||
|
owner: str, repo: str, index: int,
|
||||||
|
title: str = "", body: str = "", state: str = "",
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Update a pull request."""
|
||||||
|
return PATCH(instance, f"/repos/{owner}/{repo}/pulls/{index}", _strip({
|
||||||
|
"title": title or None, "body": body or None, "state": state or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def merge_pull_request(
|
||||||
|
owner: str, repo: str, index: int,
|
||||||
|
merge_style: str = "merge",
|
||||||
|
commit_title: str = "", commit_message: str = "",
|
||||||
|
delete_branch_after_merge: bool = False,
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Merge a pull request. merge_style: merge|rebase|squash|fast-forward-only."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/pulls/{index}/merge", _strip({
|
||||||
|
"Do": merge_style,
|
||||||
|
"MergeTitleField": commit_title or None,
|
||||||
|
"MergeMessageField": commit_message or None,
|
||||||
|
"delete_branch_after_merge": delete_branch_after_merge,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_pr_diff(owner: str, repo: str, index: int, instance: str = "") -> Any:
|
||||||
|
"""Get the diff of a pull request as text."""
|
||||||
|
api, headers = _get_instance(instance)
|
||||||
|
url = f"{api}/repos/{owner}/{repo}/pulls/{index}.diff"
|
||||||
|
r = _http.get(url, headers=headers)
|
||||||
|
if not r.is_success:
|
||||||
|
raise RuntimeError(f"Gitea API {r.status_code}: {r.text}")
|
||||||
|
return {"diff": r.text}
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_pr_reviews(owner: str, repo: str, index: int, instance: str = "") -> Any:
|
||||||
|
"""List reviews on a pull request."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/pulls/{index}/reviews")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_pr_review(
|
||||||
|
owner: str, repo: str, index: int,
|
||||||
|
state: str, body: str = "",
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Submit a PR review. state: APPROVED|COMMENT|REQUEST_CHANGES."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/pulls/{index}/reviews", _strip({
|
||||||
|
"event": state, "body": body or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# RELEASES
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_releases(owner: str, repo: str, limit: int = 10, instance: str = "") -> Any:
|
||||||
|
"""List releases."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/releases", {"limit": limit})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_latest_release(owner: str, repo: str, instance: str = "") -> Any:
|
||||||
|
"""Get the latest release."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/releases/latest")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_release(
|
||||||
|
owner: str, repo: str, tag_name: str, name: str,
|
||||||
|
body: str = "", draft: bool = False, prerelease: bool = False,
|
||||||
|
target_commitish: str = "",
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Create a release."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/releases", _strip({
|
||||||
|
"tag_name": tag_name, "name": name,
|
||||||
|
"body": body or None, "draft": draft, "prerelease": prerelease,
|
||||||
|
"target_commitish": target_commitish or None,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def delete_release(owner: str, repo: str, release_id: int, instance: str = "") -> Any:
|
||||||
|
"""Delete a release."""
|
||||||
|
return DELETE(instance, f"/repos/{owner}/{repo}/releases/{release_id}")
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# WEBHOOKS
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_repo_webhooks(owner: str, repo: str, instance: str = "") -> Any:
|
||||||
|
"""List webhooks for a repository."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/hooks")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_repo_webhook(
|
||||||
|
owner: str, repo: str, url: str, events: list[str],
|
||||||
|
secret: str = "", active: bool = True,
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""Create a webhook. events: push, issues, pull_request, release, etc."""
|
||||||
|
return POST(instance, f"/repos/{owner}/{repo}/hooks", {
|
||||||
|
"type": "gitea",
|
||||||
|
"config": _strip({"url": url, "content_type": "json", "secret": secret or None}),
|
||||||
|
"events": events,
|
||||||
|
"active": active,
|
||||||
|
})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def delete_repo_webhook(owner: str, repo: str, hook_id: int, instance: str = "") -> Any:
|
||||||
|
"""Delete a webhook."""
|
||||||
|
return DELETE(instance, f"/repos/{owner}/{repo}/hooks/{hook_id}")
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# USERS & ORGANIZATIONS
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_user(username: str, instance: str = "") -> Any:
|
||||||
|
"""Get public information about a user."""
|
||||||
|
return GET(instance, f"/users/{username}")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def search_users(query: str, limit: int = 10, instance: str = "") -> Any:
|
||||||
|
"""Search for users."""
|
||||||
|
return GET(instance, "/users/search", {"q": query, "limit": limit})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_my_orgs(instance: str = "") -> Any:
|
||||||
|
"""List organizations the authenticated user belongs to."""
|
||||||
|
return GET(instance, "/user/orgs")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def get_org(org: str, instance: str = "") -> Any:
|
||||||
|
"""Get information about an organization."""
|
||||||
|
return GET(instance, f"/orgs/{org}")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_org_repos(org: str, limit: int = 50, instance: str = "") -> Any:
|
||||||
|
"""List repositories of an organization."""
|
||||||
|
return GET(instance, f"/orgs/{org}/repos", {"limit": limit})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_org_members(org: str, instance: str = "") -> Any:
|
||||||
|
"""List members of an organization."""
|
||||||
|
return GET(instance, f"/orgs/{org}/members")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_org_teams(org: str, instance: str = "") -> Any:
|
||||||
|
"""List teams in an organization."""
|
||||||
|
return GET(instance, f"/orgs/{org}/teams")
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# COMMITS & HISTORY
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_commits(
|
||||||
|
owner: str, repo: str,
|
||||||
|
sha: str = "", path: str = "",
|
||||||
|
limit: int = 20, page: int = 1,
|
||||||
|
instance: str = "",
|
||||||
|
) -> Any:
|
||||||
|
"""List commits. sha can be branch/tag/commit hash."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/commits", _strip({
|
||||||
|
"sha": sha or None, "path": path or None,
|
||||||
|
"limit": limit, "page": page,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def compare_branches(owner: str, repo: str, base: str, head: str, instance: str = "") -> Any:
|
||||||
|
"""Compare two branches, tags, or commits."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/compare/{_b(base)}...{_b(head)}")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_repo_contributors(owner: str, repo: str, limit: int = 20, instance: str = "") -> Any:
|
||||||
|
"""List contributors to a repository."""
|
||||||
|
return GET(instance, f"/repos/{owner}/{repo}/contributors", {"limit": limit})
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# NOTIFICATIONS
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def list_notifications(include_read: bool = False, limit: int = 20, instance: str = "") -> Any:
|
||||||
|
"""List notifications. include_read=True includes already-read notifications."""
|
||||||
|
return GET(instance, "/notifications", _strip({"all": include_read, "limit": limit}))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def mark_all_notifications_read(instance: str = "") -> Any:
|
||||||
|
"""Mark all notifications as read."""
|
||||||
|
return PUT(instance, "/notifications", {})
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcp.run()
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
name: gitea
|
||||||
|
description: >
|
||||||
|
Use this skill when the user asks to work with their Gitea server:
|
||||||
|
"покажи мои репозитории", "создай репозиторий", "открой issue", "закрой issue",
|
||||||
|
"создай pull request", "смерджи PR", "добавь вебхук", "покажи релизы",
|
||||||
|
"создай ветку", "покажи коммиты", "сравни ветки", "список организаций",
|
||||||
|
"переключись на work gitea", "gitea", "list repos", "create issue",
|
||||||
|
"open PR", "merge pull request", "create release", "add webhook",
|
||||||
|
"list branches", "gitea notifications", "check gitea connection".
|
||||||
|
metadata:
|
||||||
|
version: "0.2.0"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Gitea Platform Assistant
|
||||||
|
|
||||||
|
Full Gitea REST API integration. All tools: `mcp__gitea__<tool_name>`.
|
||||||
|
|
||||||
|
## First Use
|
||||||
|
|
||||||
|
Always start by checking the connection and listing available instances:
|
||||||
|
```
|
||||||
|
mcp__gitea__list_instances() → shows configured instances + default
|
||||||
|
mcp__gitea__get_current_user() → verifies auth works
|
||||||
|
```
|
||||||
|
|
||||||
|
If these fail, tell the user to fill in `config.json` in the plugin folder.
|
||||||
|
|
||||||
|
## Instance Selection
|
||||||
|
|
||||||
|
Every tool has an optional `instance` parameter (defaults to `""` = use default from config).
|
||||||
|
|
||||||
|
When user says "на work" / "на home" / "on the work server" → pass `instance="work"` (or relevant name).
|
||||||
|
When no instance is specified → pass `instance=""` (uses config default).
|
||||||
|
|
||||||
|
## Tool Categories
|
||||||
|
|
||||||
|
### Meta & Health
|
||||||
|
- `list_instances()` — show all configured instances
|
||||||
|
- `get_server_info(instance)` — Gitea version
|
||||||
|
- `get_current_user(instance)` — authenticated user
|
||||||
|
|
||||||
|
### Repositories
|
||||||
|
- `list_my_repos(limit, page, instance)`
|
||||||
|
- `search_repos(query, instance)`
|
||||||
|
- `get_repo(owner, repo, instance)`
|
||||||
|
- `create_repo(name, description, private, auto_init, default_branch, instance)`
|
||||||
|
- `create_org_repo(org, name, ..., instance)`
|
||||||
|
- `update_repo(owner, repo, ..., instance)`
|
||||||
|
- `delete_repo(owner, repo, instance)` ⚠️ permanent
|
||||||
|
- `fork_repo(owner, repo, organization, new_repo_name, instance)`
|
||||||
|
- `set_repo_topics(owner, repo, topics, instance)`
|
||||||
|
|
||||||
|
### Branches & Tags
|
||||||
|
- `list_branches(owner, repo, instance)`
|
||||||
|
- `get_branch(owner, repo, branch, instance)`
|
||||||
|
- `create_branch(owner, repo, new_branch_name, old_branch_name, instance)`
|
||||||
|
- `delete_branch(owner, repo, branch, instance)` ⚠️
|
||||||
|
- `list_tags(owner, repo, instance)`
|
||||||
|
|
||||||
|
### Files
|
||||||
|
- `list_directory(owner, repo, path, ref, instance)` — browse repo tree
|
||||||
|
- `get_file(owner, repo, filepath, ref, instance)` — returns content + sha
|
||||||
|
- `create_file(owner, repo, filepath, content, message, branch, instance)` — plain text input, auto-encoded
|
||||||
|
- `update_file(owner, repo, filepath, content, message, sha, branch, instance)` — requires sha from get_file
|
||||||
|
- `delete_file(owner, repo, filepath, message, sha, branch, instance)` — requires sha
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
- `list_issues(owner, repo, state, type, labels, assignee, limit, instance)`
|
||||||
|
- `get_issue(owner, repo, index, instance)`
|
||||||
|
- `create_issue(owner, repo, title, body, assignees, labels, instance)`
|
||||||
|
- `update_issue(owner, repo, index, title, body, state, instance)`
|
||||||
|
- `close_issue / reopen_issue(owner, repo, index, instance)`
|
||||||
|
- `list_issue_comments / add_issue_comment / edit_issue_comment / delete_issue_comment`
|
||||||
|
|
||||||
|
### Labels & Milestones
|
||||||
|
- `list_labels / create_label(name, color #rrggbb) / add_issue_labels`
|
||||||
|
- `list_milestones / create_milestone(title, due_on)`
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
- `list_pull_requests(owner, repo, state, instance)`
|
||||||
|
- `get_pull_request(owner, repo, index, instance)`
|
||||||
|
- `create_pull_request(owner, repo, title, head, base, body, instance)`
|
||||||
|
- `merge_pull_request(owner, repo, index, merge_style, instance)` — merge_style: merge|rebase|squash
|
||||||
|
- `get_pr_diff(owner, repo, index, instance)` — returns diff text
|
||||||
|
- `list_pr_reviews / create_pr_review(state: APPROVED|COMMENT|REQUEST_CHANGES)`
|
||||||
|
|
||||||
|
### Releases
|
||||||
|
- `list_releases / get_latest_release / create_release / delete_release`
|
||||||
|
|
||||||
|
### Webhooks
|
||||||
|
- `list_repo_webhooks / create_repo_webhook(url, events) / delete_repo_webhook`
|
||||||
|
|
||||||
|
### Users & Organizations
|
||||||
|
- `get_current_user / get_user(username) / search_users(query)`
|
||||||
|
- `list_my_orgs / get_org / list_org_repos / list_org_members / list_org_teams`
|
||||||
|
|
||||||
|
### Commits & Activity
|
||||||
|
- `list_commits(owner, repo, sha, path, limit)`
|
||||||
|
- `compare_branches(owner, repo, base, head)` — diff + commit list
|
||||||
|
- `list_repo_contributors`
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- `list_notifications(all) / mark_all_notifications_read`
|
||||||
Reference in New Issue
Block a user