GitPasha MCP Server - Ready for deployment
هذا الالتزام موجود في:
23
.dockerignore
Normal file
23
.dockerignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Environments
|
||||
.env.example
|
||||
.venv
|
||||
.linux_cline
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
||||
# For GitBasha
|
||||
GITPASHA_API_KEY='your_api_key'
|
||||
GITPASHA_BASE_URL=https://app.gitpasha.com/api/v1
|
||||
GITPASHA__USERNAME='your_user_name'
|
||||
|
||||
LOG_LEVEL=DEBUG
|
||||
LOG_FILE=logs/gitpasha.log
|
||||
|
||||
MCP_TRANSPORT=sse
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
12
.gitignore
مباع
Normal file
12
.gitignore
مباع
Normal file
@@ -0,0 +1,12 @@
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
.linux_cline
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt .
|
||||
RUN python -m pip install --upgrade pip setuptools wheel \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create logs directory
|
||||
RUN mkdir -p /app/logs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV MCP_TRANSPORT=sse
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=8000
|
||||
|
||||
# Build metadata
|
||||
ENV BUILD_DATE=2025-11-22
|
||||
ENV VERSION=2.0
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD python -c "import socket; s=socket.socket(); s.settimeout(2); s.connect(('localhost', 8000)); s.close()" || exit 1
|
||||
|
||||
# Run with python directly
|
||||
CMD ["python", "-u", "main.py"]
|
||||
161
README.md
Normal file
161
README.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Git Pasha MCP Server
|
||||
|
||||
Simple MCP server for managing Git Pasha repositories.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
gitpasha-mcp/
|
||||
├── main.py
|
||||
├── requirements.txt
|
||||
├── .env
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
├── helpers/
|
||||
│ ├── __init__.py
|
||||
│ ├── headers.py
|
||||
│ ├── descriptions.py
|
||||
│ ├── error_formatter.py
|
||||
│ ├── logger.py
|
||||
│ └── http_client.py
|
||||
├── api/
|
||||
│ ├── __init__.py
|
||||
│ ├── repos/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── create.py
|
||||
│ │ ├── update.py
|
||||
│ │ └── delete.py
|
||||
│ ├── issues/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── list.py
|
||||
│ │ ├── create.py
|
||||
│ │ └── update.py
|
||||
│ ├── files/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── create_readme.py
|
||||
│ └── pulls/
|
||||
│ ├── __init__.py
|
||||
│ └── open.py
|
||||
└── tools/
|
||||
├── __init__.py
|
||||
├── repo_create.py
|
||||
├── repo_update.py
|
||||
├── repo_delete.py
|
||||
├── issue_create.py
|
||||
├── issue_list.py
|
||||
├── issue_update.py
|
||||
└── pr_open.py
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Create new repositories (with auto-suggested descriptions)
|
||||
- ✅ Delete existing repositories
|
||||
- ✅ Edit repository details
|
||||
- ✅ List all repositories
|
||||
- ✅ Get description suggestions
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Setup API Key
|
||||
|
||||
Copy `.env.example` to `.env` and add your Git Pasha API key:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and add your key:
|
||||
```
|
||||
GITPASHA_API_KEY=your_actual_key_here
|
||||
```
|
||||
|
||||
Get your API key from: https://app.gitpasha.com/settings
|
||||
|
||||
### 3. Run Locally
|
||||
|
||||
```bash
|
||||
python server.py
|
||||
```
|
||||
|
||||
### 4. Add to Claude Desktop
|
||||
|
||||
Edit Claude config file:
|
||||
|
||||
**Mac/Linux:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
|
||||
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
Add this:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gitpasha": {
|
||||
"command": "python",
|
||||
"args": ["C:\\Users\\Yours\\Desktop\\MCP Server\\git basha\\server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### > 📝 **Important Note**
|
||||
Make sure to go to the following path:
|
||||
```bash
|
||||
C:\Users\Alawakey\AppData\Local\AnthropicClaude\app-0.13.64\logs\gitpasha.log
|
||||
```
|
||||
|
||||
- If you find the file `logs\gitpasha.log` ✅ — everything is fine, and the server can run properly.
|
||||
- If the file **doesn’t exist** ❌ — you need to **create it manually** so the server can start correctly.
|
||||
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Create Repository
|
||||
```
|
||||
Create a new repo called "my-awesome-api"
|
||||
```
|
||||
Auto-suggests: "RESTful API service"
|
||||
|
||||
### Edit Repository
|
||||
```
|
||||
Edit repo "old-name" to have description "New description"
|
||||
```
|
||||
|
||||
### Delete Repository
|
||||
```
|
||||
Delete repo "test-repo" with confirmation
|
||||
```
|
||||
|
||||
### List Repositories
|
||||
```
|
||||
Show me all my repositories
|
||||
```
|
||||
|
||||
## Tools Available
|
||||
|
||||
1. **create_repo** - Create new repository
|
||||
2. **delete_repo** - Delete repository (requires confirmation)
|
||||
3. **edit_repo** - Update repository settings
|
||||
4. **list_repos** - List all repositories
|
||||
5. **get_description_suggestion** - Get description idea for repo name
|
||||
|
||||
## Notes
|
||||
|
||||
- All descriptions are auto-suggested in English
|
||||
- Simple and clean code
|
||||
- Works locally and with Claude Desktop
|
||||
- Requires Git Pasha API key
|
||||
|
||||
# create repo --> enter the repo name + description
|
||||
# delete repo --> enter the user name / name repo. EX: (mohamedelawakey/testat)
|
||||
# update repo --> enter the repo like steps of delete the enter the new name if you want then description if also you want, then chose if you want the repo private or no
|
||||
|
||||
# to update the issues enter the (mohamedelawakey/testat) as a repo, issue as a number of these issue, title, and all as you want
|
||||
15
api/__init__.py
Normal file
15
api/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from api.repos import api_create_repo, api_update_repo, api_delete_repo
|
||||
from api.issues import api_list_issues, api_create_issue, api_update_issue
|
||||
from api.files import api_create_readme
|
||||
from api.pulls import api_open_pr
|
||||
|
||||
__all__ = [
|
||||
"api_create_repo",
|
||||
"api_update_repo",
|
||||
"api_delete_repo",
|
||||
"api_list_issues",
|
||||
"api_create_issue",
|
||||
"api_update_issue",
|
||||
"api_create_readme",
|
||||
"api_open_pr",
|
||||
]
|
||||
5
api/files/__init__.py
Normal file
5
api/files/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from api.files.create_readme import api_create_readme
|
||||
|
||||
__all__ = [
|
||||
"api_create_readme"
|
||||
]
|
||||
37
api/files/create_readme.py
Normal file
37
api/files/create_readme.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
import base64
|
||||
from typing import Dict, Any
|
||||
from helpers import get_headers, build_client
|
||||
|
||||
USERNAME = os.getenv("GITPASHA__USERNAME", "")
|
||||
BASE_URL = os.getenv(
|
||||
"GITPASHA_BASE_URL",
|
||||
"https://app.gitpasha.com/api/v1"
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def api_create_readme(repo: str, content: str) -> Dict[str, Any]:
|
||||
if "/" not in repo:
|
||||
if not USERNAME:
|
||||
raise ValueError("GITPASHA__USERNAME not set in .env")
|
||||
repo = f"{USERNAME}/{repo}"
|
||||
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}/contents/README.md"
|
||||
encoded = base64.b64encode(content.encode("utf-8")).decode("utf-8")
|
||||
body = {
|
||||
"message": "Add README",
|
||||
"content": encoded,
|
||||
}
|
||||
res = client.put(
|
||||
url,
|
||||
headers=get_headers(),
|
||||
json=body
|
||||
)
|
||||
|
||||
if res.status_code < 300:
|
||||
return res.json()
|
||||
return {
|
||||
"error": "Failed to create README",
|
||||
"response": res.text,
|
||||
}
|
||||
9
api/issues/__init__.py
Normal file
9
api/issues/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from api.issues.list import api_list_issues
|
||||
from api.issues.create import api_create_issue
|
||||
from api.issues.update import api_update_issue
|
||||
|
||||
__all__ = [
|
||||
"api_list_issues",
|
||||
"api_create_issue",
|
||||
"api_update_issue"
|
||||
]
|
||||
39
api/issues/create.py
Normal file
39
api/issues/create.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
from typing import Dict, Any, Optional, List
|
||||
from helpers import get_headers, build_client
|
||||
|
||||
BASE_URL = os.getenv(
|
||||
"GITPASHA_BASE_URL",
|
||||
"https://app.gitpasha.com/api/v1"
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def api_create_issue(
|
||||
repo: str,
|
||||
title: str,
|
||||
body: str = "",
|
||||
labels: Optional[List[str]] = None,
|
||||
assignees: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
username = os.getenv("GITPASHA__USERNAME", "")
|
||||
|
||||
if "/" not in repo:
|
||||
if not username:
|
||||
raise ValueError("GITPASHA__USERNAME not set in .env")
|
||||
repo = f"{username}/{repo}"
|
||||
|
||||
payload: Dict[str, Any] = {"title": title, "body": body}
|
||||
if labels:
|
||||
payload["labels"] = labels
|
||||
if assignees:
|
||||
payload["assignees"] = assignees
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}/issues"
|
||||
res = client.post(
|
||||
url,
|
||||
headers=get_headers(),
|
||||
json=payload
|
||||
)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
return data
|
||||
38
api/issues/list.py
Normal file
38
api/issues/list.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional
|
||||
from helpers import get_headers, build_client
|
||||
|
||||
|
||||
BASE_URL = os.getenv(
|
||||
"GITPASHA_BASE_URL",
|
||||
"https://app.gitpasha.com/api/v1"
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def api_list_issues(
|
||||
repo: str,
|
||||
state: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
username = os.getenv("GITPASHA__USERNAME", "")
|
||||
|
||||
if "/" not in repo:
|
||||
if not username:
|
||||
raise ValueError("GITPASHA__USERNAME not set in .env")
|
||||
repo = f"{username}/{repo}"
|
||||
|
||||
params = {}
|
||||
if state:
|
||||
params["state"] = state
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}/issues"
|
||||
res = client.get(
|
||||
url,
|
||||
headers=get_headers(),
|
||||
params=params
|
||||
)
|
||||
res.raise_for_status()
|
||||
try:
|
||||
data = res.json()
|
||||
except Exception:
|
||||
data = []
|
||||
return data
|
||||
78
api/issues/update.py
Normal file
78
api/issues/update.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import os
|
||||
from typing import Dict, Any, Optional, List
|
||||
from helpers import get_headers, build_client
|
||||
|
||||
BASE_URL = os.getenv(
|
||||
"GITPASHA_BASE_URL",
|
||||
"https://app.gitpasha.com/api/v1"
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def api_update_issue(
|
||||
repo: str,
|
||||
issue: str,
|
||||
title: Optional[str] = None,
|
||||
body: Optional[str] = None,
|
||||
state: Optional[str] = None,
|
||||
labels: Optional[List[str]] = None,
|
||||
assignees: Optional[List[str]] = None,
|
||||
comment: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
repo = repo.strip()
|
||||
issue = issue.strip()
|
||||
|
||||
username = os.getenv("GITPASHA__USERNAME", "")
|
||||
|
||||
if "/" not in repo:
|
||||
if not username:
|
||||
raise ValueError("GITPASHA__USERNAME not set in .env")
|
||||
repo = f"{username}/{repo}"
|
||||
|
||||
out: Dict[str, Any] = {}
|
||||
with build_client() as client:
|
||||
patch_payload: Dict[str, Any] = {}
|
||||
if title is not None:
|
||||
patch_payload["title"] = title
|
||||
if body is not None:
|
||||
patch_payload["body"] = body
|
||||
if state is not None:
|
||||
patch_payload["state"] = state
|
||||
if labels is not None:
|
||||
patch_payload["labels"] = labels
|
||||
if assignees is not None:
|
||||
patch_payload["assignees"] = assignees
|
||||
|
||||
if patch_payload:
|
||||
url = f"{BASE_URL}/repos/{repo}/issues/{issue}"
|
||||
res = client.patch(
|
||||
url,
|
||||
headers=get_headers(),
|
||||
json=patch_payload
|
||||
)
|
||||
|
||||
if res.status_code == 405:
|
||||
res = client.put(
|
||||
url,
|
||||
headers=get_headers(),
|
||||
json=patch_payload
|
||||
)
|
||||
res.raise_for_status()
|
||||
out["update"] = res.json() if res.text else {"status": "ok"}
|
||||
|
||||
if comment:
|
||||
url_c = f"{BASE_URL}/repos/{repo}/issues/{issue}/comments"
|
||||
res_c = client.post(
|
||||
url_c,
|
||||
headers=get_headers(),
|
||||
json={
|
||||
"body": comment
|
||||
}
|
||||
)
|
||||
res_c.raise_for_status()
|
||||
out["comment"] = res_c.json() if res_c.text else {
|
||||
"status": "commented"
|
||||
}
|
||||
|
||||
return out or {
|
||||
"status": "no-op"
|
||||
}
|
||||
5
api/pulls/__init__.py
Normal file
5
api/pulls/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from api.pulls.open import api_open_pr
|
||||
|
||||
__all__ = [
|
||||
"api_open_pr"
|
||||
]
|
||||
39
api/pulls/open.py
Normal file
39
api/pulls/open.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
from helpers import get_headers, build_client
|
||||
|
||||
BASE_URL = os.getenv(
|
||||
"GITPASHA_BASE_URL",
|
||||
"https://app.gitpasha.com/api/v1"
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def api_open_pr(
|
||||
repo: str,
|
||||
title: str,
|
||||
head: str,
|
||||
base: str,
|
||||
body: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
username = os.getenv("GITPASHA__USERNAME", "")
|
||||
|
||||
if "/" not in repo:
|
||||
if not username:
|
||||
raise ValueError("GITPASHA__USERNAME not set in .env")
|
||||
repo = f"{username}/{repo}"
|
||||
|
||||
payload = {
|
||||
"title": title,
|
||||
"head": head,
|
||||
"base": base,
|
||||
"body": body
|
||||
}
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}/pulls"
|
||||
res = client.post(
|
||||
url,
|
||||
headers=get_headers(),
|
||||
json=payload
|
||||
)
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
9
api/repos/__init__.py
Normal file
9
api/repos/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from api.repos.create import api_create_repo
|
||||
from api.repos.update import api_update_repo
|
||||
from api.repos.delete import api_delete_repo
|
||||
|
||||
__all__ = [
|
||||
"api_create_repo",
|
||||
"api_update_repo",
|
||||
"api_delete_repo"
|
||||
]
|
||||
30
api/repos/create.py
Normal file
30
api/repos/create.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
import httpx
|
||||
from helpers import get_headers, build_client
|
||||
|
||||
BASE_URL = os.getenv(
|
||||
"GITPASHA_BASE_URL",
|
||||
"https://app.gitpasha.com/api/v1"
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def api_create_repo(
|
||||
name: str,
|
||||
description: str,
|
||||
private: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
payload = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"private": private
|
||||
}
|
||||
with build_client() as client:
|
||||
res = client.post(
|
||||
f"{BASE_URL}/user/repos",
|
||||
headers=get_headers(),
|
||||
json=payload,
|
||||
)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
return data
|
||||
33
api/repos/delete.py
Normal file
33
api/repos/delete.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
from helpers import get_headers, build_client
|
||||
|
||||
BASE_URL = os.getenv(
|
||||
"GITPASHA_BASE_URL",
|
||||
"https://app.gitpasha.com/api/v1"
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def api_delete_repo(repo: str) -> Dict[str, Any]:
|
||||
repo = repo.strip()
|
||||
username = os.getenv("GITPASHA__USERNAME", "")
|
||||
|
||||
if "/" not in repo:
|
||||
if not username:
|
||||
raise ValueError("GITPASHA__USERNAME not set in .env")
|
||||
repo = f"{username}/{repo}"
|
||||
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}"
|
||||
res = client.delete(url, headers=get_headers())
|
||||
|
||||
try:
|
||||
response_data = res.json() if res.text else {}
|
||||
except Exception:
|
||||
response_data = {}
|
||||
|
||||
if res.status_code in (200, 202, 204):
|
||||
return {"status": "deleted", **response_data}
|
||||
else:
|
||||
res.raise_for_status()
|
||||
return response_data or {"status": "deleted"}
|
||||
43
api/repos/update.py
Normal file
43
api/repos/update.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
from helpers import get_headers, build_client
|
||||
|
||||
BASE_URL = os.getenv(
|
||||
"GITPASHA_BASE_URL",
|
||||
"https://app.gitpasha.com/api/v1"
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def api_update_repo(
|
||||
repo: str,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
private: Optional[bool] = None
|
||||
) -> Dict[str, Any]:
|
||||
username = os.getenv("GITPASHA__USERNAME", "")
|
||||
|
||||
if "/" not in repo:
|
||||
if not username:
|
||||
raise ValueError("GITPASHA__USERNAME not set in .env")
|
||||
repo = f"{username}/{repo}"
|
||||
|
||||
payload: Dict[str, Any] = {}
|
||||
if name is not None:
|
||||
payload["name"] = name
|
||||
if description is not None:
|
||||
payload["description"] = description
|
||||
if private is not None:
|
||||
payload["private"] = bool(private)
|
||||
if not payload:
|
||||
raise ValueError("No fields to update")
|
||||
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}"
|
||||
res = client.patch(url, headers=get_headers(), json=payload)
|
||||
if res.status_code == 405:
|
||||
res = client.put(url, headers=get_headers(), json=payload)
|
||||
res.raise_for_status()
|
||||
try:
|
||||
return res.json()
|
||||
except Exception:
|
||||
return {"status": "ok"}
|
||||
13
helpers/__init__.py
Normal file
13
helpers/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from helpers.logger import log
|
||||
from helpers.headers import get_headers
|
||||
from helpers.descriptions import suggest_description
|
||||
from helpers.error_formatter import format_error
|
||||
from helpers.http_client import build_client
|
||||
|
||||
__all__ = [
|
||||
"log",
|
||||
"get_headers",
|
||||
"suggest_description",
|
||||
"format_error",
|
||||
"build_client"
|
||||
]
|
||||
3
helpers/descriptions.py
Normal file
3
helpers/descriptions.py
Normal file
@@ -0,0 +1,3 @@
|
||||
def suggest_description(name: str) -> str:
|
||||
name = (name or "").strip()
|
||||
return f"{name or 'test'}: Repository for testing and experiments"
|
||||
15
helpers/error_formatter.py
Normal file
15
helpers/error_formatter.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import httpx
|
||||
|
||||
|
||||
def format_error(prefix: str, e: Exception) -> str:
|
||||
if isinstance(e, httpx.HTTPStatusError):
|
||||
code = e.response.status_code
|
||||
text = e.response.text
|
||||
if code in (301, 302, 307, 308):
|
||||
return f"{prefix}: {code} - Redirect detected. Probably web UI, not API."
|
||||
if "CSRF" in text:
|
||||
return f"{prefix}: {code} - CSRF error. Wrong domain."
|
||||
if code in (401, 403):
|
||||
return f"{prefix}: {code} - Invalid or expired API key."
|
||||
return f"{prefix}: {code} - {text}"
|
||||
return f"{prefix}: {str(e)}"
|
||||
10
helpers/headers.py
Normal file
10
helpers/headers.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import os
|
||||
|
||||
|
||||
def get_headers():
|
||||
api_key = os.getenv("GITPASHA_API_KEY", "")
|
||||
return {
|
||||
"Authorization": f"token {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
33
helpers/http_client.py
Normal file
33
helpers/http_client.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import httpx
|
||||
from helpers.logger import log
|
||||
|
||||
|
||||
def log_request(req: httpx.Request):
|
||||
safe_headers = {
|
||||
k: ("<hidden>" if k.lower() == "authorization" else v)
|
||||
for k, v in req.headers.items()
|
||||
}
|
||||
log.info(f"=> {req.method} {req.url}")
|
||||
log.debug(f"Headers: {safe_headers}")
|
||||
try:
|
||||
log.debug(f"Body: {req.content.decode()[:1000]}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def log_response(res: httpx.Response):
|
||||
log.info(f"<= {res.status_code} {res.request.method} {res.request.url}")
|
||||
try:
|
||||
if res.status_code >= 400:
|
||||
content = res.read()
|
||||
log.debug(f"Response: {content.decode()[:2000]}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def build_client() -> httpx.Client:
|
||||
return httpx.Client(
|
||||
follow_redirects=False,
|
||||
timeout=120.0,
|
||||
event_hooks={"request": [log_request], "response": [log_response]},
|
||||
)
|
||||
16
helpers/logger.py
Normal file
16
helpers/logger.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG").upper()
|
||||
LOG_FILE = os.getenv("LOG_FILE", "logs/gitpasha.log")
|
||||
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, LOG_LEVEL, logging.DEBUG),
|
||||
format="%(asctime)s | %(levelname)s | %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler(LOG_FILE, mode="a", encoding="utf-8"),
|
||||
logging.StreamHandler()
|
||||
],
|
||||
)
|
||||
|
||||
log = logging.getLogger("gitpasha")
|
||||
47
main.py
Normal file
47
main.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from helpers import log
|
||||
from tools import (
|
||||
repo_create_tool,
|
||||
repo_update_tool,
|
||||
repo_delete_tool,
|
||||
issue_list_tool,
|
||||
issue_create_tool,
|
||||
issue_update_tool,
|
||||
pr_open_tool,
|
||||
)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
mcp = FastMCP("Git Pasha Server")
|
||||
|
||||
# Register all tools
|
||||
mcp.tool()(repo_create_tool)
|
||||
mcp.tool()(repo_update_tool)
|
||||
mcp.tool()(repo_delete_tool)
|
||||
mcp.tool()(issue_list_tool)
|
||||
mcp.tool()(issue_create_tool)
|
||||
mcp.tool()(issue_update_tool)
|
||||
mcp.tool()(pr_open_tool)
|
||||
|
||||
if __name__ == "__main__":
|
||||
base_url = os.getenv("GITPASHA_BASE_URL", "https://app.gitpasha.com/api/v1")
|
||||
transport_type = os.getenv("MCP_TRANSPORT", "stdio")
|
||||
|
||||
log.info(f"Starting MCP server | BASE_URL={base_url} | Transport={transport_type}")
|
||||
|
||||
if transport_type == "sse":
|
||||
# Run SSE with explicit host/port binding
|
||||
import uvicorn
|
||||
host = os.getenv("HOST", "0.0.0.0")
|
||||
port = int(os.getenv("PORT", "8000"))
|
||||
|
||||
log.info(f"Starting SSE server on {host}:{port}")
|
||||
|
||||
# Get the SSE app and run with uvicorn
|
||||
app = mcp.sse_app()
|
||||
uvicorn.run(app, host=host, port=port, log_level="info")
|
||||
else:
|
||||
# Run stdio for local development
|
||||
mcp.run(transport="stdio")
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
python-dotenv==1.0.0
|
||||
httpx==0.28.1
|
||||
mcp[cli]==1.14.1
|
||||
requests==2.32.5
|
||||
fastapi==0.101.1
|
||||
uvicorn==0.38.0
|
||||
pydantic==2.12.4
|
||||
starlette>=0.27,<0.28
|
||||
511
server.py
Normal file
511
server.py
Normal file
@@ -0,0 +1,511 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import base64
|
||||
from typing import Optional, Dict, Any, List
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Setup
|
||||
load_dotenv()
|
||||
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG").upper()
|
||||
LOG_FILE = os.getenv("LOG_FILE", "server.log")
|
||||
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, LOG_LEVEL, logging.DEBUG),
|
||||
format="%(asctime)s | %(levelname)s | %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler(LOG_FILE, mode="a", encoding="utf-8"),
|
||||
logging.StreamHandler() # still prints to console
|
||||
],
|
||||
)
|
||||
|
||||
log = logging.getLogger("gitpasha")
|
||||
|
||||
mcp = FastMCP("Git Pasha Server")
|
||||
|
||||
API_KEY = os.getenv("GITPASHA_API_KEY", "")
|
||||
BASE_URL = os.getenv(
|
||||
"GITPASHA_BASE_URL",
|
||||
"https://app.gitpasha.com/api/v1").rstrip("/")
|
||||
|
||||
# "https://app.gitpasha.com/api/v1/repos/search?sort=stars&order=desc&limit=6"
|
||||
|
||||
if not API_KEY:
|
||||
log.warning("GITPASHA_API_KEY not set in .env")
|
||||
|
||||
|
||||
# Helpers
|
||||
def get_headers() -> Dict[str, str]:
|
||||
return {
|
||||
"Authorization": f"Bearer {API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def suggest_description(name: str) -> str:
|
||||
name = (name or "").strip()
|
||||
return f"{name or 'test'}: Repository for testing and experiments"
|
||||
|
||||
|
||||
def log_request(req: httpx.Request):
|
||||
safe_headers = {
|
||||
k: ("<hidden>" if k.lower() == "authorization" else v) for k,
|
||||
v in req.headers.items()
|
||||
}
|
||||
log.info(f"=> {req.method} {req.url}")
|
||||
log.debug(f"Headers: {safe_headers}")
|
||||
try:
|
||||
log.debug(f"Body: {req.content.decode()[:1000]}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def log_response(res: httpx.Response):
|
||||
log.info(f"<= {res.status_code} {res.request.method} {res.request.url}")
|
||||
log.debug(f"Response: {res.text[:2000]}")
|
||||
|
||||
|
||||
def build_client() -> httpx.Client:
|
||||
"""Create a sync HTTP client with logging hooks"""
|
||||
return httpx.Client(
|
||||
follow_redirects=False,
|
||||
timeout=30.0,
|
||||
event_hooks={"request": [log_request], "response": [log_response]},
|
||||
)
|
||||
|
||||
|
||||
def format_error(prefix: str, e: Exception) -> str:
|
||||
"""Format different error types"""
|
||||
if isinstance(e, httpx.HTTPStatusError):
|
||||
code = e.response.status_code
|
||||
text = e.response.text
|
||||
if code in (301, 302, 307, 308):
|
||||
return f"{prefix}: {code} - Redirect detected. Probably web UI, not API."
|
||||
if "CSRF" in text:
|
||||
return f"{prefix}: {code} - CSRF error. Wrong domain."
|
||||
if code in (401, 403):
|
||||
return f"{prefix}: {code} - Invalid or expired API key."
|
||||
return f"{prefix}: {code} - {text}"
|
||||
return f"{prefix}: {str(e)}"
|
||||
|
||||
|
||||
# Core API calls (sync)
|
||||
def api_create_repo(
|
||||
name: str,
|
||||
description: Optional[str] = None,
|
||||
private: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a repository"""
|
||||
payload = {
|
||||
"name": name,
|
||||
"description": description or suggest_description(name),
|
||||
"private": private
|
||||
}
|
||||
with build_client() as client:
|
||||
# Use /user/repos for creating a repository according to Gitea API
|
||||
res = client.post(
|
||||
f"{BASE_URL}/user/repos",
|
||||
headers=get_headers(),
|
||||
json=payload,
|
||||
)
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
|
||||
|
||||
def api_update_repo(
|
||||
repo: str, *, name: Optional[str] = None,
|
||||
description: Optional[str] = None, private: Optional[bool] = None
|
||||
) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {}
|
||||
if name is not None:
|
||||
payload["name"] = name
|
||||
if description is not None:
|
||||
payload["description"] = description
|
||||
if private is not None:
|
||||
payload["private"] = bool(private)
|
||||
if not payload:
|
||||
raise ValueError("No fields to update")
|
||||
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}" # repo = "owner/repo"
|
||||
res = client.patch(url, headers=get_headers(), json=payload)
|
||||
if res.status_code == 405:
|
||||
res = client.put(url, headers=get_headers(), json=payload)
|
||||
res.raise_for_status()
|
||||
return res.json() if res.text else {"status": "ok"}
|
||||
|
||||
|
||||
def api_delete_repo(repo: str) -> Dict[str, Any]:
|
||||
"""Delete repository"""
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}" # repo = "owner/repo"
|
||||
res = client.delete(url, headers=get_headers())
|
||||
if res.status_code in (200, 202, 204):
|
||||
return {"status": "deleted"}
|
||||
res.raise_for_status()
|
||||
return res.json() if res.text else {"status": "deleted"}
|
||||
|
||||
|
||||
def api_create_readme(repo: str, content: str) -> Dict[str, Any]:
|
||||
"""Create a README.md file in the repository."""
|
||||
with build_client() as client:
|
||||
# Use PUT on contents endpoint as per Gitea API
|
||||
url = f"{BASE_URL}/repos/{repo}/contents/README.md"
|
||||
# Encode content in base64 per API spec
|
||||
encoded = base64.b64encode(content.encode("utf-8")).decode("utf-8")
|
||||
body = {
|
||||
"message": "Add README",
|
||||
"content": encoded,
|
||||
}
|
||||
res = client.put(url, headers=get_headers(), json=body)
|
||||
if res.status_code < 300:
|
||||
return res.json()
|
||||
return {
|
||||
"error": "Failed to create README",
|
||||
"response": res.text,
|
||||
}
|
||||
|
||||
|
||||
# Issues
|
||||
def api_list_issues(
|
||||
repo: str,
|
||||
state: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""List issues. state can be 'open', 'closed', or 'all'"""
|
||||
params = {}
|
||||
if state:
|
||||
params["state"] = state
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}/issues" # repo = "owner/repo"
|
||||
res = client.get(url, headers=get_headers(), params=params)
|
||||
res.raise_for_status()
|
||||
try:
|
||||
return res.json()
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def api_create_issue(
|
||||
repo: str,
|
||||
title: str,
|
||||
body: str = "",
|
||||
labels: Optional[List[str]] = None,
|
||||
assignees: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new issue"""
|
||||
payload: Dict[str, Any] = {"title": title, "body": body}
|
||||
if labels:
|
||||
payload["labels"] = labels
|
||||
if assignees:
|
||||
payload["assignees"] = assignees
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}/issues" # repo = "owner/repo"
|
||||
res = client.post(url, headers=get_headers(), json=payload)
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
|
||||
|
||||
def api_update_issue(
|
||||
repo: str,
|
||||
issue: str,
|
||||
title: Optional[str] = None,
|
||||
body: Optional[str] = None,
|
||||
state: Optional[str] = None,
|
||||
labels: Optional[List[str]] = None,
|
||||
assignees: Optional[List[str]] = None,
|
||||
comment: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Update an issue and/or add a comment"""
|
||||
out: Dict[str, Any] = {}
|
||||
with build_client() as client:
|
||||
# Update fields
|
||||
patch_payload: Dict[str, Any] = {}
|
||||
if title is not None:
|
||||
patch_payload["title"] = title
|
||||
if body is not None:
|
||||
patch_payload["body"] = body
|
||||
if state is not None:
|
||||
patch_payload["state"] = state # e.g., "open" or "closed"
|
||||
if labels is not None:
|
||||
patch_payload["labels"] = labels
|
||||
if assignees is not None:
|
||||
patch_payload["assignees"] = assignees
|
||||
|
||||
if patch_payload:
|
||||
url = f"{BASE_URL}/repos/{repo}/issues/{issue}"
|
||||
res = client.patch(url, headers=get_headers(), json=patch_payload)
|
||||
if res.status_code == 405:
|
||||
res = client.put(
|
||||
url,
|
||||
headers=get_headers(),
|
||||
json=patch_payload
|
||||
)
|
||||
res.raise_for_status()
|
||||
out["update"] = res.json() if res.text else {"status": "ok"}
|
||||
|
||||
# Optional comment
|
||||
if comment:
|
||||
url_c = f"{BASE_URL}/repos/{repo}/issues/{issue}/comments"
|
||||
res_c = client.post(
|
||||
url_c,
|
||||
headers=get_headers(),
|
||||
json={
|
||||
"body": comment
|
||||
}
|
||||
)
|
||||
res_c.raise_for_status()
|
||||
out["comment"] = res_c.json() if res_c.text else {
|
||||
"status": "commented"
|
||||
}
|
||||
|
||||
return out or {"status": "no-op"}
|
||||
|
||||
|
||||
# Pull Requests
|
||||
def api_open_pr(
|
||||
repo: str,
|
||||
title: str,
|
||||
head: str,
|
||||
base: str,
|
||||
body: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""Open a pull request from head -> base"""
|
||||
payload = {"title": title, "head": head, "base": base, "body": body}
|
||||
with build_client() as client:
|
||||
url = f"{BASE_URL}/repos/{repo}/pulls" # repo = "owner/repo"
|
||||
res = client.post(url, headers=get_headers(), json=payload)
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
|
||||
|
||||
# MCP Tools
|
||||
@mcp.tool()
|
||||
def create_repo(
|
||||
name: str,
|
||||
description: str = "",
|
||||
private: bool = False,
|
||||
create_readme: bool = True,
|
||||
readme_content: str = "# Test Repository\n\nThis repository is for testing.\n",
|
||||
) -> str:
|
||||
"""Create repo and optional README"""
|
||||
if not API_KEY:
|
||||
return "GITPASHA_API_KEY not set"
|
||||
|
||||
desc = description or suggest_description(name)
|
||||
log.info(f"Creating repo: {name}")
|
||||
try:
|
||||
repo = api_create_repo(name, desc, private)
|
||||
repo_id = repo.get("full_name") or repo.get("name") or name # "owner/repo"
|
||||
repo_url = repo.get("html_url") or repo.get("web_url") or "N/A"
|
||||
readme_status = "skipped"
|
||||
if create_readme:
|
||||
try:
|
||||
r = api_create_readme(str(repo_id), readme_content)
|
||||
readme_status = "created" if "error" not in r else "failed"
|
||||
except Exception as e:
|
||||
readme_status = f"failed: {e}"
|
||||
result = {
|
||||
"status": "success",
|
||||
"name": name,
|
||||
"description": desc,
|
||||
"private": private,
|
||||
"repo_url": repo_url,
|
||||
"readme": readme_status
|
||||
}
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Repo creation failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error")
|
||||
return f"{e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def update_repo(
|
||||
repo: str,
|
||||
name: str = "",
|
||||
description: str = "",
|
||||
set_private: bool = False,
|
||||
private: bool = False,
|
||||
) -> str:
|
||||
"""Update a repository metadata"""
|
||||
try:
|
||||
new_name = name or None
|
||||
new_desc = description or None
|
||||
new_priv = private if set_private else None
|
||||
out = api_update_repo(
|
||||
repo,
|
||||
name=new_name,
|
||||
description=new_desc,
|
||||
private=new_priv
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"repo": repo,
|
||||
"result": out
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Repo update failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while updating repo")
|
||||
return f"{e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def delete_repo(repo: str) -> str:
|
||||
"""Delete a repository"""
|
||||
try:
|
||||
out = api_delete_repo(repo)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"repo": repo,
|
||||
"result": out
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Repo delete failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while deleting repo")
|
||||
return f"{e}"
|
||||
|
||||
|
||||
# Issues tools
|
||||
@mcp.tool()
|
||||
def issues_list(repo: str, state: str = "open") -> str:
|
||||
"""List issues. state: open|closed|all"""
|
||||
try:
|
||||
items = api_list_issues(
|
||||
repo,
|
||||
state if state in ("open", "closed", "all") else None)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"count": len(items),
|
||||
"items": items
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Issues list failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while listing issues")
|
||||
return f"{e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def issue_create(
|
||||
repo: str,
|
||||
title: str,
|
||||
body: str = "",
|
||||
labels_csv: str = "",
|
||||
assignees_csv: str = ""
|
||||
) -> str:
|
||||
"""Create issue. labels_csv and assignees_csv are comma-separated"""
|
||||
try:
|
||||
labels = [
|
||||
x.strip() for x in labels_csv.split(",") if x.strip()
|
||||
] if labels_csv else None
|
||||
assignees = [
|
||||
x.strip() for x in assignees_csv.split(",") if x.strip()
|
||||
] if assignees_csv else None
|
||||
item = api_create_issue(
|
||||
repo,
|
||||
title=title,
|
||||
body=body,
|
||||
labels=labels,
|
||||
assignees=assignees
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"item": item
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Issue create failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while creating issue")
|
||||
return f"{e}"
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def issue_update(
|
||||
repo: str,
|
||||
issue: str,
|
||||
title: str = "",
|
||||
body: str = "",
|
||||
state: str = "",
|
||||
labels_csv: str = "",
|
||||
assignees_csv: str = "",
|
||||
comment: str = "",
|
||||
) -> str:
|
||||
"""Update issue fields and/or add a comment"""
|
||||
try:
|
||||
labels = [
|
||||
x.strip() for x in labels_csv.split(",") if x.strip()
|
||||
] if labels_csv else None
|
||||
assignees = [
|
||||
x.strip() for x in assignees_csv.split(",") if x.strip()
|
||||
] if assignees_csv else None
|
||||
st = state or None
|
||||
# Empty strings -> None (skip)
|
||||
t = title or None
|
||||
b = body or None
|
||||
c = comment or None
|
||||
out = api_update_issue(
|
||||
repo,
|
||||
issue,
|
||||
title=t,
|
||||
body=b,
|
||||
state=st,
|
||||
labels=labels,
|
||||
assignees=assignees,
|
||||
comment=c
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"result": out
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Issue update failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while updating issue")
|
||||
return f"{e}"
|
||||
|
||||
|
||||
# Pull Requests tools
|
||||
@mcp.tool()
|
||||
def pr_open(repo: str,
|
||||
title: str,
|
||||
head: str,
|
||||
base: str,
|
||||
body: str = ""
|
||||
) -> str:
|
||||
"""Open a pull request from head -> base"""
|
||||
try:
|
||||
pr = api_open_pr(repo, title=title, head=head, base=base, body=body)
|
||||
return json.dumps({"status": "success", "pr": pr}, ensure_ascii=False)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("PR open failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while opening PR")
|
||||
return f"{e}"
|
||||
|
||||
|
||||
# Entry Point
|
||||
if __name__ == "__main__":
|
||||
log.info(f"Starting MCP server | BASE_URL={BASE_URL}")
|
||||
mcp.run(transport="stdio")
|
||||
17
tools/__init__.py
Normal file
17
tools/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from tools.repo_create import repo_create_tool
|
||||
from tools.repo_update import repo_update_tool
|
||||
from tools.repo_delete import repo_delete_tool
|
||||
from tools.issue_list import issue_list_tool
|
||||
from tools.issue_create import issue_create_tool
|
||||
from tools.issue_update import issue_update_tool
|
||||
from tools.pr_open import pr_open_tool
|
||||
|
||||
__all__ = [
|
||||
"repo_create_tool",
|
||||
"repo_update_tool",
|
||||
"repo_delete_tool",
|
||||
"issue_list_tool",
|
||||
"issue_create_tool",
|
||||
"issue_update_tool",
|
||||
"pr_open_tool",
|
||||
]
|
||||
39
tools/issue_create.py
Normal file
39
tools/issue_create.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import json
|
||||
from helpers import format_error, log
|
||||
from api import api_create_issue
|
||||
import httpx
|
||||
|
||||
|
||||
def issue_create_tool(
|
||||
repo: str,
|
||||
title: str,
|
||||
body: str = "",
|
||||
labels_csv: str = "",
|
||||
assignees_csv: str = ""
|
||||
) -> str:
|
||||
try:
|
||||
labels = [
|
||||
x.strip() for x in labels_csv.split(",") if x.strip()
|
||||
] if labels_csv else None
|
||||
assignees = [
|
||||
x.strip() for x in assignees_csv.split(",") if x.strip()
|
||||
] if assignees_csv else None
|
||||
item = api_create_issue(
|
||||
repo,
|
||||
title=title,
|
||||
body=body,
|
||||
labels=labels,
|
||||
assignees=assignees
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"item": item
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Issue create failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while creating issue")
|
||||
return f"{e}"
|
||||
25
tools/issue_list.py
Normal file
25
tools/issue_list.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import json
|
||||
from helpers import format_error, log
|
||||
from api import api_list_issues
|
||||
import httpx
|
||||
|
||||
|
||||
def issue_list_tool(repo: str, state: str = "open") -> str:
|
||||
try:
|
||||
items = api_list_issues(
|
||||
repo,
|
||||
state if state in ("open", "closed", "all") else None
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"count": len(items),
|
||||
"items": items
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Issues list failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while listing issues")
|
||||
return f"{e}"
|
||||
49
tools/issue_update.py
Normal file
49
tools/issue_update.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
from helpers import format_error, log
|
||||
from api import api_update_issue
|
||||
import httpx
|
||||
|
||||
|
||||
def issue_update_tool(
|
||||
repo: str,
|
||||
issue: str,
|
||||
title: str = "",
|
||||
body: str = "",
|
||||
state: str = "",
|
||||
labels_csv: str = "",
|
||||
assignees_csv: str = "",
|
||||
comment: str = "",
|
||||
) -> str:
|
||||
try:
|
||||
labels = [
|
||||
x.strip() for x in labels_csv.split(",") if x.strip()
|
||||
] if labels_csv else None
|
||||
assignees = [
|
||||
x.strip() for x in assignees_csv.split(",") if x.strip()
|
||||
] if assignees_csv else None
|
||||
st = state or None
|
||||
t = title or None
|
||||
b = body or None
|
||||
c = comment or None
|
||||
out = api_update_issue(
|
||||
repo,
|
||||
issue,
|
||||
title=t,
|
||||
body=b,
|
||||
state=st,
|
||||
labels=labels,
|
||||
assignees=assignees,
|
||||
comment=c
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"result": out
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Issue update failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while updating issue")
|
||||
return f"{e}"
|
||||
33
tools/pr_open.py
Normal file
33
tools/pr_open.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
from helpers import format_error, log
|
||||
from api import api_open_pr
|
||||
import httpx
|
||||
|
||||
|
||||
def pr_open_tool(
|
||||
repo: str,
|
||||
title: str,
|
||||
head: str,
|
||||
base: str,
|
||||
body: str = ""
|
||||
) -> str:
|
||||
try:
|
||||
pr = api_open_pr(
|
||||
repo,
|
||||
title=title,
|
||||
head=head,
|
||||
base=base,
|
||||
body=body
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"pr": pr
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("PR open failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while opening PR")
|
||||
return f"{e}"
|
||||
47
tools/repo_create.py
Normal file
47
tools/repo_create.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from helpers import log, format_error, suggest_description
|
||||
from api import api_create_repo, api_create_readme
|
||||
import httpx
|
||||
|
||||
|
||||
def repo_create_tool(
|
||||
name: str,
|
||||
description: str = "",
|
||||
private: bool = False,
|
||||
create_readme: bool = True,
|
||||
readme_content: str = "# Test Repository\n\nThis repository is for testing.\n",
|
||||
) -> str:
|
||||
api_key = os.getenv("GITPASHA_API_KEY", "")
|
||||
if not api_key:
|
||||
return "GITPASHA_API_KEY not set"
|
||||
|
||||
desc = description or suggest_description(name)
|
||||
log.info(f"Creating repo: {name}")
|
||||
|
||||
try:
|
||||
repo = api_create_repo(name, desc, private)
|
||||
repo_id = repo.get("full_name") or repo.get("name") or name
|
||||
repo_url = repo.get("html_url") or repo.get("web_url") or "N/A"
|
||||
readme_status = "skipped"
|
||||
if create_readme:
|
||||
try:
|
||||
r = api_create_readme(str(repo_id), readme_content)
|
||||
readme_status = "created" if "error" not in r else "failed"
|
||||
except Exception as e:
|
||||
readme_status = f"failed: {e}"
|
||||
result = {
|
||||
"status": "success",
|
||||
"name": name,
|
||||
"description": desc,
|
||||
"private": private,
|
||||
"repo_url": repo_url,
|
||||
"readme": readme_status
|
||||
}
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Repo creation failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error")
|
||||
return f"{e}"
|
||||
22
tools/repo_delete.py
Normal file
22
tools/repo_delete.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
from helpers import format_error, log
|
||||
from api import api_delete_repo
|
||||
import httpx
|
||||
|
||||
|
||||
def repo_delete_tool(repo: str) -> str:
|
||||
try:
|
||||
out = api_delete_repo(repo)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"repo": repo,
|
||||
"result": out
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Repo delete failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while deleting repo")
|
||||
return f"{e}"
|
||||
36
tools/repo_update.py
Normal file
36
tools/repo_update.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import json
|
||||
from helpers import format_error, log
|
||||
from api import api_update_repo
|
||||
import httpx
|
||||
|
||||
|
||||
def repo_update_tool(
|
||||
repo: str,
|
||||
name: str = "",
|
||||
description: str = "",
|
||||
set_private: bool = False,
|
||||
private: bool = False,
|
||||
) -> str:
|
||||
try:
|
||||
new_name = name or None
|
||||
new_desc = description or None
|
||||
new_priv = private if set_private else None
|
||||
out = api_update_repo(
|
||||
repo,
|
||||
name=new_name,
|
||||
description=new_desc,
|
||||
private=new_priv
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"repo": repo,
|
||||
"result": out
|
||||
},
|
||||
ensure_ascii=False
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
return format_error("Repo update failed", e)
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error while updating repo")
|
||||
return f"{e}"
|
||||
المرجع في مشكلة جديدة
حظر مستخدم