GitPasha MCP Server - Ready for deployment

هذا الالتزام موجود في:
2025-11-22 16:18:29 +02:00
التزام fb1476f28a
35 ملفات معدلة مع 1545 إضافات و0 حذوفات

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
عرض الملف

@@ -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
عرض الملف

@@ -0,0 +1,12 @@
# logs
logs/
*.log
# Environments
.env
.venv
.linux_cline
# Python cache
__pycache__/
*.pyc

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
عرض الملف

@@ -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 **doesnt 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
عرض الملف

@@ -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
عرض الملف

@@ -0,0 +1,5 @@
from api.files.create_readme import api_create_readme
__all__ = [
"api_create_readme"
]

عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -0,0 +1,5 @@
from api.pulls.open import api_open_pr
__all__ = [
"api_open_pr"
]

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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -0,0 +1,3 @@
def suggest_description(name: str) -> str:
name = (name or "").strip()
return f"{name or 'test'}: Repository for testing and experiments"

عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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
عرض الملف

@@ -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}"