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.
Files changed (54) hide show
  1. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/CHANGELOG.md +16 -0
  2. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/Containerfile +1 -1
  3. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/PKG-INFO +1 -1
  4. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/pyproject.toml +1 -1
  5. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/__init__.py +1 -1
  6. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/client.py +19 -4
  7. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/server.py +10 -9
  8. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/test_tools.py +37 -0
  9. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/uv.lock +1 -1
  10. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.gatehouse-ignore +0 -0
  11. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  12. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  13. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  14. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/dependabot.yml +0 -0
  15. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/ashigaru.yml +0 -0
  16. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/ci.yml +0 -0
  17. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/constitution.yml +0 -0
  18. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/container.yml +0 -0
  19. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/dependabot-automerge.yml +0 -0
  20. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/gatehouse-retriage.yml +0 -0
  21. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/gatehouse.yml +0 -0
  22. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/publish.yml +0 -0
  23. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.github/workflows/security.yml +0 -0
  24. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.gitignore +0 -0
  25. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.pre-commit-config.yaml +0 -0
  26. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/memory/constitution.md +0 -0
  27. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/specs/000-baseline/spec.md +0 -0
  28. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/specs/001-pr-merge-logs-release/spec.md +0 -0
  29. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/specs/002-request-token-merge-gate/spec.md +0 -0
  30. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/templates/plan-template.md +0 -0
  31. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/.specify/templates/spec-template.md +0 -0
  32. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/CLAUDE.md +0 -0
  33. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/LICENSE +0 -0
  34. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/README.md +0 -0
  35. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/SECURITY.md +0 -0
  36. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/gourmand-exceptions.toml +0 -0
  37. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/gourmand.toml +0 -0
  38. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/server.json +0 -0
  39. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/__main__.py +0 -0
  40. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/config.py +0 -0
  41. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/errors.py +0 -0
  42. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/models.py +0 -0
  43. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/__init__.py +0 -0
  44. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/actions.py +0 -0
  45. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/files.py +0 -0
  46. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/issues.py +0 -0
  47. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/pull_requests.py +0 -0
  48. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/releases.py +0 -0
  49. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/src/mcp_github_crunchtools/tools/search.py +0 -0
  50. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/__init__.py +0 -0
  51. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/conftest.py +0 -0
  52. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/test_merge_gate.py +0 -0
  53. {mcp_github_crunchtools-1.3.0 → mcp_github_crunchtools-1.3.2}/tests/test_request_token.py +0 -0
  54. {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.0" \
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" \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-github-crunchtools
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Secure MCP server for GitHub issues, pull requests, Actions, releases, files, and search
5
5
  Author: crunchtools.com
6
6
  License-Expression: AGPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-github-crunchtools"
3
- version = "1.3.0"
3
+ version = "1.3.2"
4
4
  description = "Secure MCP server for GitHub issues, pull requests, Actions, releases, files, and search"
5
5
  requires-python = ">=3.10"
6
6
  license = "AGPL-3.0-or-later"
@@ -29,7 +29,7 @@ import argparse
29
29
 
30
30
  from .server import mcp
31
31
 
32
- __version__ = "1.3.0"
32
+ __version__ = "1.3.2"
33
33
  __all__ = ["main", "mcp"]
34
34
 
35
35
 
@@ -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
- retry_after = response.headers.get("retry-after")
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")
@@ -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.0",
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,
@@ -922,7 +922,7 @@ wheels = [
922
922
 
923
923
  [[package]]
924
924
  name = "mcp-github-crunchtools"
925
- version = "1.3.0"
925
+ version = "1.3.2"
926
926
  source = { editable = "." }
927
927
  dependencies = [
928
928
  { name = "fastmcp" },