Initial Commit
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import os
|
||||
import httpx
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
GITEA_URL = os.environ.get(
|
||||
"GITEA_URL", "https://git.absdev.net"
|
||||
).rstrip("/")
|
||||
|
||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN")
|
||||
|
||||
mcp = FastMCP(
|
||||
"ABSDEV Gitea",
|
||||
host="127.0.0.1",
|
||||
port=8765,
|
||||
)
|
||||
|
||||
|
||||
async def api_get(path: str, params: dict | None = None):
|
||||
if not GITEA_TOKEN:
|
||||
raise RuntimeError("GITEA_TOKEN is not configured")
|
||||
|
||||
headers = {
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
base_url=GITEA_URL,
|
||||
headers=headers,
|
||||
timeout=30,
|
||||
follow_redirects=False,
|
||||
) as client:
|
||||
response = await client.get(
|
||||
f"/api/v1/{path.lstrip('/')}",
|
||||
params=params,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def validate_repo(owner: str, repo: str):
|
||||
import re
|
||||
|
||||
pattern = r"^[A-Za-z0-9_.-]+$"
|
||||
|
||||
if not re.fullmatch(pattern, owner):
|
||||
raise ValueError("Invalid repository owner")
|
||||
|
||||
if not re.fullmatch(pattern, repo):
|
||||
raise ValueError("Invalid repository name")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def list_repositories():
|
||||
"""List repositories accessible to the configured Gitea account."""
|
||||
return await api_get(
|
||||
"user/repos",
|
||||
{"limit": 100},
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def list_branches(owner: str, repo: str):
|
||||
"""List branches in a Gitea repository."""
|
||||
validate_repo(owner, repo)
|
||||
return await api_get(
|
||||
f"repos/{owner}/{repo}/branches"
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_file(owner: str, repo: str, path: str, ref: str = ""):
|
||||
"""Read a source file from a Gitea repository."""
|
||||
from urllib.parse import quote
|
||||
|
||||
validate_repo(owner, repo)
|
||||
|
||||
if (
|
||||
not path
|
||||
or path.startswith("/")
|
||||
or any(part in ("", ".", "..") for part in path.split("/"))
|
||||
):
|
||||
raise ValueError("Invalid file path")
|
||||
|
||||
encoded_path = quote(path, safe="/")
|
||||
params = {"ref": ref} if ref else None
|
||||
|
||||
result = await api_get(
|
||||
f"repos/{owner}/{repo}/contents/{encoded_path}",
|
||||
params,
|
||||
)
|
||||
|
||||
if isinstance(result, dict) and result.get("encoding") == "base64":
|
||||
import base64
|
||||
|
||||
content = base64.b64decode(
|
||||
result.get("content", ""),
|
||||
validate=False,
|
||||
)
|
||||
|
||||
try:
|
||||
result["decoded_content"] = content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
result["decoded_content"] = "[Binary file]"
|
||||
|
||||
result.pop("content", None)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def list_commits(owner: str, repo: str):
|
||||
"""List recent commits in a repository."""
|
||||
validate_repo(owner, repo)
|
||||
return await api_get(
|
||||
f"repos/{owner}/{repo}/commits",
|
||||
{"limit": 30},
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def list_pull_requests(owner: str, repo: str):
|
||||
"""List pull requests in a repository."""
|
||||
validate_repo(owner, repo)
|
||||
return await api_get(
|
||||
f"repos/{owner}/{repo}/pulls",
|
||||
{"state": "all", "limit": 50},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http")
|
||||
Reference in New Issue
Block a user