b7e195e426
- New: mcp_server.py — FastMCP stdio server exposing 7 tools: gitea_create_issue, gitea_create_pr, gitea_close_issue, gitea_list_issues, gitea_view_issue, gitea_mark_issue, gitea_mirror_refs - New: auth.py — shared authentication and API helpers (get_credentials, get_auth_header, api_request, repo_api_url) - Refactored: create_pr.py, create_issue.py, manage_labels.py to use shared auth module (eliminates credential duplication) - New: tests/test_mcp_server.py — 17 tests for all MCP tools - Updated: tests/test_credentials.py — now tests auth.py directly - Updated: tests/test_create_issue.py — adapted for refactored imports - New: requirements.txt — frozen venv deps (mcp[cli], pytest) - Updated: README.md — MCP server as primary interface - Config: added gitea-tools to mcp_config.json Closes #1. Resolves #2, #5. Relates to #7.
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
"""Shared authentication and API helper for Gitea scripts.
|
|
|
|
Pulls credentials from the macOS keychain via `git credential fill`
|
|
so no tokens appear on the command line.
|
|
"""
|
|
import json
|
|
import base64
|
|
import subprocess
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
# Known Gitea instances — shared by all scripts.
|
|
REMOTES = {
|
|
"dadeschools": {
|
|
"host": "gitea.dadeschools.net",
|
|
"org": "Contractor",
|
|
"repo": "Timesheet",
|
|
},
|
|
"prgs": {
|
|
"host": "gitea.prgs.cc",
|
|
"org": "Scaled-Tech-Consulting",
|
|
"repo": "Timesheet",
|
|
},
|
|
}
|
|
|
|
|
|
def get_credentials(host):
|
|
"""Return (user, password) for *host* via ``git credential fill``."""
|
|
p = subprocess.Popen(
|
|
["git", "credential", "fill"],
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True,
|
|
)
|
|
out, _ = p.communicate(f"protocol=https\nhost={host}\n\n")
|
|
user = password = ""
|
|
for line in out.splitlines():
|
|
if line.startswith("username="):
|
|
user = line.split("=", 1)[1]
|
|
elif line.startswith("password="):
|
|
password = line.split("=", 1)[1]
|
|
return user, password
|
|
|
|
|
|
def get_auth_header(host):
|
|
"""Return an ``Authorization: Basic …`` header value for *host*."""
|
|
user, password = get_credentials(host)
|
|
if not user or not password:
|
|
return None
|
|
token = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
return f"Basic {token}"
|
|
|
|
|
|
def resolve_remote(args):
|
|
"""Given parsed argparse args with --remote/--host/--org/--repo,
|
|
return (host, org, repo) with overrides applied."""
|
|
profile = REMOTES[args.remote]
|
|
host = args.host or profile["host"]
|
|
org = args.org or profile["org"]
|
|
repo = args.repo or profile["repo"]
|
|
return host, org, repo
|
|
|
|
|
|
def add_remote_args(parser):
|
|
"""Add the standard --remote/--host/--org/--repo arguments to a parser."""
|
|
parser.add_argument(
|
|
"--remote", choices=sorted(REMOTES), default="dadeschools",
|
|
help="Known Gitea instance (default: dadeschools).",
|
|
)
|
|
parser.add_argument("--host", help="Override the Gitea host.")
|
|
parser.add_argument("--org", help="Override the owner/org.")
|
|
parser.add_argument("--repo", help="Override the repository.")
|
|
|
|
|
|
def api_request(method, url, auth_header, payload=None):
|
|
"""Make an authenticated JSON request to the Gitea API.
|
|
|
|
Returns parsed JSON on success, raises on HTTP errors.
|
|
"""
|
|
data = json.dumps(payload).encode("utf-8") if payload is not None else None
|
|
req = urllib.request.Request(url, data=data, method=method)
|
|
req.add_header("Authorization", auth_header)
|
|
req.add_header("Content-Type", "application/json")
|
|
try:
|
|
with urllib.request.urlopen(req) as resp:
|
|
body = resp.read().decode("utf-8")
|
|
return json.loads(body) if body else None
|
|
except urllib.error.HTTPError as e:
|
|
error_body = e.read().decode("utf-8", errors="replace")
|
|
raise RuntimeError(f"HTTP {e.code}: {error_body}") from e
|
|
|
|
|
|
def repo_api_url(host, org, repo):
|
|
"""Return the base API URL for a repo: https://host/api/v1/repos/org/repo"""
|
|
return f"https://{host}/api/v1/repos/{org}/{repo}"
|