mcp-github-crunchtools 1.3.0__tar.gz → 1.3.2__tar.gz
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-1.3.0 → mcp_github_crunchtools-1.3.2}/CHANGELOG.md +16 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/Containerfile +1 -1
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/PKG-INFO +1 -1
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/pyproject.toml +1 -1
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/__init__.py +1 -1
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/client.py +19 -4
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/server.py +10 -9
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/test_tools.py +37 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/uv.lock +1 -1
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.gatehouse-ignore +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/dependabot.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/ashigaru.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/ci.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/constitution.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/container.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/dependabot-automerge.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/gatehouse-retriage.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/gatehouse.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/publish.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/security.yml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.gitignore +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.pre-commit-config.yaml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/memory/constitution.md +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/specs/000-baseline/spec.md +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/specs/001-pr-merge-logs-release/spec.md +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/specs/002-request-token-merge-gate/spec.md +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/templates/plan-template.md +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/templates/spec-template.md +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/CLAUDE.md +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/LICENSE +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/README.md +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/SECURITY.md +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/gourmand-exceptions.toml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/gourmand.toml +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/server.json +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/__main__.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/config.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/errors.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/models.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/__init__.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/actions.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/files.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/issues.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/pull_requests.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/releases.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/search.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/__init__.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/conftest.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/test_merge_gate.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/test_request_token.py +0 -0
- {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/test_validation.py +0 -0
|
@@ -6,6 +6,22 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.3.2] - 2026-10-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- The integer ID parameters of the read tools (`issue_number`, `pull_number`,
|
|
14
|
+
`run_id`) now publish `minimum: 1` in their schema, so `0` and negative IDs
|
|
15
|
+
are rejected.
|
|
16
|
+
|
|
17
|
+
## [1.3.1] - 2026-10-16
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- A primary rate limit reported GitHub's `x-ratelimit-reset` epoch as the
|
|
22
|
+
wait ("Retry after 1792152901 seconds"). It now reports the seconds left
|
|
23
|
+
until the reset.
|
|
24
|
+
|
|
9
25
|
## [1.3.0] - 2026-10-14
|
|
10
26
|
|
|
11
27
|
### Added
|
|
@@ -27,7 +27,7 @@ FROM quay.io/hummingbird/python:latest
|
|
|
27
27
|
|
|
28
28
|
# Labels for container metadata
|
|
29
29
|
LABEL name="mcp-github-crunchtools" \
|
|
30
|
-
version="1.3.
|
|
30
|
+
version="1.3.2" \
|
|
31
31
|
summary="Secure MCP server for GitHub issues, pull requests, Actions, releases, files, and search" \
|
|
32
32
|
description="A security-focused MCP server for GitHub built on Red Hat UBI" \
|
|
33
33
|
maintainer="crunchtools.com" \
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/client.py
RENAMED
|
@@ -7,6 +7,7 @@ All requests go through this client to ensure consistent security practices.
|
|
|
7
7
|
import ipaddress
|
|
8
8
|
import logging
|
|
9
9
|
import re
|
|
10
|
+
import time
|
|
10
11
|
from typing import Any
|
|
11
12
|
from urllib.parse import urlparse
|
|
12
13
|
|
|
@@ -25,6 +26,23 @@ from .errors import (
|
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
27
28
|
|
|
29
|
+
|
|
30
|
+
def _retry_after_seconds(headers: httpx.Headers) -> int | None:
|
|
31
|
+
"""Seconds until a rate-limited call may be retried.
|
|
32
|
+
|
|
33
|
+
``retry-after`` is already a duration; ``x-ratelimit-reset`` is the UTC
|
|
34
|
+
epoch second the window resets, so it is converted to a duration.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
if (retry_after := headers.get("retry-after")) is not None:
|
|
38
|
+
return max(0, int(retry_after))
|
|
39
|
+
if (reset := headers.get("x-ratelimit-reset")) is not None:
|
|
40
|
+
return max(0, int(reset) - int(time.time()))
|
|
41
|
+
except ValueError:
|
|
42
|
+
return None
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
28
46
|
MAX_RESPONSE_SIZE = 10 * 1024 * 1024
|
|
29
47
|
# A job log is streamed and only its tail is kept, so it may be larger than a
|
|
30
48
|
# buffered API response; past this the download is abandoned.
|
|
@@ -302,10 +320,7 @@ class GitHubClient:
|
|
|
302
320
|
rate_remaining = response.headers.get("x-ratelimit-remaining")
|
|
303
321
|
|
|
304
322
|
if status_code == 429 or (status_code == 403 and rate_remaining == "0"):
|
|
305
|
-
|
|
306
|
-
if retry_after is None:
|
|
307
|
-
retry_after = response.headers.get("x-ratelimit-reset")
|
|
308
|
-
raise RateLimitError(int(retry_after) if retry_after else None)
|
|
323
|
+
raise RateLimitError(_retry_after_seconds(response.headers))
|
|
309
324
|
|
|
310
325
|
if status_code == 401:
|
|
311
326
|
raise PermissionDeniedError("Valid token with required scopes")
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/server.py
RENAMED
|
@@ -4,9 +4,10 @@ This module creates and configures the MCP server with all tools.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Annotated, Any
|
|
8
8
|
|
|
9
9
|
from fastmcp import FastMCP
|
|
10
|
+
from pydantic import Field
|
|
10
11
|
|
|
11
12
|
from .tools import (
|
|
12
13
|
create_issue,
|
|
@@ -44,7 +45,7 @@ READ_ONLY = {"readOnlyHint": True}
|
|
|
44
45
|
|
|
45
46
|
mcp = FastMCP(
|
|
46
47
|
name="mcp-github",
|
|
47
|
-
version="1.3.
|
|
48
|
+
version="1.3.2",
|
|
48
49
|
instructions=(
|
|
49
50
|
"Secure MCP server for GitHub repositories: issues, pull requests "
|
|
50
51
|
"(open, review discussion, diffs, CI checks, merge), Actions runs and "
|
|
@@ -89,7 +90,7 @@ async def list_issues_tool(
|
|
|
89
90
|
@mcp.tool(annotations=READ_ONLY)
|
|
90
91
|
async def get_issue_tool(
|
|
91
92
|
repo: str,
|
|
92
|
-
issue_number: int,
|
|
93
|
+
issue_number: Annotated[int, Field(ge=1)],
|
|
93
94
|
owner: str | None = None,
|
|
94
95
|
) -> dict[str, Any]:
|
|
95
96
|
"""Get a single GitHub issue by number.
|
|
@@ -222,7 +223,7 @@ async def list_pull_requests_tool(
|
|
|
222
223
|
@mcp.tool(annotations=READ_ONLY)
|
|
223
224
|
async def get_pull_request_tool(
|
|
224
225
|
repo: str,
|
|
225
|
-
pull_number: int,
|
|
226
|
+
pull_number: Annotated[int, Field(ge=1)],
|
|
226
227
|
owner: str | None = None,
|
|
227
228
|
) -> dict[str, Any]:
|
|
228
229
|
"""Get a single GitHub pull request by number.
|
|
@@ -241,7 +242,7 @@ async def get_pull_request_tool(
|
|
|
241
242
|
@mcp.tool(annotations=READ_ONLY)
|
|
242
243
|
async def get_pull_request_diff_tool(
|
|
243
244
|
repo: str,
|
|
244
|
-
pull_number: int,
|
|
245
|
+
pull_number: Annotated[int, Field(ge=1)],
|
|
245
246
|
owner: str | None = None,
|
|
246
247
|
) -> dict[str, Any]:
|
|
247
248
|
"""Get the unified diff for a GitHub pull request.
|
|
@@ -260,7 +261,7 @@ async def get_pull_request_diff_tool(
|
|
|
260
261
|
@mcp.tool(annotations=READ_ONLY)
|
|
261
262
|
async def get_pull_request_checks_tool(
|
|
262
263
|
repo: str,
|
|
263
|
-
pull_number: int,
|
|
264
|
+
pull_number: Annotated[int, Field(ge=1)],
|
|
264
265
|
owner: str | None = None,
|
|
265
266
|
) -> dict[str, Any]:
|
|
266
267
|
"""Get a CI verdict for a PR that distinguishes skipped from failed.
|
|
@@ -399,7 +400,7 @@ async def merge_pull_request_tool(
|
|
|
399
400
|
@mcp.tool(annotations=READ_ONLY)
|
|
400
401
|
async def list_pull_request_comments_tool(
|
|
401
402
|
repo: str,
|
|
402
|
-
pull_number: int,
|
|
403
|
+
pull_number: Annotated[int, Field(ge=1)],
|
|
403
404
|
owner: str | None = None,
|
|
404
405
|
) -> dict[str, Any]:
|
|
405
406
|
"""List everything said on a GitHub pull request, in one list.
|
|
@@ -650,7 +651,7 @@ async def rerun_failed_jobs_tool(
|
|
|
650
651
|
@mcp.tool(annotations=READ_ONLY)
|
|
651
652
|
async def get_workflow_run_tool(
|
|
652
653
|
repo: str,
|
|
653
|
-
run_id: int,
|
|
654
|
+
run_id: Annotated[int, Field(ge=1)],
|
|
654
655
|
owner: str | None = None,
|
|
655
656
|
) -> dict[str, Any]:
|
|
656
657
|
"""Get one GitHub Actions workflow run and its jobs (no logs).
|
|
@@ -670,7 +671,7 @@ async def get_workflow_run_tool(
|
|
|
670
671
|
@mcp.tool(annotations=READ_ONLY)
|
|
671
672
|
async def get_failed_job_logs_tool(
|
|
672
673
|
repo: str,
|
|
673
|
-
run_id: int,
|
|
674
|
+
run_id: Annotated[int, Field(ge=1)],
|
|
674
675
|
tail_lines: int = 200,
|
|
675
676
|
owner: str | None = None,
|
|
676
677
|
) -> dict[str, Any]:
|
|
@@ -6,6 +6,7 @@ Integration tests with a real GitHub account should be run separately.
|
|
|
6
6
|
|
|
7
7
|
import base64
|
|
8
8
|
import os
|
|
9
|
+
import time
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
import pytest
|
|
@@ -139,6 +140,20 @@ class TestReadOnlyAnnotation:
|
|
|
139
140
|
def test_every_read_only_tool_has_call_arguments(self) -> None:
|
|
140
141
|
assert set(READ_ONLY_CALLS) == READ_ONLY
|
|
141
142
|
|
|
143
|
+
@pytest.mark.asyncio
|
|
144
|
+
async def test_read_tool_integer_ids_publish_minimum_one(self) -> None:
|
|
145
|
+
from mcp_github_crunchtools.server import mcp
|
|
146
|
+
|
|
147
|
+
found = 0
|
|
148
|
+
for tool in await mcp.list_tools():
|
|
149
|
+
if tool.name not in READ_ONLY:
|
|
150
|
+
continue
|
|
151
|
+
for field in ("issue_number", "pull_number", "run_id"):
|
|
152
|
+
if field in tool.parameters["properties"]:
|
|
153
|
+
found += 1
|
|
154
|
+
assert tool.parameters["properties"][field]["minimum"] == 1
|
|
155
|
+
assert found == 7
|
|
156
|
+
|
|
142
157
|
@pytest.mark.asyncio
|
|
143
158
|
@pytest.mark.parametrize("name", sorted(READ_ONLY))
|
|
144
159
|
async def test_read_only_tool_sends_only_get(self, name: str) -> None:
|
|
@@ -1909,6 +1924,28 @@ class TestClientErrorHandling:
|
|
|
1909
1924
|
with _patch_client(resp), pytest.raises(RateLimitError):
|
|
1910
1925
|
await get_issue(owner="o", repo="r", issue_number=1)
|
|
1911
1926
|
|
|
1927
|
+
@pytest.mark.asyncio
|
|
1928
|
+
async def test_403_rate_limit_reset_is_a_duration(self) -> None:
|
|
1929
|
+
"""x-ratelimit-reset is an epoch second; the error reports the wait,
|
|
1930
|
+
not the timestamp."""
|
|
1931
|
+
from mcp_github_crunchtools.errors import RateLimitError
|
|
1932
|
+
from mcp_github_crunchtools.tools import get_issue
|
|
1933
|
+
|
|
1934
|
+
resp = _mock_response(
|
|
1935
|
+
status_code=403,
|
|
1936
|
+
json_data={"message": "API rate limit exceeded"},
|
|
1937
|
+
headers={
|
|
1938
|
+
"x-ratelimit-remaining": "0",
|
|
1939
|
+
"x-ratelimit-reset": str(int(time.time()) + 600),
|
|
1940
|
+
},
|
|
1941
|
+
)
|
|
1942
|
+
|
|
1943
|
+
with _patch_client(resp), pytest.raises(RateLimitError) as exc_info:
|
|
1944
|
+
await get_issue(owner="o", repo="r", issue_number=1)
|
|
1945
|
+
|
|
1946
|
+
seconds = int(str(exc_info.value).split("Retry after ")[1].split()[0])
|
|
1947
|
+
assert 590 <= seconds <= 600
|
|
1948
|
+
|
|
1912
1949
|
@pytest.mark.asyncio
|
|
1913
1950
|
async def test_403_preserves_github_message_when_not_rate_limited(
|
|
1914
1951
|
self,
|
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/ISSUE_TEMPLATE/config.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/ashigaru.yml
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/constitution.yml
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/container.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/gatehouse.yml
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/security.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/memory/constitution.md
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/specs/000-baseline/spec.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/templates/plan-template.md
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/templates/spec-template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/__main__.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/config.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/errors.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|