mcp-github-crunchtools 1.3.2__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.2 → mcp_github_crunchtools-1.4.0}/CHANGELOG.md +15 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/CLAUDE.md +3 -3
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/Containerfile +1 -1
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/PKG-INFO +8 -4
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/README.md +7 -3
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/pyproject.toml +1 -1
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/server.json +2 -2
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/__init__.py +1 -1
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/server.py +36 -1
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/__init__.py +2 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/issues.py +57 -3
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/pull_requests.py +13 -1
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/tests/test_tools.py +102 -3
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/uv.lock +1 -1
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.gatehouse-ignore +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/dependabot.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/ashigaru.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/ci.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/constitution.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/container.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/dependabot-automerge.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/gatehouse-retriage.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/gatehouse.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/publish.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/security.yml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.gitignore +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.pre-commit-config.yaml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.specify/memory/constitution.md +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.specify/specs/000-baseline/spec.md +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.specify/specs/001-pr-merge-logs-release/spec.md +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.specify/specs/002-request-token-merge-gate/spec.md +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.specify/templates/plan-template.md +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.specify/templates/spec-template.md +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/LICENSE +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/SECURITY.md +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/gourmand-exceptions.toml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/gourmand.toml +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/__main__.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/client.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/config.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/errors.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/models.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/actions.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/files.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/releases.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/tools/search.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/tests/__init__.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/tests/conftest.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/tests/test_merge_gate.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/tests/test_request_token.py +0 -0
- {mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/tests/test_validation.py +0 -0
|
@@ -6,6 +6,21 @@ 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
|
+
|
|
9
24
|
## [1.3.2] - 2026-10-17
|
|
10
25
|
|
|
11
26
|
### 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.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/server.py
RENAMED
|
@@ -21,6 +21,7 @@ from .tools import (
|
|
|
21
21
|
get_pull_request_checks,
|
|
22
22
|
get_pull_request_diff,
|
|
23
23
|
get_workflow_run,
|
|
24
|
+
list_issue_comments,
|
|
24
25
|
list_issues,
|
|
25
26
|
list_pull_request_comments,
|
|
26
27
|
list_pull_requests,
|
|
@@ -45,7 +46,7 @@ READ_ONLY = {"readOnlyHint": True}
|
|
|
45
46
|
|
|
46
47
|
mcp = FastMCP(
|
|
47
48
|
name="mcp-github",
|
|
48
|
-
version="1.
|
|
49
|
+
version="1.4.0",
|
|
49
50
|
instructions=(
|
|
50
51
|
"Secure MCP server for GitHub repositories: issues, pull requests "
|
|
51
52
|
"(open, review discussion, diffs, CI checks, merge), Actions runs and "
|
|
@@ -106,6 +107,40 @@ async def get_issue_tool(
|
|
|
106
107
|
return await get_issue(owner=owner, repo=repo, issue_number=issue_number)
|
|
107
108
|
|
|
108
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
|
+
|
|
109
144
|
@mcp.tool()
|
|
110
145
|
async def create_issue_tool(
|
|
111
146
|
repo: str,
|
|
@@ -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},
|
|
@@ -152,7 +154,7 @@ class TestReadOnlyAnnotation:
|
|
|
152
154
|
if field in tool.parameters["properties"]:
|
|
153
155
|
found += 1
|
|
154
156
|
assert tool.parameters["properties"][field]["minimum"] == 1
|
|
155
|
-
assert found ==
|
|
157
|
+
assert found == 8
|
|
156
158
|
|
|
157
159
|
@pytest.mark.asyncio
|
|
158
160
|
@pytest.mark.parametrize("name", sorted(READ_ONLY))
|
|
@@ -398,6 +400,69 @@ class TestIssueTools:
|
|
|
398
400
|
|
|
399
401
|
assert result["body"] == "Thanks!"
|
|
400
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
|
+
|
|
401
466
|
@pytest.mark.asyncio
|
|
402
467
|
async def test_create_issue(self) -> None:
|
|
403
468
|
from mcp_github_crunchtools.tools import create_issue
|
|
@@ -916,6 +981,40 @@ class TestPullRequestTools:
|
|
|
916
981
|
with _patch_client(resp), pytest.raises(NotFoundError):
|
|
917
982
|
await list_pull_request_comments(owner="o", repo="r", pull_number=5)
|
|
918
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
|
+
|
|
919
1018
|
@pytest.mark.asyncio
|
|
920
1019
|
async def test_reply_to_review_comment(self) -> None:
|
|
921
1020
|
from mcp_github_crunchtools.tools import reply_to_review_comment
|
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → 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.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/ashigaru.yml
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/constitution.yml
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → 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.2 → mcp_github_crunchtools-1.4.0}/.github/workflows/gatehouse.yml
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → 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.2 → mcp_github_crunchtools-1.4.0}/.specify/memory/constitution.md
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → 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.2 → mcp_github_crunchtools-1.4.0}/.specify/templates/plan-template.md
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → 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.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/__main__.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/client.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/config.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → mcp_github_crunchtools-1.4.0}/src/mcp_github_crunchtools/errors.py
RENAMED
|
File without changes
|
{mcp_github_crunchtools-1.3.2 → 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
|