mcp-github-crunchtools 1.3.1__tar.gz → 1.4.0__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.1 → mcp_github_crunchtools-1.4.0}/CHANGELOG.md +23 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/CLAUDE.md +3 -3
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/Containerfile +1 -1
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/PKG-INFO +8 -4
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/README.md +7 -3
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/pyproject.toml +1 -1
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/server.json +2 -2
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/__init__.py +1 -1
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/server.py +45 -9
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/__init__.py +2 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/issues.py +57 -3
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/pull_requests.py +13 -1
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/tests/test_tools.py +115 -2
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/uv.lock +1 -1
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.gatehouse-ignore +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/dependabot.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/ashigaru.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/ci.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/constitution.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/container.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/dependabot-automerge.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/gatehouse-retriage.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/gatehouse.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/publish.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/security.yml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.gitignore +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.pre-commit-config.yaml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/memory/constitution.md +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/specs/000-baseline/spec.md +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/specs/001-pr-merge-logs-release/spec.md +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/specs/002-request-token-merge-gate/spec.md +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/templates/plan-template.md +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/templates/spec-template.md +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/LICENSE +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/SECURITY.md +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/gourmand-exceptions.toml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/gourmand.toml +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/__main__.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/client.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/config.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/errors.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/models.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/actions.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/files.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/releases.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/search.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/tests/__init__.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/tests/conftest.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/tests/test_merge_gate.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/tests/test_request_token.py +0 -0
- {mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/tests/test_validation.py +0 -0
|
@@ -6,6 +6,29 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.4.0] - 2026-10-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `list_issue_comments_tool`: the comments on an issue or a pull request
|
|
14
|
+
conversation, oldest first, one page at a time, with id, author,
|
|
15
|
+
author_association, created_at, updated_at, body and html_url (#45).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `list_pull_request_comments_tool` called on a plain issue returned GitHub's
|
|
20
|
+
403 "Resource not accessible by personal access token", which reads like a
|
|
21
|
+
token scope problem. It now says the number is an issue and points to
|
|
22
|
+
`list_issue_comments_tool` (#45).
|
|
23
|
+
|
|
24
|
+
## [1.3.2] - 2026-10-17
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- The integer ID parameters of the read tools (`issue_number`, `pull_number`,
|
|
29
|
+
`run_id`) now publish `minimum: 1` in their schema, so `0` and negative IDs
|
|
30
|
+
are rejected.
|
|
31
|
+
|
|
9
32
|
## [1.3.1] - 2026-10-16
|
|
10
33
|
|
|
11
34
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Claude Code Instructions
|
|
2
2
|
|
|
3
|
-
Secure MCP server for the GitHub REST API with
|
|
3
|
+
Secure MCP server for the GitHub REST API with 26 tools across issues, pull requests, Actions, releases, files, and search. Works with github.com and GitHub Enterprise Server.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -51,11 +51,11 @@ claude mcp add mcp-github-crunchtools \
|
|
|
51
51
|
| `SSL_CERT_FILE` | No | — | Custom CA bundle for self-hosted instances |
|
|
52
52
|
| `GITHUB_SSL_VERIFY` | No | `true` | Set `false` to disable SSL verification |
|
|
53
53
|
|
|
54
|
-
## Available Tools (
|
|
54
|
+
## Available Tools (26)
|
|
55
55
|
|
|
56
56
|
| Category | Tools |
|
|
57
57
|
|----------|-------|
|
|
58
|
-
| Issues | `list_issues_tool`, `get_issue_tool`, `create_issue_tool`, `create_issue_comment_tool`, `update_issue_tool` |
|
|
58
|
+
| Issues | `list_issues_tool`, `get_issue_tool`, `list_issue_comments_tool`, `create_issue_tool`, `create_issue_comment_tool`, `update_issue_tool` |
|
|
59
59
|
| Pull Requests | `list_pull_requests_tool`, `get_pull_request_tool`, `get_pull_request_diff_tool`, `get_pull_request_checks_tool`, `list_pull_request_comments_tool`, `create_pull_request_tool`, `update_pull_request_tool`, `reply_to_review_comment_tool`, `merge_pull_request_tool` |
|
|
60
60
|
| Files | `get_file_content_tool`, `list_repo_tree_tool` |
|
|
61
61
|
| Search | `search_code_tool`, `search_issues_tool` |
|
|
@@ -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.
|
|
30
|
+
version="1.4.0" \
|
|
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
|
+
Version: 1.4.0
|
|
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
|
|
@@ -60,7 +60,7 @@ The container image is built on the [Hummingbird Python base image](https://quay
|
|
|
60
60
|
|
|
61
61
|
## Features
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
26 tools. Tools marked *write* change something on GitHub; the others only
|
|
64
64
|
read and publish `readOnlyHint`. The last column is the fine-grained token
|
|
65
65
|
permission the tool needs (see [Token permissions](#token-permissions)).
|
|
66
66
|
|
|
@@ -70,6 +70,7 @@ permission the tool needs (see [Token permissions](#token-permissions)).
|
|
|
70
70
|
|------|--------------|------------|
|
|
71
71
|
| `list_issues_tool` | List issues for a repository (pull requests excluded) | Issues: read |
|
|
72
72
|
| `get_issue_tool` | Get a single issue by number | Issues: read |
|
|
73
|
+
| `list_issue_comments_tool` | Comments on an issue or a pull request conversation, oldest first | Issues: read, or Pull requests: read for a PR |
|
|
73
74
|
| `create_issue_tool` | Open an issue (*write*) | Issues: write |
|
|
74
75
|
| `create_issue_comment_tool` | Comment on an issue or pull request (*write*) | Issues: write, or Pull requests: write for a PR |
|
|
75
76
|
| `update_issue_tool` | Edit, close or reopen an issue (*write*) | Issues: write |
|
|
@@ -123,7 +124,10 @@ permission the tool needs (see [Token permissions](#token-permissions)).
|
|
|
123
124
|
|
|
124
125
|
- `list_pull_request_comments_tool` reads each of its three kinds oldest
|
|
125
126
|
first, up to 300 items per kind. `truncated` and `truncated_kinds` say when
|
|
126
|
-
newer items were left out.
|
|
127
|
+
newer items were left out. Called on a plain issue it says so; use
|
|
128
|
+
`list_issue_comments_tool` there.
|
|
129
|
+
- `list_issue_comments_tool` returns one page (up to 100 comments); follow
|
|
130
|
+
`pagination.next_page` for more.
|
|
127
131
|
- `get_workflow_run_tool` lists up to 300 jobs; `jobs_truncated` says when the
|
|
128
132
|
run has more.
|
|
129
133
|
- `get_failed_job_logs_tool` returns at most 1000 lines per job (200 by
|
|
@@ -218,7 +222,7 @@ For a fine-grained personal access token, the repository permissions are:
|
|
|
218
222
|
|------------|--------------------|---------------------|
|
|
219
223
|
| Metadata | every tool (GitHub grants it with any other permission) | — |
|
|
220
224
|
| Contents | `get_file_content_tool`, `list_repo_tree_tool` | `merge_pull_request_tool`, `create_release_tool` |
|
|
221
|
-
| Issues | `list_issues_tool`, `get_issue_tool` | `create_issue_tool`, `update_issue_tool`, `create_issue_comment_tool` |
|
|
225
|
+
| Issues | `list_issues_tool`, `get_issue_tool`, `list_issue_comments_tool` | `create_issue_tool`, `update_issue_tool`, `create_issue_comment_tool` |
|
|
222
226
|
| Pull requests | `merge_pull_request_tool`, `list_pull_requests_tool`, `get_pull_request_tool`, `get_pull_request_diff_tool`, `get_pull_request_checks_tool`, `list_pull_request_comments_tool` | `create_pull_request_tool`, `update_pull_request_tool`, `reply_to_review_comment_tool`, `create_issue_comment_tool` on a PR |
|
|
223
227
|
| Actions | `list_workflow_runs_tool`, `get_workflow_run_tool`, `get_failed_job_logs_tool` | `trigger_workflow_tool`, `rerun_workflow_run_tool`, `rerun_failed_jobs_tool` |
|
|
224
228
|
| Commit statuses | `get_pull_request_checks_tool`, `merge_pull_request_tool` | — |
|
|
@@ -34,7 +34,7 @@ The container image is built on the [Hummingbird Python base image](https://quay
|
|
|
34
34
|
|
|
35
35
|
## Features
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
26 tools. Tools marked *write* change something on GitHub; the others only
|
|
38
38
|
read and publish `readOnlyHint`. The last column is the fine-grained token
|
|
39
39
|
permission the tool needs (see [Token permissions](#token-permissions)).
|
|
40
40
|
|
|
@@ -44,6 +44,7 @@ permission the tool needs (see [Token permissions](#token-permissions)).
|
|
|
44
44
|
|------|--------------|------------|
|
|
45
45
|
| `list_issues_tool` | List issues for a repository (pull requests excluded) | Issues: read |
|
|
46
46
|
| `get_issue_tool` | Get a single issue by number | Issues: read |
|
|
47
|
+
| `list_issue_comments_tool` | Comments on an issue or a pull request conversation, oldest first | Issues: read, or Pull requests: read for a PR |
|
|
47
48
|
| `create_issue_tool` | Open an issue (*write*) | Issues: write |
|
|
48
49
|
| `create_issue_comment_tool` | Comment on an issue or pull request (*write*) | Issues: write, or Pull requests: write for a PR |
|
|
49
50
|
| `update_issue_tool` | Edit, close or reopen an issue (*write*) | Issues: write |
|
|
@@ -97,7 +98,10 @@ permission the tool needs (see [Token permissions](#token-permissions)).
|
|
|
97
98
|
|
|
98
99
|
- `list_pull_request_comments_tool` reads each of its three kinds oldest
|
|
99
100
|
first, up to 300 items per kind. `truncated` and `truncated_kinds` say when
|
|
100
|
-
newer items were left out.
|
|
101
|
+
newer items were left out. Called on a plain issue it says so; use
|
|
102
|
+
`list_issue_comments_tool` there.
|
|
103
|
+
- `list_issue_comments_tool` returns one page (up to 100 comments); follow
|
|
104
|
+
`pagination.next_page` for more.
|
|
101
105
|
- `get_workflow_run_tool` lists up to 300 jobs; `jobs_truncated` says when the
|
|
102
106
|
run has more.
|
|
103
107
|
- `get_failed_job_logs_tool` returns at most 1000 lines per job (200 by
|
|
@@ -192,7 +196,7 @@ For a fine-grained personal access token, the repository permissions are:
|
|
|
192
196
|
|------------|--------------------|---------------------|
|
|
193
197
|
| Metadata | every tool (GitHub grants it with any other permission) | — |
|
|
194
198
|
| Contents | `get_file_content_tool`, `list_repo_tree_tool` | `merge_pull_request_tool`, `create_release_tool` |
|
|
195
|
-
| Issues | `list_issues_tool`, `get_issue_tool` | `create_issue_tool`, `update_issue_tool`, `create_issue_comment_tool` |
|
|
199
|
+
| Issues | `list_issues_tool`, `get_issue_tool`, `list_issue_comments_tool` | `create_issue_tool`, `update_issue_tool`, `create_issue_comment_tool` |
|
|
196
200
|
| Pull requests | `merge_pull_request_tool`, `list_pull_requests_tool`, `get_pull_request_tool`, `get_pull_request_diff_tool`, `get_pull_request_checks_tool`, `list_pull_request_comments_tool` | `create_pull_request_tool`, `update_pull_request_tool`, `reply_to_review_comment_tool`, `create_issue_comment_tool` on a PR |
|
|
197
201
|
| Actions | `list_workflow_runs_tool`, `get_workflow_run_tool`, `get_failed_job_logs_tool` | `trigger_workflow_tool`, `rerun_workflow_run_tool`, `rerun_failed_jobs_tool` |
|
|
198
202
|
| Commit statuses | `get_pull_request_checks_tool`, `merge_pull_request_tool` | — |
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/crunchtools/mcp-github",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.4.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "mcp-github-crunchtools",
|
|
15
|
-
"version": "1.
|
|
15
|
+
"version": "1.4.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/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,
|
|
@@ -20,6 +21,7 @@ from .tools import (
|
|
|
20
21
|
get_pull_request_checks,
|
|
21
22
|
get_pull_request_diff,
|
|
22
23
|
get_workflow_run,
|
|
24
|
+
list_issue_comments,
|
|
23
25
|
list_issues,
|
|
24
26
|
list_pull_request_comments,
|
|
25
27
|
list_pull_requests,
|
|
@@ -44,7 +46,7 @@ READ_ONLY = {"readOnlyHint": True}
|
|
|
44
46
|
|
|
45
47
|
mcp = FastMCP(
|
|
46
48
|
name="mcp-github",
|
|
47
|
-
version="1.
|
|
49
|
+
version="1.4.0",
|
|
48
50
|
instructions=(
|
|
49
51
|
"Secure MCP server for GitHub repositories: issues, pull requests "
|
|
50
52
|
"(open, review discussion, diffs, CI checks, merge), Actions runs and "
|
|
@@ -89,7 +91,7 @@ async def list_issues_tool(
|
|
|
89
91
|
@mcp.tool(annotations=READ_ONLY)
|
|
90
92
|
async def get_issue_tool(
|
|
91
93
|
repo: str,
|
|
92
|
-
issue_number: int,
|
|
94
|
+
issue_number: Annotated[int, Field(ge=1)],
|
|
93
95
|
owner: str | None = None,
|
|
94
96
|
) -> dict[str, Any]:
|
|
95
97
|
"""Get a single GitHub issue by number.
|
|
@@ -105,6 +107,40 @@ async def get_issue_tool(
|
|
|
105
107
|
return await get_issue(owner=owner, repo=repo, issue_number=issue_number)
|
|
106
108
|
|
|
107
109
|
|
|
110
|
+
@mcp.tool(annotations=READ_ONLY)
|
|
111
|
+
async def list_issue_comments_tool(
|
|
112
|
+
repo: str,
|
|
113
|
+
issue_number: Annotated[int, Field(ge=1)],
|
|
114
|
+
owner: str | None = None,
|
|
115
|
+
per_page: int = 30,
|
|
116
|
+
page: int = 1,
|
|
117
|
+
) -> dict[str, Any]:
|
|
118
|
+
"""List the comments on a GitHub issue or pull request conversation, oldest first.
|
|
119
|
+
|
|
120
|
+
Works for plain issues, which list_pull_request_comments_tool does not.
|
|
121
|
+
On a pull request it returns only the conversation comments; diff
|
|
122
|
+
comments and reviews come from list_pull_request_comments_tool.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
repo: Repository name
|
|
126
|
+
issue_number: Issue or pull request number
|
|
127
|
+
owner: Repository owner (defaults to GITHUB_DEFAULT_ORG if unset)
|
|
128
|
+
per_page: Results per page, max 100 (default: 30)
|
|
129
|
+
page: Page number (default: 1)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
{"issue_number", "items", "pagination"?}; each item carries id,
|
|
133
|
+
author, author_association, created_at, updated_at, body, html_url
|
|
134
|
+
"""
|
|
135
|
+
return await list_issue_comments(
|
|
136
|
+
owner=owner,
|
|
137
|
+
repo=repo,
|
|
138
|
+
issue_number=issue_number,
|
|
139
|
+
per_page=per_page,
|
|
140
|
+
page=page,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
108
144
|
@mcp.tool()
|
|
109
145
|
async def create_issue_tool(
|
|
110
146
|
repo: str,
|
|
@@ -222,7 +258,7 @@ async def list_pull_requests_tool(
|
|
|
222
258
|
@mcp.tool(annotations=READ_ONLY)
|
|
223
259
|
async def get_pull_request_tool(
|
|
224
260
|
repo: str,
|
|
225
|
-
pull_number: int,
|
|
261
|
+
pull_number: Annotated[int, Field(ge=1)],
|
|
226
262
|
owner: str | None = None,
|
|
227
263
|
) -> dict[str, Any]:
|
|
228
264
|
"""Get a single GitHub pull request by number.
|
|
@@ -241,7 +277,7 @@ async def get_pull_request_tool(
|
|
|
241
277
|
@mcp.tool(annotations=READ_ONLY)
|
|
242
278
|
async def get_pull_request_diff_tool(
|
|
243
279
|
repo: str,
|
|
244
|
-
pull_number: int,
|
|
280
|
+
pull_number: Annotated[int, Field(ge=1)],
|
|
245
281
|
owner: str | None = None,
|
|
246
282
|
) -> dict[str, Any]:
|
|
247
283
|
"""Get the unified diff for a GitHub pull request.
|
|
@@ -260,7 +296,7 @@ async def get_pull_request_diff_tool(
|
|
|
260
296
|
@mcp.tool(annotations=READ_ONLY)
|
|
261
297
|
async def get_pull_request_checks_tool(
|
|
262
298
|
repo: str,
|
|
263
|
-
pull_number: int,
|
|
299
|
+
pull_number: Annotated[int, Field(ge=1)],
|
|
264
300
|
owner: str | None = None,
|
|
265
301
|
) -> dict[str, Any]:
|
|
266
302
|
"""Get a CI verdict for a PR that distinguishes skipped from failed.
|
|
@@ -399,7 +435,7 @@ async def merge_pull_request_tool(
|
|
|
399
435
|
@mcp.tool(annotations=READ_ONLY)
|
|
400
436
|
async def list_pull_request_comments_tool(
|
|
401
437
|
repo: str,
|
|
402
|
-
pull_number: int,
|
|
438
|
+
pull_number: Annotated[int, Field(ge=1)],
|
|
403
439
|
owner: str | None = None,
|
|
404
440
|
) -> dict[str, Any]:
|
|
405
441
|
"""List everything said on a GitHub pull request, in one list.
|
|
@@ -650,7 +686,7 @@ async def rerun_failed_jobs_tool(
|
|
|
650
686
|
@mcp.tool(annotations=READ_ONLY)
|
|
651
687
|
async def get_workflow_run_tool(
|
|
652
688
|
repo: str,
|
|
653
|
-
run_id: int,
|
|
689
|
+
run_id: Annotated[int, Field(ge=1)],
|
|
654
690
|
owner: str | None = None,
|
|
655
691
|
) -> dict[str, Any]:
|
|
656
692
|
"""Get one GitHub Actions workflow run and its jobs (no logs).
|
|
@@ -670,7 +706,7 @@ async def get_workflow_run_tool(
|
|
|
670
706
|
@mcp.tool(annotations=READ_ONLY)
|
|
671
707
|
async def get_failed_job_logs_tool(
|
|
672
708
|
repo: str,
|
|
673
|
-
run_id: int,
|
|
709
|
+
run_id: Annotated[int, Field(ge=1)],
|
|
674
710
|
tail_lines: int = 200,
|
|
675
711
|
owner: str | None = None,
|
|
676
712
|
) -> dict[str, Any]:
|
|
@@ -16,6 +16,7 @@ from .issues import (
|
|
|
16
16
|
create_issue,
|
|
17
17
|
create_issue_comment,
|
|
18
18
|
get_issue,
|
|
19
|
+
list_issue_comments,
|
|
19
20
|
list_issues,
|
|
20
21
|
update_issue,
|
|
21
22
|
)
|
|
@@ -45,6 +46,7 @@ __all__ = [
|
|
|
45
46
|
"get_pull_request_checks",
|
|
46
47
|
"get_pull_request_diff",
|
|
47
48
|
"get_workflow_run",
|
|
49
|
+
"list_issue_comments",
|
|
48
50
|
"list_issues",
|
|
49
51
|
"list_pull_request_comments",
|
|
50
52
|
"list_pull_requests",
|
|
@@ -88,6 +88,62 @@ async def get_issue(
|
|
|
88
88
|
return await client.get(f"/repos/{owner}/{repo}/issues/{issue_number}")
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
async def list_issue_comments(
|
|
92
|
+
owner: str | None,
|
|
93
|
+
repo: str,
|
|
94
|
+
issue_number: int,
|
|
95
|
+
per_page: int = 30,
|
|
96
|
+
page: int = 1,
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
"""List the comments on an issue or a pull request conversation, oldest first.
|
|
99
|
+
|
|
100
|
+
Diff comments and reviews on a pull request are not included; use
|
|
101
|
+
list_pull_request_comments for those.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
owner: Repository owner (defaults to GITHUB_DEFAULT_ORG if unset)
|
|
105
|
+
repo: Repository name
|
|
106
|
+
issue_number: Issue or pull request number
|
|
107
|
+
per_page: Results per page (max 100)
|
|
108
|
+
page: Page number
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
{"issue_number", "items", "pagination"?}; each item carries id,
|
|
112
|
+
author, author_association, created_at, updated_at, body, html_url
|
|
113
|
+
"""
|
|
114
|
+
owner = resolve_owner(owner)
|
|
115
|
+
repo = validate_name(repo, "repo")
|
|
116
|
+
issue_number = validate_positive_int(issue_number, "issue_number")
|
|
117
|
+
|
|
118
|
+
client = get_client()
|
|
119
|
+
result = await client.get(
|
|
120
|
+
f"/repos/{owner}/{repo}/issues/{issue_number}/comments",
|
|
121
|
+
params={
|
|
122
|
+
"per_page": clamp_per_page(per_page),
|
|
123
|
+
"page": validate_positive_int(page, "page"),
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
response: dict[str, Any] = {
|
|
128
|
+
"issue_number": issue_number,
|
|
129
|
+
"items": [
|
|
130
|
+
{
|
|
131
|
+
"id": raw.get("id"),
|
|
132
|
+
"author": (raw.get("user") or {}).get("login"),
|
|
133
|
+
"author_association": raw.get("author_association"),
|
|
134
|
+
"created_at": raw.get("created_at"),
|
|
135
|
+
"updated_at": raw.get("updated_at"),
|
|
136
|
+
"body": raw.get("body") or "",
|
|
137
|
+
"html_url": raw.get("html_url"),
|
|
138
|
+
}
|
|
139
|
+
for raw in result.get("items", [])
|
|
140
|
+
],
|
|
141
|
+
}
|
|
142
|
+
if "pagination" in result:
|
|
143
|
+
response["pagination"] = result["pagination"]
|
|
144
|
+
return response
|
|
145
|
+
|
|
146
|
+
|
|
91
147
|
async def create_issue(
|
|
92
148
|
owner: str | None,
|
|
93
149
|
repo: str,
|
|
@@ -169,9 +225,7 @@ async def update_issue(
|
|
|
169
225
|
json_data["state"] = state
|
|
170
226
|
if state_reason is not None:
|
|
171
227
|
if state_reason not in ("completed", "not_planned", "reopened"):
|
|
172
|
-
raise ValidationError(
|
|
173
|
-
"state_reason must be 'completed', 'not_planned', or 'reopened'"
|
|
174
|
-
)
|
|
228
|
+
raise ValidationError("state_reason must be 'completed', 'not_planned', or 'reopened'")
|
|
175
229
|
json_data["state_reason"] = state_reason
|
|
176
230
|
if title is not None:
|
|
177
231
|
json_data["title"] = title
|
|
@@ -728,7 +728,19 @@ async def list_pull_request_comments(
|
|
|
728
728
|
items: list[dict[str, Any]] = []
|
|
729
729
|
truncated_kinds: list[str] = []
|
|
730
730
|
for kind, path in sources.items():
|
|
731
|
-
|
|
731
|
+
try:
|
|
732
|
+
raw_items, cut = await _collect_pages(client, path)
|
|
733
|
+
except (GitHubApiError, NotFoundError):
|
|
734
|
+
# GitHub answers /pulls/{n}/... for a plain issue with a 403 that
|
|
735
|
+
# reads like a token scope problem. Say what is actually wrong.
|
|
736
|
+
if kind != "issue_comment":
|
|
737
|
+
issue = await client.get(f"/repos/{owner}/{repo}/issues/{pull_number}")
|
|
738
|
+
if "pull_request" not in issue:
|
|
739
|
+
raise ValidationError(
|
|
740
|
+
f"#{pull_number} is an issue, not a pull request; "
|
|
741
|
+
"use list_issue_comments_tool"
|
|
742
|
+
) from None
|
|
743
|
+
raise
|
|
732
744
|
items.extend(_discussion_item(kind, raw) for raw in raw_items)
|
|
733
745
|
if cut:
|
|
734
746
|
truncated_kinds.append(kind)
|
|
@@ -38,16 +38,17 @@ class TestToolRegistration:
|
|
|
38
38
|
assert callable(func), f"{name} is not callable"
|
|
39
39
|
|
|
40
40
|
def test_tool_count(self) -> None:
|
|
41
|
-
"""Server should export exactly
|
|
41
|
+
"""Server should export exactly 26 tools."""
|
|
42
42
|
from mcp_github_crunchtools.tools import __all__
|
|
43
43
|
|
|
44
|
-
assert len(__all__) ==
|
|
44
|
+
assert len(__all__) == 26
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
READ_ONLY = frozenset(
|
|
48
48
|
{
|
|
49
49
|
"list_issues_tool",
|
|
50
50
|
"get_issue_tool",
|
|
51
|
+
"list_issue_comments_tool",
|
|
51
52
|
"list_pull_requests_tool",
|
|
52
53
|
"get_pull_request_tool",
|
|
53
54
|
"get_pull_request_diff_tool",
|
|
@@ -82,6 +83,7 @@ WRITES = frozenset(
|
|
|
82
83
|
READ_ONLY_CALLS: dict[str, dict[str, object]] = {
|
|
83
84
|
"list_issues_tool": {"owner": "o", "repo": "r"},
|
|
84
85
|
"get_issue_tool": {"owner": "o", "repo": "r", "issue_number": 1},
|
|
86
|
+
"list_issue_comments_tool": {"owner": "o", "repo": "r", "issue_number": 1},
|
|
85
87
|
"list_pull_requests_tool": {"owner": "o", "repo": "r"},
|
|
86
88
|
"get_pull_request_tool": {"owner": "o", "repo": "r", "pull_number": 1},
|
|
87
89
|
"get_pull_request_diff_tool": {"owner": "o", "repo": "r", "pull_number": 1},
|
|
@@ -140,6 +142,20 @@ class TestReadOnlyAnnotation:
|
|
|
140
142
|
def test_every_read_only_tool_has_call_arguments(self) -> None:
|
|
141
143
|
assert set(READ_ONLY_CALLS) == READ_ONLY
|
|
142
144
|
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_read_tool_integer_ids_publish_minimum_one(self) -> None:
|
|
147
|
+
from mcp_github_crunchtools.server import mcp
|
|
148
|
+
|
|
149
|
+
found = 0
|
|
150
|
+
for tool in await mcp.list_tools():
|
|
151
|
+
if tool.name not in READ_ONLY:
|
|
152
|
+
continue
|
|
153
|
+
for field in ("issue_number", "pull_number", "run_id"):
|
|
154
|
+
if field in tool.parameters["properties"]:
|
|
155
|
+
found += 1
|
|
156
|
+
assert tool.parameters["properties"][field]["minimum"] == 1
|
|
157
|
+
assert found == 8
|
|
158
|
+
|
|
143
159
|
@pytest.mark.asyncio
|
|
144
160
|
@pytest.mark.parametrize("name", sorted(READ_ONLY))
|
|
145
161
|
async def test_read_only_tool_sends_only_get(self, name: str) -> None:
|
|
@@ -384,6 +400,69 @@ class TestIssueTools:
|
|
|
384
400
|
|
|
385
401
|
assert result["body"] == "Thanks!"
|
|
386
402
|
|
|
403
|
+
@pytest.mark.asyncio
|
|
404
|
+
async def test_list_issue_comments(self) -> None:
|
|
405
|
+
from mcp_github_crunchtools.tools import list_issue_comments
|
|
406
|
+
|
|
407
|
+
resp = _mock_response(
|
|
408
|
+
json_data=[
|
|
409
|
+
{
|
|
410
|
+
"id": 7,
|
|
411
|
+
"user": {"login": "alice"},
|
|
412
|
+
"author_association": "MEMBER",
|
|
413
|
+
"created_at": "2026-10-01T10:00:00Z",
|
|
414
|
+
"updated_at": "2026-10-01T10:05:00Z",
|
|
415
|
+
"body": "Repro attached.",
|
|
416
|
+
"html_url": "https://github.com/o/r/issues/42#issuecomment-7",
|
|
417
|
+
"reactions": {"dropped": True},
|
|
418
|
+
},
|
|
419
|
+
{"id": 8, "user": None, "body": None},
|
|
420
|
+
],
|
|
421
|
+
headers={"link": '<https://api.github.com/x?page=3>; rel="next"'},
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
with _patch_client(resp) as mock_client:
|
|
425
|
+
result = await list_issue_comments(
|
|
426
|
+
owner="o", repo="r", issue_number=42, per_page=500, page=2
|
|
427
|
+
)
|
|
428
|
+
call = mock_client.return_value.request.call_args
|
|
429
|
+
assert call.kwargs["method"] == "GET"
|
|
430
|
+
assert call.kwargs["url"] == "/repos/o/r/issues/42/comments"
|
|
431
|
+
assert call.kwargs["params"] == {"per_page": 100, "page": 2}
|
|
432
|
+
|
|
433
|
+
assert result["issue_number"] == 42
|
|
434
|
+
assert result["pagination"]["next_page"] == 3
|
|
435
|
+
assert result["items"][0] == {
|
|
436
|
+
"id": 7,
|
|
437
|
+
"author": "alice",
|
|
438
|
+
"author_association": "MEMBER",
|
|
439
|
+
"created_at": "2026-10-01T10:00:00Z",
|
|
440
|
+
"updated_at": "2026-10-01T10:05:00Z",
|
|
441
|
+
"body": "Repro attached.",
|
|
442
|
+
"html_url": "https://github.com/o/r/issues/42#issuecomment-7",
|
|
443
|
+
}
|
|
444
|
+
assert result["items"][1]["author"] is None
|
|
445
|
+
assert result["items"][1]["body"] == ""
|
|
446
|
+
|
|
447
|
+
@pytest.mark.asyncio
|
|
448
|
+
async def test_list_issue_comments_refuses_bad_number(self) -> None:
|
|
449
|
+
from mcp_github_crunchtools.tools import list_issue_comments
|
|
450
|
+
|
|
451
|
+
with _patch_client(_mock_response(json_data=[])) as mock_client:
|
|
452
|
+
with pytest.raises(ValueError, match="positive integer"):
|
|
453
|
+
await list_issue_comments(owner="o", repo="r", issue_number=0)
|
|
454
|
+
mock_client.return_value.request.assert_not_awaited()
|
|
455
|
+
|
|
456
|
+
@pytest.mark.asyncio
|
|
457
|
+
async def test_list_issue_comments_not_found(self) -> None:
|
|
458
|
+
from mcp_github_crunchtools.errors import NotFoundError
|
|
459
|
+
from mcp_github_crunchtools.tools import list_issue_comments
|
|
460
|
+
|
|
461
|
+
resp = _mock_response(status_code=404, json_data={"message": "Not Found"})
|
|
462
|
+
|
|
463
|
+
with _patch_client(resp), pytest.raises(NotFoundError):
|
|
464
|
+
await list_issue_comments(owner="o", repo="r", issue_number=42)
|
|
465
|
+
|
|
387
466
|
@pytest.mark.asyncio
|
|
388
467
|
async def test_create_issue(self) -> None:
|
|
389
468
|
from mcp_github_crunchtools.tools import create_issue
|
|
@@ -902,6 +981,40 @@ class TestPullRequestTools:
|
|
|
902
981
|
with _patch_client(resp), pytest.raises(NotFoundError):
|
|
903
982
|
await list_pull_request_comments(owner="o", repo="r", pull_number=5)
|
|
904
983
|
|
|
984
|
+
@pytest.mark.asyncio
|
|
985
|
+
async def test_list_pull_request_comments_on_an_issue(self) -> None:
|
|
986
|
+
"""GitHub's 403 for /pulls/{n} on an issue becomes a clear error."""
|
|
987
|
+
from mcp_github_crunchtools.errors import ValidationError
|
|
988
|
+
from mcp_github_crunchtools.tools import list_pull_request_comments
|
|
989
|
+
|
|
990
|
+
forbidden = _mock_response(
|
|
991
|
+
status_code=403,
|
|
992
|
+
json_data={"message": "Resource not accessible by personal access token"},
|
|
993
|
+
)
|
|
994
|
+
issue = _mock_response(json_data={"number": 5, "title": "A plain issue"})
|
|
995
|
+
|
|
996
|
+
with (
|
|
997
|
+
_patch_client_sequence(_mock_response(json_data=[]), forbidden, issue) as mock_client,
|
|
998
|
+
pytest.raises(ValidationError, match="#5 is an issue, not a pull request"),
|
|
999
|
+
):
|
|
1000
|
+
await list_pull_request_comments(owner="o", repo="r", pull_number=5)
|
|
1001
|
+
calls = mock_client.return_value.request.call_args_list
|
|
1002
|
+
assert calls[-1].kwargs["url"] == "/repos/o/r/issues/5"
|
|
1003
|
+
|
|
1004
|
+
@pytest.mark.asyncio
|
|
1005
|
+
async def test_list_pull_request_comments_403_on_a_pr_passes_through(self) -> None:
|
|
1006
|
+
from mcp_github_crunchtools.errors import GitHubApiError
|
|
1007
|
+
from mcp_github_crunchtools.tools import list_pull_request_comments
|
|
1008
|
+
|
|
1009
|
+
forbidden = _mock_response(status_code=403, json_data={"message": "Forbidden"})
|
|
1010
|
+
pull = _mock_response(json_data={"number": 5, "pull_request": {"url": "x"}})
|
|
1011
|
+
|
|
1012
|
+
with (
|
|
1013
|
+
_patch_client_sequence(_mock_response(json_data=[]), forbidden, pull),
|
|
1014
|
+
pytest.raises(GitHubApiError, match="403"),
|
|
1015
|
+
):
|
|
1016
|
+
await list_pull_request_comments(owner="o", repo="r", pull_number=5)
|
|
1017
|
+
|
|
905
1018
|
@pytest.mark.asyncio
|
|
906
1019
|
async def test_reply_to_review_comment(self) -> None:
|
|
907
1020
|
from mcp_github_crunchtools.tools import reply_to_review_comment
|
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/ISSUE_TEMPLATE/config.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/ashigaru.yml
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/constitution.yml
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/container.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/gatehouse.yml
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.github/workflows/security.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/memory/constitution.md
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/specs/000-baseline/spec.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/templates/plan-template.md
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/.specify/templates/spec-template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/__main__.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/client.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/config.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/errors.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.1 → mcp_github_crunchtools-1.4.0}/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
|