Initial commit: MCP Hub Community Edition v3.0.0
Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
451
plugins/gitea/client.py
Normal file
451
plugins/gitea/client.py
Normal file
@@ -0,0 +1,451 @@
|
||||
"""
|
||||
Gitea REST API Client
|
||||
|
||||
Handles all HTTP communication with Gitea REST API.
|
||||
Separates API communication from business logic.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
class GiteaClient:
|
||||
"""
|
||||
Gitea REST API client for HTTP communication.
|
||||
|
||||
Handles authentication, request formatting, and error handling
|
||||
for all Gitea API endpoints.
|
||||
"""
|
||||
|
||||
def __init__(self, site_url: str, token: str | None = None, oauth_enabled: bool = False):
|
||||
"""
|
||||
Initialize Gitea API client.
|
||||
|
||||
Args:
|
||||
site_url: Gitea instance URL (e.g., https://gitea.example.com)
|
||||
token: Personal access token for authentication
|
||||
oauth_enabled: Whether OAuth is enabled for this site
|
||||
"""
|
||||
self.site_url = site_url.rstrip("/")
|
||||
self.api_base = f"{self.site_url}/api/v1"
|
||||
self.token = token
|
||||
self.oauth_enabled = oauth_enabled
|
||||
|
||||
# Initialize logger
|
||||
self.logger = logging.getLogger(f"GiteaClient.{site_url}")
|
||||
|
||||
def _get_headers(self, additional_headers: dict | None = None) -> dict[str, str]:
|
||||
"""
|
||||
Get request headers with authentication.
|
||||
|
||||
Args:
|
||||
additional_headers: Additional headers to include
|
||||
|
||||
Returns:
|
||||
Dict: Headers with authentication
|
||||
"""
|
||||
headers = {"Content-Type": "application/json", "accept": "application/json"}
|
||||
|
||||
# Add token authentication if available
|
||||
if self.token:
|
||||
headers["Authorization"] = f"token {self.token}"
|
||||
|
||||
# Merge additional headers
|
||||
if additional_headers:
|
||||
headers.update(additional_headers)
|
||||
|
||||
return headers
|
||||
|
||||
async def request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: dict | None = None,
|
||||
json_data: dict | None = None,
|
||||
headers_override: dict | None = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Make authenticated request to Gitea REST API.
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, PUT, DELETE, PATCH)
|
||||
endpoint: API endpoint (without base URL)
|
||||
params: Query parameters
|
||||
json_data: JSON body data
|
||||
headers_override: Override default headers
|
||||
|
||||
Returns:
|
||||
API response (dict, list, or None)
|
||||
|
||||
Raises:
|
||||
Exception: On API errors with status code and message
|
||||
"""
|
||||
# Build full URL
|
||||
url = f"{self.api_base}/{endpoint.lstrip('/')}"
|
||||
|
||||
# Setup headers
|
||||
headers = self._get_headers(headers_override)
|
||||
|
||||
# Filter out None values from params
|
||||
if params:
|
||||
params = {k: v for k, v in params.items() if v is not None}
|
||||
|
||||
# Filter None values from JSON data
|
||||
if json_data:
|
||||
json_data = {k: v for k, v in json_data.items() if v is not None}
|
||||
|
||||
# Make request
|
||||
self.logger.debug(f"{method} {url}")
|
||||
self.logger.debug(f"Params: {params}")
|
||||
self.logger.debug(f"Data: {json_data}")
|
||||
|
||||
async with (
|
||||
aiohttp.ClientSession() as session,
|
||||
session.request(
|
||||
method=method, url=url, params=params, json=json_data, headers=headers
|
||||
) as response,
|
||||
):
|
||||
# Log response
|
||||
self.logger.debug(f"Response status: {response.status}")
|
||||
|
||||
# Handle empty responses (e.g., 204 No Content)
|
||||
if response.status == 204:
|
||||
return {"success": True, "message": "Operation completed successfully"}
|
||||
|
||||
# Try to parse JSON response
|
||||
try:
|
||||
response_data = await response.json()
|
||||
except Exception:
|
||||
response_text = await response.text()
|
||||
if response.status >= 400:
|
||||
raise Exception(f"Gitea API error (status {response.status}): {response_text}")
|
||||
return {"success": True, "message": response_text}
|
||||
|
||||
# Check for errors
|
||||
if response.status >= 400:
|
||||
error_msg = response_data.get("message", "Unknown error")
|
||||
raise Exception(f"Gitea API error (status {response.status}): {error_msg}")
|
||||
|
||||
return response_data
|
||||
|
||||
# Repository endpoints
|
||||
async def list_repositories(
|
||||
self, owner: str | None = None, page: int = 1, limit: int = 30
|
||||
) -> list[dict]:
|
||||
"""List repositories for a user/org or current user"""
|
||||
if owner:
|
||||
endpoint = f"users/{owner}/repos"
|
||||
else:
|
||||
endpoint = "user/repos"
|
||||
|
||||
params = {"page": page, "limit": limit}
|
||||
return await self.request("GET", endpoint, params=params)
|
||||
|
||||
async def get_repository(self, owner: str, repo: str) -> dict:
|
||||
"""Get repository details"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}")
|
||||
|
||||
async def create_repository(self, data: dict, org: str | None = None) -> dict:
|
||||
"""Create a new repository"""
|
||||
if org:
|
||||
endpoint = f"orgs/{org}/repos"
|
||||
else:
|
||||
endpoint = "user/repos"
|
||||
return await self.request("POST", endpoint, json_data=data)
|
||||
|
||||
async def update_repository(self, owner: str, repo: str, data: dict) -> dict:
|
||||
"""Update repository settings"""
|
||||
return await self.request("PATCH", f"repos/{owner}/{repo}", json_data=data)
|
||||
|
||||
async def delete_repository(self, owner: str, repo: str) -> dict:
|
||||
"""Delete a repository"""
|
||||
return await self.request("DELETE", f"repos/{owner}/{repo}")
|
||||
|
||||
# Branch endpoints
|
||||
async def list_branches(
|
||||
self, owner: str, repo: str, page: int = 1, limit: int = 30
|
||||
) -> list[dict]:
|
||||
"""List repository branches"""
|
||||
params = {"page": page, "limit": limit}
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/branches", params=params)
|
||||
|
||||
async def get_branch(self, owner: str, repo: str, branch: str) -> dict:
|
||||
"""Get branch details"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/branches/{branch}")
|
||||
|
||||
async def create_branch(self, owner: str, repo: str, data: dict) -> dict:
|
||||
"""Create a new branch"""
|
||||
return await self.request("POST", f"repos/{owner}/{repo}/branches", json_data=data)
|
||||
|
||||
async def delete_branch(self, owner: str, repo: str, branch: str) -> dict:
|
||||
"""Delete a branch"""
|
||||
return await self.request("DELETE", f"repos/{owner}/{repo}/branches/{branch}")
|
||||
|
||||
# Tag endpoints
|
||||
async def list_tags(self, owner: str, repo: str, page: int = 1, limit: int = 30) -> list[dict]:
|
||||
"""List repository tags"""
|
||||
params = {"page": page, "limit": limit}
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/tags", params=params)
|
||||
|
||||
async def create_tag(self, owner: str, repo: str, data: dict) -> dict:
|
||||
"""Create a new tag"""
|
||||
return await self.request("POST", f"repos/{owner}/{repo}/tags", json_data=data)
|
||||
|
||||
async def delete_tag(self, owner: str, repo: str, tag: str) -> dict:
|
||||
"""Delete a tag"""
|
||||
return await self.request("DELETE", f"repos/{owner}/{repo}/tags/{tag}")
|
||||
|
||||
# File endpoints
|
||||
async def get_file(self, owner: str, repo: str, filepath: str, ref: str | None = None) -> dict:
|
||||
"""Get file contents"""
|
||||
params = {"ref": ref} if ref else {}
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/contents/{filepath}", params=params)
|
||||
|
||||
async def create_file(self, owner: str, repo: str, filepath: str, data: dict) -> dict:
|
||||
"""Create a file"""
|
||||
# Encode content to base64 if not already encoded
|
||||
if "content" in data:
|
||||
content = data["content"]
|
||||
|
||||
# Check if content is already base64 encoded
|
||||
is_already_base64 = data.get("content_is_base64", False)
|
||||
|
||||
if not is_already_base64:
|
||||
# Plain text content - encode to base64
|
||||
try:
|
||||
data["content"] = base64.b64encode(content.encode()).decode()
|
||||
except (AttributeError, UnicodeDecodeError):
|
||||
# If content is already bytes or has encoding issues, try direct encoding
|
||||
if isinstance(content, bytes):
|
||||
data["content"] = base64.b64encode(content).decode()
|
||||
else:
|
||||
raise ValueError("Content must be a string or bytes")
|
||||
|
||||
# Remove the flag before sending to API
|
||||
data.pop("content_is_base64", None)
|
||||
|
||||
return await self.request(
|
||||
"POST", f"repos/{owner}/{repo}/contents/{filepath}", json_data=data
|
||||
)
|
||||
|
||||
async def update_file(self, owner: str, repo: str, filepath: str, data: dict) -> dict:
|
||||
"""Update a file"""
|
||||
# Encode content to base64 if not already encoded
|
||||
if "content" in data:
|
||||
content = data["content"]
|
||||
|
||||
# Check if content is already base64 encoded
|
||||
is_already_base64 = data.get("content_is_base64", False)
|
||||
|
||||
if not is_already_base64:
|
||||
# Plain text content - encode to base64
|
||||
try:
|
||||
data["content"] = base64.b64encode(content.encode()).decode()
|
||||
except (AttributeError, UnicodeDecodeError):
|
||||
# If content is already bytes or has encoding issues, try direct encoding
|
||||
if isinstance(content, bytes):
|
||||
data["content"] = base64.b64encode(content).decode()
|
||||
else:
|
||||
raise ValueError("Content must be a string or bytes")
|
||||
|
||||
# Remove the flag before sending to API
|
||||
data.pop("content_is_base64", None)
|
||||
|
||||
return await self.request(
|
||||
"PUT", f"repos/{owner}/{repo}/contents/{filepath}", json_data=data
|
||||
)
|
||||
|
||||
async def delete_file(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
filepath: str,
|
||||
sha: str,
|
||||
message: str,
|
||||
branch: str | None = None,
|
||||
) -> dict:
|
||||
"""Delete a file"""
|
||||
data = {"sha": sha, "message": message}
|
||||
if branch:
|
||||
data["branch"] = branch
|
||||
return await self.request(
|
||||
"DELETE", f"repos/{owner}/{repo}/contents/{filepath}", json_data=data
|
||||
)
|
||||
|
||||
# Issue endpoints
|
||||
async def list_issues(self, owner: str, repo: str, params: dict) -> list[dict]:
|
||||
"""List repository issues"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/issues", params=params)
|
||||
|
||||
async def get_issue(self, owner: str, repo: str, index: int) -> dict:
|
||||
"""Get issue details"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/issues/{index}")
|
||||
|
||||
async def create_issue(self, owner: str, repo: str, data: dict) -> dict:
|
||||
"""Create a new issue"""
|
||||
return await self.request("POST", f"repos/{owner}/{repo}/issues", json_data=data)
|
||||
|
||||
async def update_issue(self, owner: str, repo: str, index: int, data: dict) -> dict:
|
||||
"""Update an issue"""
|
||||
return await self.request("PATCH", f"repos/{owner}/{repo}/issues/{index}", json_data=data)
|
||||
|
||||
async def list_issue_comments(self, owner: str, repo: str, index: int) -> list[dict]:
|
||||
"""List issue comments"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/issues/{index}/comments")
|
||||
|
||||
async def create_issue_comment(self, owner: str, repo: str, index: int, data: dict) -> dict:
|
||||
"""Create issue comment"""
|
||||
return await self.request(
|
||||
"POST", f"repos/{owner}/{repo}/issues/{index}/comments", json_data=data
|
||||
)
|
||||
|
||||
# Label endpoints
|
||||
async def list_labels(self, owner: str, repo: str) -> list[dict]:
|
||||
"""List repository labels"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/labels")
|
||||
|
||||
async def create_label(self, owner: str, repo: str, data: dict) -> dict:
|
||||
"""Create a label"""
|
||||
return await self.request("POST", f"repos/{owner}/{repo}/labels", json_data=data)
|
||||
|
||||
async def delete_label(self, owner: str, repo: str, label_id: int) -> dict:
|
||||
"""Delete a label"""
|
||||
return await self.request("DELETE", f"repos/{owner}/{repo}/labels/{label_id}")
|
||||
|
||||
# Milestone endpoints
|
||||
async def list_milestones(self, owner: str, repo: str, state: str | None = None) -> list[dict]:
|
||||
"""List repository milestones"""
|
||||
params = {"state": state} if state else {}
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/milestones", params=params)
|
||||
|
||||
async def create_milestone(self, owner: str, repo: str, data: dict) -> dict:
|
||||
"""Create a milestone"""
|
||||
return await self.request("POST", f"repos/{owner}/{repo}/milestones", json_data=data)
|
||||
|
||||
# Pull Request endpoints
|
||||
async def list_pull_requests(self, owner: str, repo: str, params: dict) -> list[dict]:
|
||||
"""List repository pull requests"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/pulls", params=params)
|
||||
|
||||
async def get_pull_request(self, owner: str, repo: str, index: int) -> dict:
|
||||
"""Get pull request details"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/pulls/{index}")
|
||||
|
||||
async def create_pull_request(self, owner: str, repo: str, data: dict) -> dict:
|
||||
"""Create a new pull request"""
|
||||
return await self.request("POST", f"repos/{owner}/{repo}/pulls", json_data=data)
|
||||
|
||||
async def update_pull_request(self, owner: str, repo: str, index: int, data: dict) -> dict:
|
||||
"""Update a pull request"""
|
||||
return await self.request("PATCH", f"repos/{owner}/{repo}/pulls/{index}", json_data=data)
|
||||
|
||||
async def merge_pull_request(self, owner: str, repo: str, index: int, data: dict) -> dict:
|
||||
"""Merge a pull request"""
|
||||
return await self.request(
|
||||
"POST", f"repos/{owner}/{repo}/pulls/{index}/merge", json_data=data
|
||||
)
|
||||
|
||||
async def list_pr_commits(self, owner: str, repo: str, index: int) -> list[dict]:
|
||||
"""List pull request commits"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/pulls/{index}/commits")
|
||||
|
||||
async def list_pr_files(self, owner: str, repo: str, index: int) -> list[dict]:
|
||||
"""List pull request files"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/pulls/{index}/files")
|
||||
|
||||
async def get_pr_diff(self, owner: str, repo: str, index: int) -> str:
|
||||
"""Get pull request diff"""
|
||||
# Override accept header for diff
|
||||
headers = {"accept": "text/plain"}
|
||||
response = await self.request(
|
||||
"GET", f"repos/{owner}/{repo}/pulls/{index}.diff", headers_override=headers
|
||||
)
|
||||
return response
|
||||
|
||||
async def list_pr_reviews(self, owner: str, repo: str, index: int) -> list[dict]:
|
||||
"""List pull request reviews"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/pulls/{index}/reviews")
|
||||
|
||||
async def create_pr_review(self, owner: str, repo: str, index: int, data: dict) -> dict:
|
||||
"""Create pull request review"""
|
||||
return await self.request(
|
||||
"POST", f"repos/{owner}/{repo}/pulls/{index}/reviews", json_data=data
|
||||
)
|
||||
|
||||
async def request_pr_reviewers(self, owner: str, repo: str, index: int, data: dict) -> dict:
|
||||
"""Request pull request reviewers"""
|
||||
return await self.request(
|
||||
"POST", f"repos/{owner}/{repo}/pulls/{index}/requested_reviewers", json_data=data
|
||||
)
|
||||
|
||||
# User endpoints
|
||||
async def get_user(self, username: str) -> dict:
|
||||
"""Get user information"""
|
||||
return await self.request("GET", f"users/{username}")
|
||||
|
||||
async def list_user_repos(self, username: str, page: int = 1, limit: int = 30) -> list[dict]:
|
||||
"""List user repositories"""
|
||||
params = {"page": page, "limit": limit}
|
||||
return await self.request("GET", f"users/{username}/repos", params=params)
|
||||
|
||||
async def search_users(self, query: str | None = None, uid: int | None = None) -> list[dict]:
|
||||
"""Search users"""
|
||||
params = {}
|
||||
if query:
|
||||
params["q"] = query
|
||||
if uid:
|
||||
params["uid"] = uid
|
||||
response = await self.request("GET", "users/search", params=params)
|
||||
return response.get("data", [])
|
||||
|
||||
# Organization endpoints
|
||||
async def list_organizations(self, page: int = 1, limit: int = 30) -> list[dict]:
|
||||
"""List current user's organizations"""
|
||||
params = {"page": page, "limit": limit}
|
||||
return await self.request("GET", "user/orgs", params=params)
|
||||
|
||||
async def get_organization(self, org: str) -> dict:
|
||||
"""Get organization information"""
|
||||
return await self.request("GET", f"orgs/{org}")
|
||||
|
||||
async def list_org_repos(self, org: str, page: int = 1, limit: int = 30) -> list[dict]:
|
||||
"""List organization repositories"""
|
||||
params = {"page": page, "limit": limit}
|
||||
return await self.request("GET", f"orgs/{org}/repos", params=params)
|
||||
|
||||
async def list_org_teams(self, org: str, page: int = 1, limit: int = 30) -> list[dict]:
|
||||
"""List organization teams"""
|
||||
params = {"page": page, "limit": limit}
|
||||
return await self.request("GET", f"orgs/{org}/teams", params=params)
|
||||
|
||||
async def list_team_members(self, team_id: int, page: int = 1, limit: int = 30) -> list[dict]:
|
||||
"""List team members"""
|
||||
params = {"page": page, "limit": limit}
|
||||
return await self.request("GET", f"teams/{team_id}/members", params=params)
|
||||
|
||||
# Webhook endpoints
|
||||
async def list_webhooks(self, owner: str, repo: str) -> list[dict]:
|
||||
"""List repository webhooks"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/hooks")
|
||||
|
||||
async def create_webhook(self, owner: str, repo: str, data: dict) -> dict:
|
||||
"""Create a webhook"""
|
||||
return await self.request("POST", f"repos/{owner}/{repo}/hooks", json_data=data)
|
||||
|
||||
async def get_webhook(self, owner: str, repo: str, hook_id: int) -> dict:
|
||||
"""Get webhook details"""
|
||||
return await self.request("GET", f"repos/{owner}/{repo}/hooks/{hook_id}")
|
||||
|
||||
async def update_webhook(self, owner: str, repo: str, hook_id: int, data: dict) -> dict:
|
||||
"""Update a webhook"""
|
||||
return await self.request("PATCH", f"repos/{owner}/{repo}/hooks/{hook_id}", json_data=data)
|
||||
|
||||
async def delete_webhook(self, owner: str, repo: str, hook_id: int) -> dict:
|
||||
"""Delete a webhook"""
|
||||
return await self.request("DELETE", f"repos/{owner}/{repo}/hooks/{hook_id}")
|
||||
|
||||
async def test_webhook(self, owner: str, repo: str, hook_id: int) -> dict:
|
||||
"""Test a webhook"""
|
||||
return await self.request("POST", f"repos/{owner}/{repo}/hooks/{hook_id}/tests")
|
||||
Reference in New Issue
Block a user