mcp-github-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-github-agent
3
+ Version: 0.1.0
4
+ Summary: MCP server giving AI agents controlled GitHub access with policy guard and audit trail
5
+ Author-email: Morgan Fu <maogenfu0121@163.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/FMorgan-111/github-mcp-server
8
+ Project-URL: Repository, https://github.com/FMorgan-111/github-mcp-server
9
+ Project-URL: Issues, https://github.com/FMorgan-111/github-mcp-server/issues
10
+ Keywords: mcp,github,ai-agent,claude-code,codex,policy,audit
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Software Development :: Version Control :: Git
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: fastmcp
24
+ Requires-Dist: httpx
25
+ Requires-Dist: python-dotenv
26
+
27
+ # GitHub MCP Agent Server
28
+
29
+ [![CI](https://github.com/FMorgan-111/github-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/FMorgan-111/github-mcp-server/actions/workflows/ci.yml)
30
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/release/python-3100/)
31
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
32
+
33
+ > MCP server giving AI agents controlled GitHub access with policy guard and audit trail.
34
+
35
+ ---
36
+
37
+ ## Architecture
38
+
39
+ ```
40
+ ┌───────────────┐ MCP stdio ┌───────────────────┐
41
+ │ AI Agent │ ◄──────────────────────► │ GitHub MCP Server │
42
+ │ (Claude, Codex│ JSON-RPC 2.0 │ │
43
+ │ or any MCP │ ┌─┴───────────────┐ │
44
+ │ client) │ │ Policy Guard │ │
45
+ └───────────────┘ ├─────────────────┤ │
46
+ │ Audit Log │ │
47
+ └─────────────────┘ │
48
+ ┌──────────────────────────────────────────────┐ │
49
+ │ github_client.py │ │
50
+ │ (httpx) │ │
51
+ │ GitHub API │ │
52
+ └──────────────────────────────────────────────┘ │
53
+ ┌──────────────────────────────────────────────┐ │
54
+ │ Review Engine (ruff + regex) │ │
55
+ └──────────────────────────────────────────────┘ │
56
+ └───┘
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Features & Tools
62
+
63
+ | Tool | Description | Example Input | Example Output |
64
+ |-------------------|--------------------------------------------|-------------------------------------------------------------------------------|---------------------------------------------|
65
+ | `search_code` | Search GitHub repo code by query | `search_code("def main", "owner/repo")` | List of matching files with paths & URLs |
66
+ | `list_issues` | List open or closed issues | `list_issues("owner/repo", state="open")` | Formatted list of issues |
67
+ | `create_issue` | Create a new issue (policy-guarded) | `create_issue("owner/repo", "Bug: timeout", "Steps to reproduce...")` | Confirmation with issue URL |
68
+ | `get_pr_diff` | Fetch raw diff of a pull request | `get_pr_diff("owner/repo", pr_number=7)` | Unified diff as text |
69
+ | `create_pr` | Create a pull request between branches | `create_pr("owner/repo", "Add feature", "Details...", head="feat", base="main")` | Confirmation with PR URL |
70
+ | `review_pr_diff` | Run local automated code review rules | `review_pr_diff("owner/repo", pr_number=7)` | List of warnings and errors found |
71
+ | `comment_pr_review`| Post review findings as PR comments | `comment_pr_review("owner/repo", pr_number=7)` | Number of comments posted |
72
+
73
+ ---
74
+
75
+ ## Security Model
76
+
77
+ - **Repository Allowlist:** Only repositories explicitly allowed by policy can be accessed.
78
+ - **Branch Protection:** PRs to protected branches are subject to branch protection rules before merging.
79
+ - **Dry-Run Mode:** Execute operations without side effects for safe testing and auditing.
80
+ - **Audit Logging:** All actions logged in JSONL format with precise timestamps to maintain traceability.
81
+ - **Policy Configuration:** Start from `policy.example.json` to configure repository allowlists and protected branches.
82
+
83
+ ---
84
+
85
+ ## FAQ
86
+
87
+ **Why not call the GitHub API directly?**
88
+ Direct API calls lack centralized policy enforcement, audit logging, and uniform tooling support for AI agents. This MCP server adds a controlled, auditable layer for safer automation.
89
+
90
+ **What MCP clients work with this?**
91
+ Agents like Claude Code, OpenAI Codex, and any client supporting the MCP JSON-RPC 2.0 transport can connect to this server.
92
+
93
+ **How is this different from the official GitHub MCP server?**
94
+ This project's key differentiator is policy enforcement. Repository allowlists prevent agents from touching unauthorized repos, branch protection blocks accidental PRs to sensitive branches, dry-run mode supports safe validation, and write actions are audit-logged with timestamps.
95
+
96
+ ---
97
+
98
+ ## Quick Start
99
+
100
+ ### Prerequisites
101
+
102
+ - Python 3.10+
103
+ - GitHub personal access token with `repo` scope
104
+
105
+ ### Install
106
+
107
+ ```bash
108
+ pip install fastmcp httpx python-dotenv
109
+ ```
110
+
111
+ ### Configure and Run
112
+
113
+ ```bash
114
+ git clone https://github.com/FMorgan-111/github-mcp-server.git
115
+ cd github-mcp-server
116
+ cp .env.example .env && nano .env
117
+ python3 -m src.main
118
+ ```
119
+
120
+ ### Connect with Claude Code
121
+
122
+ ```bash
123
+ claude mcp add github-agent -- python3 /path/to/github-mcp-server/src/main.py
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Development
129
+
130
+ ```bash
131
+ pip install -e . --break-system-packages
132
+ python3 -m pytest tests/ -v
133
+ python3 -m src.main
134
+ ```
135
+
136
+ 23 tests, all passing.
137
+
138
+ ---
139
+
140
+ ## Deployment with Docker
141
+
142
+ ```bash
143
+ docker build -t github-mcp-server .
144
+ docker run -e GITHUB_TOKEN=ghp_your_token_here -i github-mcp-server
145
+ ```
146
+
147
+ The `-e GITHUB_TOKEN=...` flag passes the token into the container because the local `.env` file is not copied into the image.
148
+
149
+ ---
150
+
151
+ ## License
152
+
153
+ MIT
@@ -0,0 +1,27 @@
1
+ src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ src/audit.py,sha256=g6A_eFiE8J7JR1sCXuVnIhvFTNX3en2fkEO3u57B1Pw,4072
3
+ src/config.py,sha256=yQNjY8-cwbpsmoHJQM510bSjsHtMzlsdEeHCkaLDiZE,923
4
+ src/diff_parser.py,sha256=_Kov_1Rca3yCG14vmhwlHlcdRv5qjtI1sttGzTNnGRE,1282
5
+ src/github_client.py,sha256=r33uLy0qSiKI9QOUxSu4WKPx6OXL4mWyAG2Iwh9SsjE,4476
6
+ src/main.py,sha256=QF9cGxSUH7HsiQHgdh6SbhRJbDQuB4l-sFRVhwP8NyA,727
7
+ src/policy.py,sha256=p3Nl4FywiEbb9gSbGjlRxW4A6wlLsY8IOhPEIrdQ7Fk,4647
8
+ src/review.py,sha256=I0_gSxNyBiLKG4dvFsqzu_QV-rZHXMNDZm8WJ_UI0PM,4299
9
+ src/review_engine.py,sha256=snsa7MjHwd94rueENuTdGOArHO5cAd8qhX0in4YmY-M,2804
10
+ src/tools.py,sha256=C7vLFL8rMWTkHgg06jvufUlfp854BEy9brCB-AhfrjE,9956
11
+ src/analyzers/base.py,sha256=B82EoBGWBikAVUo9t2vHLA7uQQxNWLGh-kB9zrjlMEA,544
12
+ src/analyzers/ruff.py,sha256=gcP6lFO-agYesDxHoldUnBfbpkFTI96vXWkaqd7tgQs,1498
13
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ tests/test_audit.py,sha256=LuP0d6T6Q1AyhJBqoJc1XK-Zjzex7AG7RFk8-nvvVsc,6612
15
+ tests/test_config.py,sha256=uVFgkmo3IOfnYPziBgdr5fAv5xziTcTIqTd4oulyVz4,1610
16
+ tests/test_diff_parser.py,sha256=JRH2xWL0zQYQOtJalecplWRE44tFTVT6QbCQ2gPlExk,1648
17
+ tests/test_github_client.py,sha256=rEO3RnT9jc0y9ePqji18D0j1kYghYboKJFffAvjJom4,6307
18
+ tests/test_main.py,sha256=1Yugo8k_B8WbuLBm2ovyDojMmGHwQL4rexyqfv5x1Pk,1489
19
+ tests/test_policy.py,sha256=KiyFJjvyV-qSS5dwChRtgC3Ec77Ev9OLbKSHk0LAczg,5142
20
+ tests/test_review_engine.py,sha256=lmpN9Jd3VQ7QI4udr-EH7j0T7DeJafoo-6VQeuQ9xaI,5624
21
+ tests/test_ruff_analyzer.py,sha256=CdDhIRZtjF-py1zGWpwiJRjGrwfrjO51K47DSFBnruM,2451
22
+ tests/test_tools.py,sha256=5dSkd1rPwDsRGRL2kBQ_UVmwveQp4TpL2aN4SFC5ADs,14269
23
+ mcp_github_agent-0.1.0.dist-info/METADATA,sha256=J3iBTSJ2ZPg04wsRsBiTVpF2FLsOf6LHmHv8ioSFIsU,7558
24
+ mcp_github_agent-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
25
+ mcp_github_agent-0.1.0.dist-info/entry_points.txt,sha256=9NAVe9kKlXQM0qPY4-hFkjPzrk8GrrknzGYuBDnKBUg,45
26
+ mcp_github_agent-0.1.0.dist-info/top_level.txt,sha256=KW3xgkz9NLMTcmmzgKvW8RFpCFkRIQ085qzq2diFf68,10
27
+ mcp_github_agent-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ github-mcp = src.main:main
@@ -0,0 +1,2 @@
1
+ src
2
+ tests
src/__init__.py ADDED
File without changes
src/analyzers/base.py ADDED
@@ -0,0 +1,21 @@
1
+ """Analyzer protocol — pluggable code analysis backends."""
2
+ from dataclasses import dataclass, field
3
+ from typing import Protocol
4
+
5
+
6
+ @dataclass
7
+ class Finding:
8
+ severity: str # "error" | "warning" | "info"
9
+ file: str
10
+ line: int
11
+ rule: str # e.g. "F401", "E501"
12
+ message: str
13
+ suggestion: str = ""
14
+ source: str = "" # "ruff", "bandit", "regex"
15
+
16
+
17
+ class Analyzer(Protocol):
18
+ """Any backend that takes file path → list of Findings."""
19
+
20
+ def analyze(self, file_path: str) -> list[Finding]:
21
+ ...
src/analyzers/ruff.py ADDED
@@ -0,0 +1,49 @@
1
+ """Ruff subprocess analyzer — run ruff check as JSON, return Findings."""
2
+ import json
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from .base import Finding
7
+
8
+
9
+ class RuffAnalyzer:
10
+ """Run ruff check on a file, return structured findings."""
11
+
12
+ def __init__(self, ruff_cmd: str = "ruff"):
13
+ self.cmd = ruff_cmd
14
+
15
+ def analyze(self, file_path: str) -> list[Finding]:
16
+ path = Path(file_path)
17
+ if path.suffix != ".py":
18
+ return []
19
+
20
+ try:
21
+ result = subprocess.run(
22
+ [self.cmd, "check", "--output-format", "json", str(path)],
23
+ capture_output=True,
24
+ text=True,
25
+ timeout=30,
26
+ )
27
+ except (FileNotFoundError, subprocess.TimeoutExpired):
28
+ return []
29
+
30
+ if result.returncode not in (0, 1):
31
+ return [] # ruff crash / config error? skip
32
+
33
+ try:
34
+ violations = json.loads(result.stdout or "[]")
35
+ except json.JSONDecodeError:
36
+ return []
37
+
38
+ findings = []
39
+ for v in violations:
40
+ findings.append(Finding(
41
+ severity="error" if v.get("fix") else "warning",
42
+ file=v.get("filename", str(path)),
43
+ line=v.get("location", {}).get("row", 0),
44
+ rule=v.get("code", ""),
45
+ message=v.get("message", ""),
46
+ suggestion=v.get("fix", {}).get("message", ""),
47
+ source="ruff",
48
+ ))
49
+ return findings
src/audit.py ADDED
@@ -0,0 +1,120 @@
1
+ """Audit logger — JSONL write operations for traceability."""
2
+ import json
3
+ import os
4
+ import sys
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from typing import Optional
8
+
9
+
10
+ class AuditLogger:
11
+ """Write structured operation audits as JSONL to a sink."""
12
+
13
+ REDACT_KEYS = {
14
+ "GITHUB_TOKEN", "token", "password", "api_key",
15
+ "authorization", "auth", "secret"
16
+ }
17
+ ALLOWED_DIRS = {"/var/log/github-mcp", "stdout", "stderr"}
18
+
19
+ def __init__(self, sink: str = "stdout"):
20
+ """
21
+ Args:
22
+ sink: "stdout", "stderr", or an absolute path under allowed dirs.
23
+ """
24
+ self.sink = sink
25
+ self._validate_sink()
26
+ self._fobj: Optional[object] = None
27
+
28
+ def _validate_sink(self) -> None:
29
+ if self.sink in ("stdout", "stderr"):
30
+ return
31
+ if not os.path.isabs(self.sink):
32
+ raise ValueError(
33
+ f"Audit file sink must be absolute path, got: {self.sink}"
34
+ )
35
+ # Prevent traversal: resolve symlinks, then check the canonical
36
+ # parent directory is under an allowed tree (if configured).
37
+ # For production, restrict via GITHUB_AUDIT_DIR_ALLOWLIST.
38
+ parent = os.path.dirname(os.path.abspath(self.sink))
39
+ allowlist_str = os.environ.get("GITHUB_AUDIT_DIR_ALLOWLIST", "")
40
+ if allowlist_str:
41
+ allowed = allowlist_str.split(",")
42
+ if not any(
43
+ os.path.commonpath([parent, d]) == d for d in allowed
44
+ ):
45
+ raise ValueError(
46
+ f"Audit sink parent {parent} not in "
47
+ f"GITHUB_AUDIT_DIR_ALLOWLIST ({allowlist_str})"
48
+ )
49
+
50
+ def _get_stream(self):
51
+ if self.sink == "stdout":
52
+ return sys.stdout
53
+ if self.sink == "stderr":
54
+ return sys.stderr
55
+ if self._fobj is None:
56
+ parent = os.path.dirname(self.sink)
57
+ if parent:
58
+ os.makedirs(parent, exist_ok=True)
59
+ self._fobj = open(self.sink, "a", encoding="utf-8")
60
+ return self._fobj
61
+
62
+ def log(
63
+ self,
64
+ tool: str,
65
+ action: str,
66
+ repo: str,
67
+ dry_run: bool = False,
68
+ policy_decision: str = "allow",
69
+ policy_rule: str = "",
70
+ request_body: Optional[dict] = None,
71
+ response: Optional[dict] = None,
72
+ error: Optional[str] = None,
73
+ ) -> None:
74
+ """Write one audit entry."""
75
+ entry = {
76
+ "timestamp": datetime.now(timezone.utc).isoformat(),
77
+ "request_id": uuid.uuid4().hex[:12],
78
+ "tool": tool,
79
+ "action": action,
80
+ "repo": repo,
81
+ "dry_run": dry_run,
82
+ "policy": {
83
+ "decision": policy_decision,
84
+ "matched_rule": policy_rule,
85
+ },
86
+ "request": _redact(request_body) if isinstance(request_body, dict) else None,
87
+ "response": _redact(response) if isinstance(response, dict) else None,
88
+ "error": error,
89
+ }
90
+ try:
91
+ stream = self._get_stream()
92
+ stream.write(json.dumps(entry, ensure_ascii=False, default=str) + "\n")
93
+ stream.flush()
94
+ except Exception as e:
95
+ # Audit must never crash the tool, but log the failure somewhere
96
+ print(f"[audit] write failed: {e}", file=sys.stderr)
97
+
98
+ def close(self) -> None:
99
+ if self._fobj:
100
+ self._fobj.close()
101
+ self._fobj = None
102
+
103
+
104
+ def _redact(obj):
105
+ """Recursively redact sensitive keys from dicts and lists."""
106
+ if isinstance(obj, dict):
107
+ out = {}
108
+ for k, v in obj.items():
109
+ if k.lower() in AuditLogger.REDACT_KEYS:
110
+ out[k] = "***REDACTED***"
111
+ elif isinstance(v, (dict, list)):
112
+ out[k] = _redact(v)
113
+ elif isinstance(v, str) and len(v) > 200:
114
+ out[k] = v[:200] + "..."
115
+ else:
116
+ out[k] = v
117
+ return out
118
+ if isinstance(obj, list):
119
+ return [_redact(item) for item in obj]
120
+ return obj
src/config.py ADDED
@@ -0,0 +1,36 @@
1
+ """GitHub MCP Agent — configuration"""
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+
8
+ def get_github_token() -> str:
9
+ token = os.environ.get("GITHUB_TOKEN", "")
10
+ if not token:
11
+ raise ValueError(
12
+ "GITHUB_TOKEN not set. "
13
+ "Create a .env file with GITHUB_TOKEN=ghp_xxx "
14
+ "or set the environment variable."
15
+ )
16
+ return token
17
+
18
+
19
+ def get_github_api_base() -> str:
20
+ return os.environ.get("GITHUB_API_BASE", "https://api.github.com")
21
+
22
+
23
+ def get_policy_path() -> str:
24
+ return os.environ.get("GITHUB_POLICY_PATH", "policy.json")
25
+
26
+
27
+ def get_policy_required() -> bool:
28
+ return os.environ.get("GITHUB_POLICY_REQUIRED", "").lower() in ("true", "1", "yes")
29
+
30
+
31
+ def get_audit_sink() -> str:
32
+ return os.environ.get("GITHUB_AUDIT_LOG", "stdout")
33
+
34
+
35
+ def get_dry_run_enabled() -> bool:
36
+ return os.environ.get("GITHUB_DRY_RUN", "").lower() in ("true", "1", "yes")
src/diff_parser.py ADDED
@@ -0,0 +1,36 @@
1
+ """Parse unified diff → changed files + line map."""
2
+ import re
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass
7
+ class ChangedFile:
8
+ path: str
9
+ added_lines: set[int] = field(default_factory=set)
10
+ removed_lines: set[int] = field(default_factory=set)
11
+
12
+
13
+ def parse_diff(diff_text: str) -> list[ChangedFile]:
14
+ """Parse unified diff, return changed files with line numbers."""
15
+ files: dict[str, ChangedFile] = {}
16
+ current_file = ""
17
+ new_line = 0
18
+
19
+ for line in diff_text.split("\n"):
20
+ if line.startswith("+++ b/"):
21
+ current_file = line[6:]
22
+ files[current_file] = ChangedFile(path=current_file)
23
+ new_line = 0
24
+ elif line.startswith("@@") and current_file:
25
+ m = re.search(r"\+(\d+)", line)
26
+ if m:
27
+ new_line = int(m.group(1)) - 1
28
+ elif line.startswith("+") and not line.startswith("+++") and current_file:
29
+ new_line += 1
30
+ files[current_file].added_lines.add(new_line)
31
+ elif line.startswith(" ") and current_file:
32
+ new_line += 1
33
+ elif line.startswith("-") and not line.startswith("---") and current_file:
34
+ files[current_file].removed_lines.add(new_line + 1)
35
+
36
+ return [f for f in files.values() if f.added_lines]
src/github_client.py ADDED
@@ -0,0 +1,111 @@
1
+ """GitHub API client wrapper using httpx"""
2
+ import httpx
3
+ from typing import Optional, Dict, Any, List, Union
4
+
5
+
6
+ class GitHubClient:
7
+ def __init__(self, token: str, base_url: str = "https://api.github.com"):
8
+ self.base_url = base_url.rstrip('/')
9
+ self.headers = {
10
+ "Authorization": f"Bearer {token}",
11
+ "Accept": "application/vnd.github+json",
12
+ "X-GitHub-Api-Version": "2022-11-28",
13
+ }
14
+
15
+ def search_code(self, query: str, repo: Optional[str] = None) -> Dict[str, Any]:
16
+ try:
17
+ q = f"repo:{repo} {query}" if repo else query
18
+ with httpx.Client(timeout=20) as client:
19
+ response = client.get(
20
+ f"{self.base_url}/search/code",
21
+ headers=self.headers,
22
+ params={"q": q}
23
+ )
24
+ response.raise_for_status()
25
+ data = response.json()
26
+ return {
27
+ "items": [
28
+ {
29
+ "path": item["path"],
30
+ "repo": item["repository"]["full_name"],
31
+ "url": item["html_url"]
32
+ }
33
+ for item in data.get("items", [])
34
+ ]
35
+ }
36
+ except Exception as e:
37
+ return {"error": f"Search failed: {str(e)}"}
38
+
39
+ def list_issues(self, repo: str, state: str = "open") -> Dict[str, Any]:
40
+ try:
41
+ with httpx.Client(timeout=20) as client:
42
+ response = client.get(
43
+ f"{self.base_url}/repos/{repo}/issues",
44
+ headers=self.headers,
45
+ params={"state": state}
46
+ )
47
+ response.raise_for_status()
48
+ return response.json()
49
+ except Exception as e:
50
+ return {"error": f"List issues failed: {str(e)}"}
51
+
52
+ def create_issue(self, repo: str, title: str, body: str) -> Dict[str, Any]:
53
+ try:
54
+ with httpx.Client(timeout=20) as client:
55
+ response = client.post(
56
+ f"{self.base_url}/repos/{repo}/issues",
57
+ headers=self.headers,
58
+ json={"title": title, "body": body}
59
+ )
60
+ response.raise_for_status()
61
+ return response.json()
62
+ except Exception as e:
63
+ return {"error": f"Create issue failed: {str(e)}"}
64
+
65
+ def get_pr_diff(self, repo: str, pr_number: int) -> Dict[str, Any]:
66
+ try:
67
+ headers = {**self.headers, "Accept": "application/vnd.github.v3.diff"}
68
+ with httpx.Client(timeout=20) as client:
69
+ response = client.get(
70
+ f"{self.base_url}/repos/{repo}/pulls/{pr_number}",
71
+ headers=headers
72
+ )
73
+ response.raise_for_status()
74
+ return {"diff": response.text}
75
+ except Exception as e:
76
+ return {"error": f"Get PR diff failed: {str(e)}"}
77
+
78
+ def create_pr(self, repo: str, title: str, body: str, head: str, base: str) -> Dict[str, Any]:
79
+ try:
80
+ with httpx.Client(timeout=20) as client:
81
+ response = client.post(
82
+ f"{self.base_url}/repos/{repo}/pulls",
83
+ headers=self.headers,
84
+ json={"title": title, "body": body, "head": head, "base": base}
85
+ )
86
+ response.raise_for_status()
87
+ return response.json()
88
+ except Exception as e:
89
+ return {"error": f"Create PR failed: {str(e)}"}
90
+
91
+ def create_review_comment(
92
+ self, repo: str, pr_number: int, body: str,
93
+ commit_id: str = "", path: str = "", line: int = 0
94
+ ) -> Dict[str, Any]:
95
+ """Post a review comment on a PR line."""
96
+ try:
97
+ payload = {"body": body}
98
+ if commit_id and path and line > 0:
99
+ payload["commit_id"] = commit_id
100
+ payload["path"] = path
101
+ payload["line"] = line
102
+ with httpx.Client(timeout=20) as client:
103
+ resp = client.post(
104
+ f"{self.base_url}/repos/{repo}/pulls/{pr_number}/reviews",
105
+ headers=self.headers,
106
+ json=payload,
107
+ )
108
+ resp.raise_for_status()
109
+ return resp.json()
110
+ except Exception as e:
111
+ return {"error": f"Review comment failed: {str(e)}"}
src/main.py ADDED
@@ -0,0 +1,29 @@
1
+ """GitHub MCP Server entry point"""
2
+ import os
3
+ import sys
4
+ import signal
5
+
6
+ # Ensure the project dir is on the path so imports work regardless of cwd
7
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
+
9
+ from src.tools import mcp
10
+
11
+
12
+ def main():
13
+ """Run the MCP server"""
14
+ try:
15
+ # Handle SIGTERM gracefully for container environments
16
+ def signal_handler(signum, frame):
17
+ sys.exit(0)
18
+
19
+ signal.signal(signal.SIGTERM, signal_handler)
20
+ mcp.run(transport="stdio")
21
+ except KeyboardInterrupt:
22
+ sys.exit(0)
23
+ except Exception as e:
24
+ print(f"Error starting MCP server: {e}", file=sys.stderr)
25
+ sys.exit(1)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()