- 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.
This commit is contained in:
Executable → Regular
+19
-56
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a Gitea issue.
|
||||
|
||||
Parameterized over title/body and the target Gitea instance, mirroring
|
||||
create_pr.py. Two instances are known out of the box:
|
||||
Parameterized over title/body and the target Gitea instance.
|
||||
Two instances are known out of the box:
|
||||
|
||||
dadeschools -> gitea.dadeschools.net / Contractor / Timesheet
|
||||
prgs -> gitea.prgs.cc / Scaled-Tech-Consulting / Timesheet
|
||||
@@ -11,60 +11,29 @@ Auth is pulled from the macOS keychain via `git credential fill` for the
|
||||
chosen host -- no tokens on the command line.
|
||||
|
||||
Examples:
|
||||
create_issue.py --title "PDF: open after creation" \\
|
||||
--body "Auto-open the generated PDF in the default viewer."
|
||||
|
||||
create_issue.py --remote prgs --title "Fix X" --body-file desc.md
|
||||
create_issue.py --title "Bug: blank PDF" --body "Blank on Safari"
|
||||
create_issue.py --remote prgs --title "Add tests" --body-file desc.md
|
||||
create_issue.py --host gitea.example.com --org Foo --repo Bar --title "..."
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import argparse
|
||||
import subprocess
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
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():
|
||||
# split(maxsplit=1): tokens/passwords can themselves contain '='.
|
||||
if line.startswith("username="):
|
||||
user = line.split("=", 1)[1]
|
||||
elif line.startswith("password="):
|
||||
password = line.split("=", 1)[1]
|
||||
return user, password
|
||||
from auth import (
|
||||
get_credentials, resolve_remote, add_remote_args,
|
||||
api_request, repo_api_url,
|
||||
)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description="Create a Gitea issue.")
|
||||
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.")
|
||||
add_remote_args(parser)
|
||||
parser.add_argument("--title", required=True, help="Issue title.")
|
||||
parser.add_argument("--body", default="", help="Issue body text.")
|
||||
parser.add_argument("--body-file", help="Read issue body from this file ('-' for stdin).")
|
||||
parser.add_argument("--body-file",
|
||||
help="Read issue body from this file ('-' for stdin).")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
profile = REMOTES[args.remote]
|
||||
host = args.host or profile["host"]
|
||||
org = args.org or profile["org"]
|
||||
repo = args.repo or profile["repo"]
|
||||
host, org, repo = resolve_remote(args)
|
||||
|
||||
body = args.body
|
||||
if args.body_file:
|
||||
@@ -81,22 +50,16 @@ def main(argv=None):
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
url = f"https://{host}/api/v1/repos/{org}/{repo}/issues"
|
||||
payload = {"title": args.title, "body": body}
|
||||
req = urllib.request.Request(
|
||||
url, data=json.dumps(payload).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
auth_b64 = base64.b64encode(f"{user}:{password}".encode("utf-8")).decode("utf-8")
|
||||
req.add_header("Authorization", f"Basic {auth_b64}")
|
||||
import base64
|
||||
auth = f"Basic {base64.b64encode(f'{user}:{password}'.encode()).decode()}"
|
||||
url = f"{repo_api_url(host, org, repo)}/issues"
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
data = json.load(response)
|
||||
data = api_request("POST", url, auth, {"title": args.title, "body": body})
|
||||
print(f"Issue #{data.get('number')}: {data.get('html_url')}")
|
||||
return 0
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"Error {e.code}: {e.read().decode()}", file=sys.stderr)
|
||||
except RuntimeError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user