mcp-github-crunchtools 1.0.1__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.
- mcp_github_crunchtools/__init__.py +58 -0
- mcp_github_crunchtools/__main__.py +6 -0
- mcp_github_crunchtools/client.py +271 -0
- mcp_github_crunchtools/config.py +140 -0
- mcp_github_crunchtools/errors.py +65 -0
- mcp_github_crunchtools/models.py +215 -0
- mcp_github_crunchtools/server.py +496 -0
- mcp_github_crunchtools/tools/__init__.py +48 -0
- mcp_github_crunchtools/tools/actions.py +187 -0
- mcp_github_crunchtools/tools/files.py +124 -0
- mcp_github_crunchtools/tools/issues.py +224 -0
- mcp_github_crunchtools/tools/pull_requests.py +287 -0
- mcp_github_crunchtools/tools/search.py +81 -0
- mcp_github_crunchtools-1.0.1.dist-info/METADATA +263 -0
- mcp_github_crunchtools-1.0.1.dist-info/RECORD +18 -0
- mcp_github_crunchtools-1.0.1.dist-info/WHEEL +4 -0
- mcp_github_crunchtools-1.0.1.dist-info/entry_points.txt +2 -0
- mcp_github_crunchtools-1.0.1.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Pull request tools.
|
|
2
|
+
|
|
3
|
+
Tools for listing, fetching, diffing, and checking GitHub pull requests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..client import get_client
|
|
9
|
+
from ..errors import ValidationError
|
|
10
|
+
from ..models import (
|
|
11
|
+
PR_STATES,
|
|
12
|
+
clamp_per_page,
|
|
13
|
+
resolve_owner,
|
|
14
|
+
validate_name,
|
|
15
|
+
validate_positive_int,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
DIFF_ACCEPT = "application/vnd.github.diff"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def list_pull_requests(
|
|
22
|
+
owner: str | None,
|
|
23
|
+
repo: str,
|
|
24
|
+
state: str = "open",
|
|
25
|
+
per_page: int = 30,
|
|
26
|
+
page: int = 1,
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""List pull requests for a repository.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
owner: Repository owner (defaults to GITHUB_DEFAULT_ORG if unset)
|
|
32
|
+
repo: Repository name
|
|
33
|
+
state: Filter by state (open, closed, all)
|
|
34
|
+
per_page: Results per page (max 100)
|
|
35
|
+
page: Page number
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of pull requests with pagination info
|
|
39
|
+
"""
|
|
40
|
+
owner = resolve_owner(owner)
|
|
41
|
+
repo = validate_name(repo, "repo")
|
|
42
|
+
if state not in PR_STATES:
|
|
43
|
+
allowed = ", ".join(sorted(PR_STATES))
|
|
44
|
+
raise ValidationError(f"Invalid state. Allowed: {allowed}")
|
|
45
|
+
|
|
46
|
+
client = get_client()
|
|
47
|
+
|
|
48
|
+
params: dict[str, Any] = {
|
|
49
|
+
"state": state,
|
|
50
|
+
"per_page": clamp_per_page(per_page),
|
|
51
|
+
"page": validate_positive_int(page, "page"),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return await client.get(f"/repos/{owner}/{repo}/pulls", params=params)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def get_pull_request(
|
|
58
|
+
owner: str | None,
|
|
59
|
+
repo: str,
|
|
60
|
+
pull_number: int,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
"""Get a single pull request by number.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
owner: Repository owner (defaults to GITHUB_DEFAULT_ORG if unset)
|
|
66
|
+
repo: Repository name
|
|
67
|
+
pull_number: Pull request number
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Pull request details including head/base refs and merge status
|
|
71
|
+
"""
|
|
72
|
+
owner = resolve_owner(owner)
|
|
73
|
+
repo = validate_name(repo, "repo")
|
|
74
|
+
pull_number = validate_positive_int(pull_number, "pull_number")
|
|
75
|
+
|
|
76
|
+
client = get_client()
|
|
77
|
+
return await client.get(f"/repos/{owner}/{repo}/pulls/{pull_number}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def get_pull_request_diff(
|
|
81
|
+
owner: str | None,
|
|
82
|
+
repo: str,
|
|
83
|
+
pull_number: int,
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
"""Get the unified diff for a pull request.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
owner: Repository owner (defaults to GITHUB_DEFAULT_ORG if unset)
|
|
89
|
+
repo: Repository name
|
|
90
|
+
pull_number: Pull request number
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Dictionary with the diff text under the "content" key
|
|
94
|
+
"""
|
|
95
|
+
owner = resolve_owner(owner)
|
|
96
|
+
repo = validate_name(repo, "repo")
|
|
97
|
+
pull_number = validate_positive_int(pull_number, "pull_number")
|
|
98
|
+
|
|
99
|
+
client = get_client()
|
|
100
|
+
return await client.get(
|
|
101
|
+
f"/repos/{owner}/{repo}/pulls/{pull_number}",
|
|
102
|
+
accept=DIFF_ACCEPT,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _classify_checks(
|
|
107
|
+
check_runs: dict[str, Any], combined_status: dict[str, Any]
|
|
108
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
109
|
+
"""Sort check-runs and commit-status contexts into verdict buckets.
|
|
110
|
+
|
|
111
|
+
Check-run conclusions failure/cancelled/timed_out/action_required are
|
|
112
|
+
failures; skipped/neutral are skips (NOT failures); success passes. An
|
|
113
|
+
incomplete run, or any other completed conclusion, is pending. Commit
|
|
114
|
+
statuses map failure/error -> failing, pending -> pending, success ->
|
|
115
|
+
passed.
|
|
116
|
+
"""
|
|
117
|
+
buckets: dict[str, list[dict[str, Any]]] = {
|
|
118
|
+
"failing": [],
|
|
119
|
+
"pending": [],
|
|
120
|
+
"skipped": [],
|
|
121
|
+
"passed": [],
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for run in check_runs.get("check_runs", []):
|
|
125
|
+
name = run.get("name")
|
|
126
|
+
status = run.get("status")
|
|
127
|
+
if status != "completed":
|
|
128
|
+
buckets["pending"].append({"name": name, "status": status})
|
|
129
|
+
continue
|
|
130
|
+
match run.get("conclusion"):
|
|
131
|
+
case (
|
|
132
|
+
"failure" | "cancelled" | "timed_out" | "action_required"
|
|
133
|
+
) as conclusion:
|
|
134
|
+
url = run.get("html_url") or run.get("details_url") or ""
|
|
135
|
+
buckets["failing"].append(
|
|
136
|
+
{"name": name, "conclusion": conclusion, "url": url}
|
|
137
|
+
)
|
|
138
|
+
case ("skipped" | "neutral") as conclusion:
|
|
139
|
+
buckets["skipped"].append({"name": name, "conclusion": conclusion})
|
|
140
|
+
case "success":
|
|
141
|
+
buckets["passed"].append({"name": name})
|
|
142
|
+
case _:
|
|
143
|
+
buckets["pending"].append({"name": name, "status": status})
|
|
144
|
+
|
|
145
|
+
for ctx in combined_status.get("statuses", []):
|
|
146
|
+
name = ctx.get("context")
|
|
147
|
+
match ctx.get("state"):
|
|
148
|
+
case ("failure" | "error") as state:
|
|
149
|
+
url = ctx.get("target_url") or ""
|
|
150
|
+
buckets["failing"].append(
|
|
151
|
+
{"name": name, "conclusion": state, "url": url}
|
|
152
|
+
)
|
|
153
|
+
case "pending":
|
|
154
|
+
buckets["pending"].append({"name": name, "status": "pending"})
|
|
155
|
+
case "success":
|
|
156
|
+
buckets["passed"].append({"name": name})
|
|
157
|
+
|
|
158
|
+
return buckets
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def get_pull_request_checks(
|
|
162
|
+
owner: str | None,
|
|
163
|
+
repo: str,
|
|
164
|
+
pull_number: int,
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
"""Get a CI verdict for a pull request that distinguishes skip from fail.
|
|
167
|
+
|
|
168
|
+
Resolves the PR head SHA, then classifies every check-run (GitHub Checks
|
|
169
|
+
API) and legacy commit-status context into exactly one bucket: passed,
|
|
170
|
+
failing, pending, or skipped.
|
|
171
|
+
|
|
172
|
+
IMPORTANT: SKIPPED checks (conclusion "skipped" or "neutral") are NOT
|
|
173
|
+
failures and do not block merging. The legacy combined commit status can
|
|
174
|
+
report an overall state of "failure" when checks are merely skipped, which
|
|
175
|
+
is why this tool classifies each check explicitly instead of trusting that
|
|
176
|
+
aggregate. Use the ``ready_to_merge`` boolean as the signal for whether the
|
|
177
|
+
PR is clear to merge: it is True only when there are no failing and no
|
|
178
|
+
pending checks and the PR is not known-unmergeable.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
owner: Repository owner (defaults to GITHUB_DEFAULT_ORG if unset)
|
|
182
|
+
repo: Repository name
|
|
183
|
+
pull_number: Pull request number
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
A verdict with the head SHA, mergeability, per-bucket check lists, a
|
|
187
|
+
summary count, and the ``ready_to_merge`` signal
|
|
188
|
+
"""
|
|
189
|
+
owner = resolve_owner(owner)
|
|
190
|
+
repo = validate_name(repo, "repo")
|
|
191
|
+
pull_number = validate_positive_int(pull_number, "pull_number")
|
|
192
|
+
|
|
193
|
+
client = get_client()
|
|
194
|
+
|
|
195
|
+
pr = await client.get(f"/repos/{owner}/{repo}/pulls/{pull_number}")
|
|
196
|
+
head = pr.get("head") or {}
|
|
197
|
+
sha = head.get("sha")
|
|
198
|
+
if not sha:
|
|
199
|
+
raise ValidationError("Could not resolve pull request head SHA")
|
|
200
|
+
|
|
201
|
+
check_runs = await client.get(
|
|
202
|
+
f"/repos/{owner}/{repo}/commits/{sha}/check-runs"
|
|
203
|
+
)
|
|
204
|
+
combined_status = await client.get(
|
|
205
|
+
f"/repos/{owner}/{repo}/commits/{sha}/status"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
buckets = _classify_checks(check_runs, combined_status)
|
|
209
|
+
failing = buckets["failing"]
|
|
210
|
+
pending = buckets["pending"]
|
|
211
|
+
skipped = buckets["skipped"]
|
|
212
|
+
passed = buckets["passed"]
|
|
213
|
+
|
|
214
|
+
mergeable = pr.get("mergeable")
|
|
215
|
+
ready_to_merge = (
|
|
216
|
+
not failing and not pending and mergeable is not False
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"pull_number": pull_number,
|
|
221
|
+
"head_sha": sha,
|
|
222
|
+
"mergeable": mergeable,
|
|
223
|
+
"mergeable_state": pr.get("mergeable_state", ""),
|
|
224
|
+
"ready_to_merge": ready_to_merge,
|
|
225
|
+
"summary": {
|
|
226
|
+
"passed": len(passed),
|
|
227
|
+
"failing": len(failing),
|
|
228
|
+
"pending": len(pending),
|
|
229
|
+
"skipped": len(skipped),
|
|
230
|
+
},
|
|
231
|
+
"failing": failing,
|
|
232
|
+
"pending": pending,
|
|
233
|
+
"skipped": skipped,
|
|
234
|
+
"passed": passed,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def update_pull_request(
|
|
239
|
+
owner: str | None,
|
|
240
|
+
repo: str,
|
|
241
|
+
pull_number: int,
|
|
242
|
+
state: str | None = None,
|
|
243
|
+
title: str | None = None,
|
|
244
|
+
body: str | None = None,
|
|
245
|
+
) -> dict[str, Any]:
|
|
246
|
+
"""Update a pull request — including closing or reopening it.
|
|
247
|
+
|
|
248
|
+
Note: this does NOT merge. Set state="closed" to close a PR without merging.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
owner: Repository owner (defaults to GITHUB_DEFAULT_ORG if unset)
|
|
252
|
+
repo: Repository name
|
|
253
|
+
pull_number: Pull request number
|
|
254
|
+
state: "open" or "closed" (set "closed" to close the PR)
|
|
255
|
+
title: New title (optional)
|
|
256
|
+
body: New body (optional)
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Updated PR details (number, state, html_url, title)
|
|
260
|
+
"""
|
|
261
|
+
owner = resolve_owner(owner)
|
|
262
|
+
repo = validate_name(repo, "repo")
|
|
263
|
+
pull_number = validate_positive_int(pull_number, "pull_number")
|
|
264
|
+
|
|
265
|
+
json_data: dict[str, Any] = {}
|
|
266
|
+
if state is not None:
|
|
267
|
+
if state not in ("open", "closed"):
|
|
268
|
+
raise ValidationError("state must be 'open' or 'closed'")
|
|
269
|
+
json_data["state"] = state
|
|
270
|
+
if title is not None:
|
|
271
|
+
json_data["title"] = title
|
|
272
|
+
if body is not None:
|
|
273
|
+
json_data["body"] = body
|
|
274
|
+
if not json_data:
|
|
275
|
+
raise ValidationError("no fields to update")
|
|
276
|
+
|
|
277
|
+
client = get_client()
|
|
278
|
+
result = await client.patch(
|
|
279
|
+
f"/repos/{owner}/{repo}/pulls/{pull_number}",
|
|
280
|
+
json_data=json_data,
|
|
281
|
+
)
|
|
282
|
+
return {
|
|
283
|
+
"number": result.get("number"),
|
|
284
|
+
"state": result.get("state"),
|
|
285
|
+
"html_url": result.get("html_url"),
|
|
286
|
+
"title": result.get("title"),
|
|
287
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Search tools.
|
|
2
|
+
|
|
3
|
+
Tools for searching code and issues/PRs across GitHub.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..client import get_client
|
|
9
|
+
from ..errors import ValidationError
|
|
10
|
+
from ..models import (
|
|
11
|
+
MAX_QUERY_LENGTH,
|
|
12
|
+
clamp_per_page,
|
|
13
|
+
validate_positive_int,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _validate_query(query: str) -> str:
|
|
18
|
+
if not query or not query.strip():
|
|
19
|
+
raise ValidationError("query must not be empty")
|
|
20
|
+
query = query.strip()
|
|
21
|
+
if len(query) > MAX_QUERY_LENGTH:
|
|
22
|
+
raise ValidationError("query is too long")
|
|
23
|
+
return query
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def search_code(
|
|
27
|
+
query: str,
|
|
28
|
+
per_page: int = 30,
|
|
29
|
+
page: int = 1,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""Search for code across GitHub.
|
|
32
|
+
|
|
33
|
+
Uses GitHub's code search syntax (e.g., "addClass repo:jquery/jquery").
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
query: Search query string
|
|
37
|
+
per_page: Results per page (max 100)
|
|
38
|
+
page: Page number
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Search results with total count and matched code items
|
|
42
|
+
"""
|
|
43
|
+
query = _validate_query(query)
|
|
44
|
+
|
|
45
|
+
client = get_client()
|
|
46
|
+
params: dict[str, Any] = {
|
|
47
|
+
"q": query,
|
|
48
|
+
"per_page": clamp_per_page(per_page),
|
|
49
|
+
"page": validate_positive_int(page, "page"),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return await client.get("/search/code", params=params)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def search_issues(
|
|
56
|
+
query: str,
|
|
57
|
+
per_page: int = 30,
|
|
58
|
+
page: int = 1,
|
|
59
|
+
) -> dict[str, Any]:
|
|
60
|
+
"""Search for issues and pull requests across GitHub.
|
|
61
|
+
|
|
62
|
+
Uses GitHub's issue search syntax (e.g., "is:open is:pr author:octocat").
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
query: Search query string
|
|
66
|
+
per_page: Results per page (max 100)
|
|
67
|
+
page: Page number
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Search results with total count and matched issues/PRs
|
|
71
|
+
"""
|
|
72
|
+
query = _validate_query(query)
|
|
73
|
+
|
|
74
|
+
client = get_client()
|
|
75
|
+
params: dict[str, Any] = {
|
|
76
|
+
"q": query,
|
|
77
|
+
"per_page": clamp_per_page(per_page),
|
|
78
|
+
"page": validate_positive_int(page, "page"),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return await client.get("/search/issues", params=params)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mcp-github-crunchtools
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Secure MCP server for GitHub issues, pull requests, files, and search
|
|
5
|
+
Author: crunchtools.com
|
|
6
|
+
License-Expression: AGPL-3.0-or-later
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: devops,fastmcp,github,mcp
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: fastmcp>=2.0
|
|
18
|
+
Requires-Dist: httpx>=0.28
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# MCP GitHub CrunchTools
|
|
28
|
+
|
|
29
|
+
A secure MCP (Model Context Protocol) server for GitHub issues, pull requests, repository files, and search. Works with github.com and GitHub Enterprise Server.
|
|
30
|
+
|
|
31
|
+
## Overview
|
|
32
|
+
|
|
33
|
+
This MCP server is designed to be:
|
|
34
|
+
|
|
35
|
+
- **Secure by default** - STRIDE threat model (see [SECURITY.md](SECURITY.md)), Pydantic input validation, and the API token held as a `SecretStr` to prevent accidental logging
|
|
36
|
+
- **No third-party services** - Runs locally via stdio, your API token never leaves your machine
|
|
37
|
+
- **Multi-instance** - Works with github.com or GitHub Enterprise Server via configurable API URL
|
|
38
|
+
- **Cross-platform** - Works on Linux, macOS, and Windows
|
|
39
|
+
- **Automatically updated** - GitHub Actions monitor for CVEs and update dependencies
|
|
40
|
+
- **Containerized** - Available at `quay.io/crunchtools/mcp-github` built on [Hummingbird Python](https://quay.io/repository/hummingbird/python) base image
|
|
41
|
+
|
|
42
|
+
## Naming Convention
|
|
43
|
+
|
|
44
|
+
| Component | Name |
|
|
45
|
+
|-----------|------|
|
|
46
|
+
| GitHub repo | [crunchtools/mcp-github](https://github.com/crunchtools/mcp-github) |
|
|
47
|
+
| Container | `quay.io/crunchtools/mcp-github` |
|
|
48
|
+
| Python package (PyPI) | `mcp-github-crunchtools` |
|
|
49
|
+
| CLI command | `mcp-github-crunchtools` |
|
|
50
|
+
| Module import | `mcp_github_crunchtools` |
|
|
51
|
+
|
|
52
|
+
## Why Hummingbird?
|
|
53
|
+
|
|
54
|
+
The container image is built on the [Hummingbird Python base image](https://quay.io/repository/hummingbird/python) from [Project Hummingbird](https://github.com/hummingbird-project), which provides:
|
|
55
|
+
|
|
56
|
+
- **Minimal CVE exposure** - Built with a minimal package set, dramatically reducing the attack surface
|
|
57
|
+
- **Regular updates** - Security patches are applied promptly
|
|
58
|
+
- **Optimized for Python** - Pre-configured Python environment
|
|
59
|
+
- **Production-ready** - Proper signal handling and non-root user defaults
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
### Issues (3 tools)
|
|
64
|
+
- `list_issues_tool` - List issues for a repository (pull requests excluded)
|
|
65
|
+
- `get_issue_tool` - Get a single issue by number
|
|
66
|
+
- `create_issue_comment_tool` - Comment on an issue or pull request (write)
|
|
67
|
+
|
|
68
|
+
### Pull Requests (4 tools)
|
|
69
|
+
- `list_pull_requests_tool` - List pull requests for a repository
|
|
70
|
+
- `get_pull_request_tool` - Get a single pull request by number
|
|
71
|
+
- `get_pull_request_diff_tool` - Get the unified diff for a pull request
|
|
72
|
+
- `get_pull_request_checks_tool` - Combined CI status (check-runs + commit status)
|
|
73
|
+
|
|
74
|
+
### Files (2 tools)
|
|
75
|
+
- `get_file_content_tool` - Read decoded file content from a repository
|
|
76
|
+
- `list_repo_tree_tool` - List the git tree (files and directories)
|
|
77
|
+
|
|
78
|
+
### Search (2 tools)
|
|
79
|
+
- `search_code_tool` - Search code across GitHub
|
|
80
|
+
- `search_issues_tool` - Search issues and pull requests across GitHub
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
### With uvx (Recommended)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uvx mcp-github-crunchtools
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### With pip
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install mcp-github-crunchtools
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### With Container
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
podman run -e GITHUB_TOKEN=your_token \
|
|
100
|
+
quay.io/crunchtools/mcp-github
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
### Environment Variables
|
|
106
|
+
|
|
107
|
+
| Variable | Required | Default | Description |
|
|
108
|
+
|----------|----------|---------|-------------|
|
|
109
|
+
| `GITHUB_TOKEN` | Yes | — | GitHub Personal Access Token |
|
|
110
|
+
| `GITHUB_API_URL` | No | `https://api.github.com` | API base URL (set for GHES) |
|
|
111
|
+
| `GITHUB_DEFAULT_ORG` | No | — | Default owner when a tool omits `owner` |
|
|
112
|
+
| `SSL_CERT_FILE` | No | — | Custom CA bundle path, for self-hosted GHES with an internal CA |
|
|
113
|
+
| `GITHUB_SSL_VERIFY` | No | `true` | Set `false` to disable TLS verification (not recommended) |
|
|
114
|
+
|
|
115
|
+
### Creating a GitHub Personal Access Token
|
|
116
|
+
|
|
117
|
+
1. **Navigate to token settings**
|
|
118
|
+
- Go to https://github.com/settings/tokens
|
|
119
|
+
|
|
120
|
+
2. **Create a token**
|
|
121
|
+
- **Name**: `mcp-github-crunchtools`
|
|
122
|
+
- **Expiration**: Set an appropriate date (90 days recommended)
|
|
123
|
+
- **Scopes**: Grant read access to contents, issues, and pull requests.
|
|
124
|
+
Add write to issues/PRs only if you need `create_issue_comment_tool`.
|
|
125
|
+
|
|
126
|
+
3. **Copy and Store Token**
|
|
127
|
+
- Copy the token immediately (shown only once)
|
|
128
|
+
- Store securely in a password manager
|
|
129
|
+
|
|
130
|
+
### Add to Claude Code
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
claude mcp add mcp-github-crunchtools \
|
|
134
|
+
--env GITHUB_TOKEN=your_token_here \
|
|
135
|
+
-- uvx mcp-github-crunchtools
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For GitHub Enterprise Server:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
claude mcp add mcp-github-crunchtools \
|
|
142
|
+
--env GITHUB_TOKEN=your_token_here \
|
|
143
|
+
--env GITHUB_API_URL=https://ghe.example.com/api/v3 \
|
|
144
|
+
-- uvx mcp-github-crunchtools
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
For the container version:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
claude mcp add mcp-github-crunchtools \
|
|
151
|
+
--env GITHUB_TOKEN=your_token_here \
|
|
152
|
+
-- podman run -i --rm -e GITHUB_TOKEN quay.io/crunchtools/mcp-github
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Usage Examples
|
|
156
|
+
|
|
157
|
+
### List Issues
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
User: List open issues for crunchtools/mcp-github
|
|
161
|
+
Assistant: [calls list_issues_tool with owner="crunchtools", repo="mcp-github"]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Review a Pull Request
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
User: Show me the diff for PR #5 in crunchtools/mcp-github
|
|
168
|
+
Assistant: [calls get_pull_request_diff_tool with pull_number=5]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Check CI Status
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
User: Did the checks pass on pull request 5?
|
|
175
|
+
Assistant: [calls get_pull_request_checks_tool with pull_number=5]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Read a File
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
User: Show me src/server.py from crunchtools/mcp-github
|
|
182
|
+
Assistant: [calls get_file_content_tool with path="src/server.py"]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Search
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
User: Find code using FastMCP in crunchtools repos
|
|
189
|
+
Assistant: [calls search_code_tool with query="FastMCP org:crunchtools"]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Security
|
|
193
|
+
|
|
194
|
+
This server was designed with security as a primary concern. See [SECURITY.md](SECURITY.md) for details.
|
|
195
|
+
|
|
196
|
+
### Key Security Features
|
|
197
|
+
|
|
198
|
+
1. **Token Protection**
|
|
199
|
+
- Stored as SecretStr (never accidentally logged)
|
|
200
|
+
- Environment variable only (never in files or args)
|
|
201
|
+
- Sanitized from all error messages
|
|
202
|
+
|
|
203
|
+
2. **Input Validation**
|
|
204
|
+
- Pydantic models for write inputs
|
|
205
|
+
- Allowlist character validation for owner/repo names
|
|
206
|
+
- Path traversal prevention for file reads
|
|
207
|
+
|
|
208
|
+
3. **API Hardening**
|
|
209
|
+
- Bearer-token auth and pinned GitHub API version
|
|
210
|
+
- HTTPS enforcement (except localhost)
|
|
211
|
+
- TLS certificate validation
|
|
212
|
+
- Request timeouts (30s)
|
|
213
|
+
- Response size limits (10MB)
|
|
214
|
+
|
|
215
|
+
4. **Automated CVE Scanning**
|
|
216
|
+
- GitHub Actions scan dependencies
|
|
217
|
+
- Container security scanning with Trivy
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
### Setup
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
git clone https://github.com/crunchtools/mcp-github.git
|
|
225
|
+
cd mcp-github
|
|
226
|
+
uv sync --all-extras
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Run Tests
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
uv run pytest
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Lint and Type Check
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
uv run ruff check src tests
|
|
239
|
+
uv run mypy src
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Build Container
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
podman build -t mcp-github .
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
AGPL-3.0-or-later
|
|
251
|
+
|
|
252
|
+
## Contributing
|
|
253
|
+
|
|
254
|
+
Contributions welcome! Please read SECURITY.md before submitting security-related changes.
|
|
255
|
+
|
|
256
|
+
## Links
|
|
257
|
+
|
|
258
|
+
- [GitHub REST API Documentation](https://docs.github.com/en/rest)
|
|
259
|
+
- [FastMCP Documentation](https://gofastmcp.com/)
|
|
260
|
+
- [MCP Specification](https://modelcontextprotocol.io/)
|
|
261
|
+
- [crunchtools.com](https://crunchtools.com)
|
|
262
|
+
|
|
263
|
+
<!-- mcp-name: io.github.crunchtools/github -->
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
mcp_github_crunchtools/__init__.py,sha256=TA95qz8FfAAQot2W1bJEylmCEBzv6yEN8np_T188lGU,1630
|
|
2
|
+
mcp_github_crunchtools/__main__.py,sha256=BYo13fF7OXaJiVbx-g9OxLXxXFX_FtH8SjvIiBDw3WM,115
|
|
3
|
+
mcp_github_crunchtools/client.py,sha256=Tg2_4s86Fj6iXhU7FRQgO0nTBJm35kMYCvIVG-IpscY,9112
|
|
4
|
+
mcp_github_crunchtools/config.py,sha256=n9NpxhxJdigCj_5T7C5qbBqJJMkyikxHooxPXJteOVw,4093
|
|
5
|
+
mcp_github_crunchtools/errors.py,sha256=z99ivMY95PrVu0a1VV9S88_d51F8NWOmoAk23K5p20g,1919
|
|
6
|
+
mcp_github_crunchtools/models.py,sha256=KgxcbSk0Zxngr0oOF_d9xDzt7L3_n5Wo4lOSyVqwoFE,5871
|
|
7
|
+
mcp_github_crunchtools/server.py,sha256=GYOBOSFTDKCdUpdlvQGdxOFItSBMU18MXXp0TddehZc,13726
|
|
8
|
+
mcp_github_crunchtools/tools/__init__.py,sha256=poTwR5xIH15MmUPSYlrGQI2HcAD107yz6UlsUzkkRyQ,1041
|
|
9
|
+
mcp_github_crunchtools/tools/actions.py,sha256=rli_XZ_wwO_gx1sTcY9v1ZrgadQRbtvWIFH14DZIRnA,5884
|
|
10
|
+
mcp_github_crunchtools/tools/files.py,sha256=3F2nCeGVoP8GKTFuhTPhyOxQBEBxp4lWWTmgv907axQ,3635
|
|
11
|
+
mcp_github_crunchtools/tools/issues.py,sha256=XQ8CWLh4YRsHWEN3mQRnG7XwuO9Q0_6HB82HYOXSfhE,6429
|
|
12
|
+
mcp_github_crunchtools/tools/pull_requests.py,sha256=INN75H71d1XNVvWieMLlx7JAfm89v02i_mCcvDQwklk,9068
|
|
13
|
+
mcp_github_crunchtools/tools/search.py,sha256=FVLA0J7J_GNCh39qZQlFZlMRn4XyVN54ODkxVJbHbQg,1961
|
|
14
|
+
mcp_github_crunchtools-1.0.1.dist-info/METADATA,sha256=s221ntasJ3PZw1jX_lJb0ylHn_0qF9ePLKl9b93zcko,7848
|
|
15
|
+
mcp_github_crunchtools-1.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
16
|
+
mcp_github_crunchtools-1.0.1.dist-info/entry_points.txt,sha256=RwJaHs3zcHeGjhsSDbZgxpGpDEWBKOFIZojcXXCpIMs,71
|
|
17
|
+
mcp_github_crunchtools-1.0.1.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
18
|
+
mcp_github_crunchtools-1.0.1.dist-info/RECORD,,
|