mcp-github-crunchtools 1.1.0__tar.gz → 1.3.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.0/.github/workflows/ashigaru.yml +98 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/ci.yml +1 -1
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/constitution.yml +1 -1
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/dependabot-automerge.yml +1 -1
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/gatehouse-retriage.yml +1 -1
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/gatehouse.yml +2 -2
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.pre-commit-config.yaml +1 -1
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.specify/memory/constitution.md +30 -10
- mcp_github_crunchtools-1.3.0/.specify/specs/001-pr-merge-logs-release/spec.md +141 -0
- mcp_github_crunchtools-1.3.0/.specify/specs/002-request-token-merge-gate/spec.md +132 -0
- mcp_github_crunchtools-1.3.0/CHANGELOG.md +116 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/CLAUDE.md +31 -8
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/Containerfile +3 -3
- mcp_github_crunchtools-1.3.0/PKG-INFO +432 -0
- mcp_github_crunchtools-1.3.0/README.md +406 -0
- mcp_github_crunchtools-1.3.0/SECURITY.md +362 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/pyproject.toml +2 -2
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/server.json +3 -3
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/__init__.py +5 -2
- mcp_github_crunchtools-1.3.0/src/mcp_github_crunchtools/client.py +465 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/config.py +23 -11
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/errors.py +22 -4
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/models.py +80 -14
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/server.py +252 -2
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/tools/__init__.py +14 -0
- mcp_github_crunchtools-1.3.0/src/mcp_github_crunchtools/tools/actions.py +389 -0
- mcp_github_crunchtools-1.3.0/src/mcp_github_crunchtools/tools/pull_requests.py +785 -0
- mcp_github_crunchtools-1.3.0/src/mcp_github_crunchtools/tools/releases.py +82 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/tests/conftest.py +30 -3
- mcp_github_crunchtools-1.3.0/tests/test_merge_gate.py +584 -0
- mcp_github_crunchtools-1.3.0/tests/test_request_token.py +325 -0
- mcp_github_crunchtools-1.3.0/tests/test_tools.py +1949 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/tests/test_validation.py +89 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/uv.lock +1 -1
- mcp_github_crunchtools-1.1.0/CHANGELOG.md +0 -51
- mcp_github_crunchtools-1.1.0/PKG-INFO +0 -263
- mcp_github_crunchtools-1.1.0/README.md +0 -237
- mcp_github_crunchtools-1.1.0/SECURITY.md +0 -219
- mcp_github_crunchtools-1.1.0/src/mcp_github_crunchtools/client.py +0 -271
- mcp_github_crunchtools-1.1.0/src/mcp_github_crunchtools/tools/actions.py +0 -187
- mcp_github_crunchtools-1.1.0/src/mcp_github_crunchtools/tools/pull_requests.py +0 -287
- mcp_github_crunchtools-1.1.0/tests/test_tools.py +0 -941
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.gatehouse-ignore +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/dependabot.yml +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/container.yml +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/publish.yml +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/security.yml +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.gitignore +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.specify/specs/000-baseline/spec.md +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.specify/templates/plan-template.md +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.specify/templates/spec-template.md +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/LICENSE +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/gourmand-exceptions.toml +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/gourmand.toml +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/__main__.py +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/tools/files.py +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/tools/issues.py +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/tools/search.py +0 -0
- {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Ashigaru — issues in, fixed bugs out. ONE file enrolls a repo.
|
|
2
|
+
#
|
|
3
|
+
# Drop this in at .github/workflows/ashigaru.yml. The organization supplies the
|
|
4
|
+
# secrets (CLAUDE_CODE_OAUTH_TOKEN, ASHIGARU_APP_ID, ASHIGARU_APP_KEY,
|
|
5
|
+
# OPENROUTER_API_KEY) and the switch (variable ASHIGARU_ENABLED = true).
|
|
6
|
+
#
|
|
7
|
+
# What happens:
|
|
8
|
+
# • a new issue is triaged: bug, documentation and performance issues are
|
|
9
|
+
# labeled ready-to-code and fixed; features are labeled triaged and left
|
|
10
|
+
# for you; unclear ones get needs-info
|
|
11
|
+
# • an issue from someone without write access is triaged too, but is never
|
|
12
|
+
# fixed until a maintainer adds ready-to-code
|
|
13
|
+
# • the fix arrives as a pull request from ashigaru/issue-N with auto-merge on
|
|
14
|
+
# • Gatehouse reviews it like any other; Ashigaru answers each finding
|
|
15
|
+
# (`fixed in <sha>` or `not a bug: <reason>`) for up to five rounds, then
|
|
16
|
+
# labels the pull request needs-human and stops
|
|
17
|
+
# • GitHub merges when the required checks pass. No job here can merge.
|
|
18
|
+
#
|
|
19
|
+
# To steer it by hand: add `ready-to-code` to have an issue fixed,
|
|
20
|
+
# `ready-for-triage` to have it looked at again.
|
|
21
|
+
#
|
|
22
|
+
# Where the checks are. This file only routes events. The workflows it calls
|
|
23
|
+
# do the checking, at the pinned release, before any agent runs:
|
|
24
|
+
# • the switch: every called workflow stops at its first job unless the
|
|
25
|
+
# organization variable ASHIGARU_ENABLED is true
|
|
26
|
+
# • who may reach an agent: scripts/guard.py asks the API for the write
|
|
27
|
+
# access of the issue's author and of whoever triggered the event
|
|
28
|
+
# • which pull requests are touched: fix.yml acts only on ashigaru/ branches
|
|
29
|
+
# in this repository, opened by the Ashigaru app
|
|
30
|
+
# • what the secrets can do: the job that runs the model gets a read-only
|
|
31
|
+
# token; the app key is used only by jobs that run no model
|
|
32
|
+
#
|
|
33
|
+
# Copy unchanged apart from the pins, which Dependabot keeps current.
|
|
34
|
+
|
|
35
|
+
name: Ashigaru
|
|
36
|
+
|
|
37
|
+
on:
|
|
38
|
+
issues:
|
|
39
|
+
types: [opened, labeled]
|
|
40
|
+
workflow_run:
|
|
41
|
+
workflows: [Gatehouse]
|
|
42
|
+
types: [completed]
|
|
43
|
+
|
|
44
|
+
permissions: {}
|
|
45
|
+
|
|
46
|
+
# One run at a time per issue or per pull request branch; a second event waits.
|
|
47
|
+
concurrency:
|
|
48
|
+
group: ashigaru-${{ github.event.issue.number || github.event.workflow_run.head_branch }}
|
|
49
|
+
cancel-in-progress: false
|
|
50
|
+
|
|
51
|
+
jobs:
|
|
52
|
+
triage:
|
|
53
|
+
name: Triage
|
|
54
|
+
if: >-
|
|
55
|
+
github.event_name == 'issues' &&
|
|
56
|
+
(github.event.action == 'opened' || github.event.label.name == 'ready-for-triage')
|
|
57
|
+
permissions:
|
|
58
|
+
contents: read
|
|
59
|
+
issues: write
|
|
60
|
+
uses: crunchtools/ashigaru/.github/workflows/triage.yml@v2
|
|
61
|
+
secrets:
|
|
62
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
63
|
+
|
|
64
|
+
code:
|
|
65
|
+
name: Code
|
|
66
|
+
needs: triage
|
|
67
|
+
# After a triage that said ready-to-code, or when a maintainer adds the label.
|
|
68
|
+
if: >-
|
|
69
|
+
always() && github.event_name == 'issues' &&
|
|
70
|
+
(needs.triage.outputs.auto_code == 'true' ||
|
|
71
|
+
(github.event.action == 'labeled' && github.event.label.name == 'ready-to-code'))
|
|
72
|
+
permissions:
|
|
73
|
+
contents: read
|
|
74
|
+
issues: read
|
|
75
|
+
uses: crunchtools/ashigaru/.github/workflows/code.yml@v2
|
|
76
|
+
secrets:
|
|
77
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
78
|
+
ASHIGARU_APP_ID: ${{ secrets.ASHIGARU_APP_ID }}
|
|
79
|
+
ASHIGARU_APP_KEY: ${{ secrets.ASHIGARU_APP_KEY }}
|
|
80
|
+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
81
|
+
|
|
82
|
+
fix:
|
|
83
|
+
name: Fix
|
|
84
|
+
if: >-
|
|
85
|
+
github.event_name == 'workflow_run' &&
|
|
86
|
+
github.event.workflow_run.event == 'pull_request_target' &&
|
|
87
|
+
github.event.workflow_run.head_repository.full_name == github.repository &&
|
|
88
|
+
startsWith(github.event.workflow_run.head_branch, 'ashigaru/')
|
|
89
|
+
permissions:
|
|
90
|
+
contents: read
|
|
91
|
+
pull-requests: read
|
|
92
|
+
checks: read
|
|
93
|
+
actions: read
|
|
94
|
+
uses: crunchtools/ashigaru/.github/workflows/fix.yml@v2
|
|
95
|
+
secrets:
|
|
96
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
97
|
+
ASHIGARU_APP_ID: ${{ secrets.ASHIGARU_APP_ID }}
|
|
98
|
+
ASHIGARU_APP_KEY: ${{ secrets.ASHIGARU_APP_KEY }}
|
|
@@ -20,4 +20,4 @@ jobs:
|
|
|
20
20
|
if: github.event.workflow_run.event == 'pull_request_review_comment'
|
|
21
21
|
permissions:
|
|
22
22
|
actions: write # re-run the triage job; nothing else
|
|
23
|
-
uses: crunchtools/gatehouse/.github/workflows/retriage.yml@v0
|
|
23
|
+
uses: crunchtools/gatehouse/.github/workflows/retriage.yml@v0
|
{mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/gatehouse.yml
RENAMED
|
@@ -90,7 +90,7 @@ jobs:
|
|
|
90
90
|
permissions:
|
|
91
91
|
contents: read
|
|
92
92
|
pull-requests: write
|
|
93
|
-
uses: crunchtools/gatehouse/.github/workflows/review.yml@v0
|
|
93
|
+
uses: crunchtools/gatehouse/.github/workflows/review.yml@v0
|
|
94
94
|
# Advisory by default — findings post as comments, the check always passes.
|
|
95
95
|
# Uncomment to let critical/high findings fail the check (still not required):
|
|
96
96
|
# with:
|
|
@@ -107,4 +107,4 @@ jobs:
|
|
|
107
107
|
if: always()
|
|
108
108
|
permissions:
|
|
109
109
|
pull-requests: read
|
|
110
|
-
uses: crunchtools/gatehouse/.github/workflows/triage.yml@v0
|
|
110
|
+
uses: crunchtools/gatehouse/.github/workflows/triage.yml@v0
|
{mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.specify/memory/constitution.md
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# mcp-github-crunchtools Constitution
|
|
2
2
|
|
|
3
|
-
> **Version:** 1.
|
|
3
|
+
> **Version:** 1.3.0
|
|
4
4
|
> **Ratified:** 2026-06-14
|
|
5
|
-
> **Amended:** 2026-10-
|
|
5
|
+
> **Amended:** 2026-10-14
|
|
6
6
|
> **Status:** Active
|
|
7
|
-
> **Inherits:** [crunchtools/constitution](https://github.com/crunchtools/constitution) v1.
|
|
7
|
+
> **Inherits:** [crunchtools/constitution](https://github.com/crunchtools/constitution) v1.24.0
|
|
8
8
|
> **Profile:** MCP Server
|
|
9
9
|
|
|
10
10
|
This file holds what is specific to mcp-github. The fleet rules and the MCP
|
|
@@ -15,17 +15,34 @@ restated here.
|
|
|
15
15
|
|
|
16
16
|
## Security Model Specifics
|
|
17
17
|
|
|
18
|
-
- **Credentials:** `GITHUB_TOKEN
|
|
19
|
-
|
|
20
|
-
`
|
|
18
|
+
- **Credentials:** `GITHUB_TOKEN`, held as `SecretStr`, read from the
|
|
19
|
+
environment only; required unless `GITHUB_ACCEPT_REQUEST_TOKEN` is on, in
|
|
20
|
+
which case an HTTP request's `Authorization: Bearer <token>` is used for
|
|
21
|
+
that request (strict format, malformed refused, never stored on the shared
|
|
22
|
+
client). `GitHubApiError` scrubs both tokens from messages, and
|
|
23
|
+
`Config.__repr__()`/`__str__()` never expose the token.
|
|
24
|
+
- **Merge gate:** `merge_pull_request` merges only when every required
|
|
25
|
+
status check of the base branch (rulesets and classic protection) passes
|
|
26
|
+
on the head commit, pinned to that head; it fails closed when checks
|
|
27
|
+
cannot be read, and when none are required unless the caller passes
|
|
28
|
+
`allow_no_required_checks=True`.
|
|
21
29
|
- **Input limits:** Pydantic models with `extra="forbid"`; issue and PR
|
|
22
|
-
states are allowlisted (`open`, `closed`, `all`)
|
|
30
|
+
states are allowlisted (`open`, `closed`, `all`), as are merge methods
|
|
31
|
+
(`merge`, `squash`, `rebase`); owner and repo names are
|
|
23
32
|
checked for injection characters; git refs follow check-ref-format
|
|
24
|
-
rules (no `..`, `//` or leading/trailing `.` or `/`)
|
|
33
|
+
rules (no `..`, `//` or leading/trailing `.` or `/`); an expected head SHA
|
|
34
|
+
is a full 40 or 64 character hex SHA. `NotFoundError`
|
|
25
35
|
truncates long identifiers.
|
|
26
36
|
- **API:** `Authorization: Bearer` header, never the URL; the
|
|
27
37
|
`X-GitHub-Api-Version` header is pinned to `2022-11-28`; path parameters
|
|
28
38
|
are URL-encoded; requests time out after 30s; responses are capped at 10MB.
|
|
39
|
+
- **Redirects:** the API client follows none. A job log, which GitHub serves
|
|
40
|
+
as a redirect to a pre-signed storage URL, is fetched by a separate client
|
|
41
|
+
that has no token, over HTTPS to a named host only, streamed, with only its
|
|
42
|
+
tail kept. The signed URL is never logged or put in an error message.
|
|
43
|
+
- **Result caps:** tools that gather several pages say when they cut:
|
|
44
|
+
300 items per kind for pull request comments, 300 jobs per run, and for
|
|
45
|
+
failed-job logs 1000 lines per job, 5 jobs and 256 KiB per result.
|
|
29
46
|
- **Surface:** pure API wrappers. No filesystem access, shell execution or
|
|
30
47
|
code evaluation.
|
|
31
48
|
|
|
@@ -42,8 +59,9 @@ The server works with github.com and GitHub Enterprise Server:
|
|
|
42
59
|
|
|
43
60
|
## Tool Groups
|
|
44
61
|
|
|
45
|
-
Issues, pull requests, Actions, files and search
|
|
46
|
-
tested for 401, 404, 429 and 204
|
|
62
|
+
Issues, pull requests, Actions, releases, files and search: 25 tools, of which
|
|
63
|
+
14 only read. Client error handling is tested for 401, 404, 429 and 204
|
|
64
|
+
responses in `TestClientErrorHandling`.
|
|
47
65
|
|
|
48
66
|
## Instance
|
|
49
67
|
|
|
@@ -62,3 +80,5 @@ tested for 401, 404, 429 and 204 responses in `TestClientErrorHandling`.
|
|
|
62
80
|
| 1.0.0 | 2026-06-14 | Initial constitution (ported from mcp-gitlab-crunchtools) |
|
|
63
81
|
| 1.0.1 | 2026-09-25 | Inherit constitution v1.17.0 (Gatehouse gates) |
|
|
64
82
|
| 1.1.0 | 2026-10-02 | Manifest under constitution v1.18.0: profile restatement removed, mcp-github specifics kept; `..` rule moved from file paths to git refs to match the code |
|
|
83
|
+
| 1.2.0 | 2026-10-12 | Seven tools for pull requests, Actions runs and logs, and releases (spec 001): redirect handling, result caps, merge-method and SHA validation, releases tool group |
|
|
84
|
+
| 1.3.0 | 2026-10-14 | Per-request token (`GITHUB_ACCEPT_REQUEST_TOKEN`) and the merge gate (spec 002) |
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Specification: Pull Request, Run Log and Release Tools
|
|
2
|
+
|
|
3
|
+
> **Spec ID:** 001-pr-merge-logs-release
|
|
4
|
+
> **Status:** Implemented
|
|
5
|
+
> **Version:** 1.2.0
|
|
6
|
+
> **Author:** crunchtools.com
|
|
7
|
+
> **Date:** 2026-10-12
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
An agent that takes a change from a branch to a release needs to open the pull
|
|
12
|
+
request, read what reviewers and CI said about it, answer them, merge, and
|
|
13
|
+
publish. Until now it did the missing steps with the `gh` CLI, which hands it
|
|
14
|
+
other people's text with no gateway in between. These seven tools close that
|
|
15
|
+
gap so the whole loop runs through the MCP server.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## New Tools
|
|
20
|
+
|
|
21
|
+
| Tool | Method | Endpoint | Description |
|
|
22
|
+
|------|--------|----------|-------------|
|
|
23
|
+
| `create_pull_request_tool` | POST | `/repos/{owner}/{repo}/pulls` | Open a pull request (write) |
|
|
24
|
+
| `merge_pull_request_tool` | PUT | `/repos/{owner}/{repo}/pulls/{pull_number}/merge` | Merge, squash or rebase; optional expected head SHA (write) |
|
|
25
|
+
| `list_pull_request_comments_tool` | GET | `.../issues/{n}/comments` + `.../pulls/{n}/comments` + `.../pulls/{n}/reviews` | One list of everything said on a PR |
|
|
26
|
+
| `reply_to_review_comment_tool` | POST | `/repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies` | Reply in a review thread (write) |
|
|
27
|
+
| `get_workflow_run_tool` | GET | `/repos/{owner}/{repo}/actions/runs/{run_id}` + `.../jobs` | Run status and jobs, no logs |
|
|
28
|
+
| `get_failed_job_logs_tool` | GET | `.../actions/runs/{run_id}/jobs` + `.../actions/jobs/{job_id}/logs` | Tail of each failed job's log |
|
|
29
|
+
| `create_release_tool` | POST | `/repos/{owner}/{repo}/releases` | Create a release and its tag (write) |
|
|
30
|
+
|
|
31
|
+
The three GET tools publish `readOnlyHint`. Not in scope: creating or forking
|
|
32
|
+
repositories, deleting packages, further search tools.
|
|
33
|
+
|
|
34
|
+
### Result shapes and caps
|
|
35
|
+
|
|
36
|
+
| Tool | Cap | How the result reports it |
|
|
37
|
+
|------|-----|---------------------------|
|
|
38
|
+
| `list_pull_request_comments_tool` | 300 items per kind, read oldest first (at most 9 requests) | `truncated`, `truncated_kinds` |
|
|
39
|
+
| `get_workflow_run_tool` | 300 jobs | `jobs_truncated` |
|
|
40
|
+
| `get_failed_job_logs_tool` | `tail_lines` at most 1000 (default 200); 5 failed jobs; 256 KiB of log text per result, split evenly between jobs; 20 MB per download | `tail_lines` (value used), `failed_jobs_omitted`, per-job and overall `truncated` |
|
|
41
|
+
|
|
42
|
+
A job is failed when its conclusion is `failure` or `timed_out`. Cancelled
|
|
43
|
+
jobs are left out: in a failed run they are mostly siblings stopped by
|
|
44
|
+
fail-fast, and their logs say nothing about the cause.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Security Considerations
|
|
49
|
+
|
|
50
|
+
### Layer 1 — Token Protection
|
|
51
|
+
|
|
52
|
+
- GitHub serves a job log as a 302 to a pre-signed URL on a storage host. The
|
|
53
|
+
API client is created with `follow_redirects=False`; the redirect target is
|
|
54
|
+
fetched by a second client that has no default headers, so the token is not
|
|
55
|
+
sent there.
|
|
56
|
+
- The signed URL is a short-lived credential. It is not logged, not put in an
|
|
57
|
+
error message, and not kept in an exception chain.
|
|
58
|
+
|
|
59
|
+
### Layer 2 — Input Validation
|
|
60
|
+
|
|
61
|
+
- `MERGE_METHODS` allowlist; `validate_commit_sha` for the expected head;
|
|
62
|
+
`validate_head` for `branch` or `owner:branch`; `validate_ref` for base,
|
|
63
|
+
tag and target.
|
|
64
|
+
- `CreateReleaseInput` for the release title and notes. Pull request title
|
|
65
|
+
and body reuse `CreateIssueInput`, and a review reply reuses
|
|
66
|
+
`CreateIssueCommentInput`: the limits are the same.
|
|
67
|
+
- Blank optional values (`sha`, `target_commitish`, `name`) mean "not given".
|
|
68
|
+
|
|
69
|
+
### Layer 3 — API Hardening
|
|
70
|
+
|
|
71
|
+
- The redirect target must be HTTPS, name a host rather than an IP address,
|
|
72
|
+
and carry no embedded credentials. It is not redirected further and must
|
|
73
|
+
answer 200. The host is not held to a list of names: GitHub documents no
|
|
74
|
+
stable set of log-storage hosts, and GHES storage is the operator's.
|
|
75
|
+
- The download is streamed with a rolling 256 KiB tail and abandoned past
|
|
76
|
+
20 MB. Paging loops are bounded by a page count, not by what the server
|
|
77
|
+
says.
|
|
78
|
+
|
|
79
|
+
### Layer 4 — Dangerous Operation Prevention
|
|
80
|
+
|
|
81
|
+
- No filesystem, shell or code evaluation. Logs are held in memory only.
|
|
82
|
+
- Comments, reviews and logs are text written by others. The server returns
|
|
83
|
+
them as data and bounds their size; it does not interpret them.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Module Changes
|
|
88
|
+
|
|
89
|
+
### New Files
|
|
90
|
+
|
|
91
|
+
| File | Purpose |
|
|
92
|
+
|------|---------|
|
|
93
|
+
| `tools/releases.py` | `create_release` |
|
|
94
|
+
|
|
95
|
+
### Modified Files
|
|
96
|
+
|
|
97
|
+
| File | Changes |
|
|
98
|
+
|------|---------|
|
|
99
|
+
| `client.py` | `_send` split out of `_request`; `get_redirected_tail`; `follow_redirects=False` |
|
|
100
|
+
| `models.py` | `MERGE_METHODS`, `validate_head`, `validate_commit_sha`, `CreateReleaseInput` |
|
|
101
|
+
| `tools/pull_requests.py` | `create_pull_request`, `merge_pull_request`, `list_pull_request_comments`, `reply_to_review_comment` |
|
|
102
|
+
| `tools/actions.py` | `get_workflow_run`, `get_failed_job_logs` |
|
|
103
|
+
| `tools/__init__.py` | Exports |
|
|
104
|
+
| `server.py` | Seven `@mcp.tool` wrappers |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Testing Requirements
|
|
109
|
+
|
|
110
|
+
### Mocked API Tests
|
|
111
|
+
|
|
112
|
+
- [x] Happy path, validation refusal and GitHub error path for each tool
|
|
113
|
+
- [x] Caps: comments per kind, jobs per run, `tail_lines`, bytes, failed jobs
|
|
114
|
+
- [x] The storage request carries no `Authorization` header (real httpx
|
|
115
|
+
client over a mock transport, so the assertion is on the request as sent)
|
|
116
|
+
- [x] Redirects that are not HTTPS, carry credentials or address an IP are
|
|
117
|
+
refused; storage errors and transport failures do not expose the
|
|
118
|
+
signed URL
|
|
119
|
+
|
|
120
|
+
### Input Validation Tests
|
|
121
|
+
|
|
122
|
+
- [x] `validate_head`, `validate_commit_sha`, `CreateReleaseInput`
|
|
123
|
+
|
|
124
|
+
### Tool Count Update
|
|
125
|
+
|
|
126
|
+
- [x] `test_tool_count` is 25; `READ_ONLY` and `WRITES` updated
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Dependencies
|
|
131
|
+
|
|
132
|
+
- Depends on: 000-baseline
|
|
133
|
+
- Blocks: none
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Changelog
|
|
138
|
+
|
|
139
|
+
| Version | Date | Changes |
|
|
140
|
+
|---------|------|---------|
|
|
141
|
+
| 1.2.0 | 2026-10-12 | Implemented |
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Specification: Per-Request Token and Merge Gate
|
|
2
|
+
|
|
3
|
+
> **Spec ID:** 002-request-token-merge-gate
|
|
4
|
+
> **Status:** Implemented
|
|
5
|
+
> **Version:** 1.3.0
|
|
6
|
+
> **Author:** crunchtools.com
|
|
7
|
+
> **Date:** 2026-10-14
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Two changes, both about whose authority a call carries.
|
|
12
|
+
|
|
13
|
+
1. **Per-request token.** GitHub's hosted MCP server takes the caller's
|
|
14
|
+
personal access token as `Authorization: Bearer <token>` on every HTTP
|
|
15
|
+
request and acts with it. Behind an opt-in, this server does the same, so
|
|
16
|
+
it can stand in for GitHub's server without the client changing.
|
|
17
|
+
2. **Merge gate.** The token an agent merges with may belong to an
|
|
18
|
+
organization owner, and owners can bypass ruleset-required checks: GitHub
|
|
19
|
+
would merge a pull request whose checks failed. `merge_pull_request_tool`
|
|
20
|
+
now enforces the required checks itself.
|
|
21
|
+
|
|
22
|
+
No tools are added; the count stays at 25.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Per-Request Token
|
|
27
|
+
|
|
28
|
+
| Setting | Incoming `Authorization` | Token used |
|
|
29
|
+
|---------|-------------------------|------------|
|
|
30
|
+
| `GITHUB_ACCEPT_REQUEST_TOKEN` off (default) | ignored | `GITHUB_TOKEN` (required) |
|
|
31
|
+
| on, HTTP request with `Bearer <token>` | used | the request's token |
|
|
32
|
+
| on, HTTP request without the header, or stdio | — | `GITHUB_TOKEN` |
|
|
33
|
+
| on, no header and no `GITHUB_TOKEN` | — | none: error before any API call |
|
|
34
|
+
| on, header present but malformed | refused | none: error, not forwarded |
|
|
35
|
+
|
|
36
|
+
- The header is read with `fastmcp.server.dependencies.get_http_headers(include={"authorization"})`,
|
|
37
|
+
which returns `{}` outside an HTTP request.
|
|
38
|
+
- Accepted form: `(?i:bearer) ` followed by 1 to 1024 characters in
|
|
39
|
+
`\x21`–`\x7e`, matched in full.
|
|
40
|
+
- The token is chosen in `GitHubClient._send` for each call and passed as
|
|
41
|
+
that call's header. The shared `httpx.AsyncClient` has an `Authorization`
|
|
42
|
+
default only when `GITHUB_TOKEN` is set.
|
|
43
|
+
- `errors.request_token` (a `ContextVar`) holds the request's token so that
|
|
44
|
+
`GitHubApiError` scrubs it; each MCP request runs in its own task context.
|
|
45
|
+
- The job-log second hop (`get_redirected_tail`) still sends no token.
|
|
46
|
+
|
|
47
|
+
## Merge Gate
|
|
48
|
+
|
|
49
|
+
Before `PUT /repos/{owner}/{repo}/pulls/{n}/merge`:
|
|
50
|
+
|
|
51
|
+
| Step | Request | Refuses when |
|
|
52
|
+
|------|---------|--------------|
|
|
53
|
+
| 1 | `GET .../pulls/{n}` | caller `sha` is not the head; head or base missing |
|
|
54
|
+
| 2a | `GET .../rules/branches/{base}` (paged) | — |
|
|
55
|
+
| 2b | `GET .../branches/{base}` (`protection.required_status_checks`) | any error, or `protected` with no `protection` shown |
|
|
56
|
+
| 3 | — | no required checks, unless `allow_no_required_checks=True` |
|
|
57
|
+
| 4 | `GET .../commits/{head}/check-runs` and `.../status` (paged) | a required check is missing, pending or failed |
|
|
58
|
+
| 5 | `GET .../pulls/{n}` again | head or base changed since step 1 |
|
|
59
|
+
| 6 | `PUT .../merge` with `sha` = head | (GitHub: 405, 409) |
|
|
60
|
+
|
|
61
|
+
- A required check is `(context, app id or None)`. Rulesets give
|
|
62
|
+
`integration_id`; classic protection gives `checks[].app_id`, where -1
|
|
63
|
+
means any, or bare `contexts`.
|
|
64
|
+
- The most recent check run of the context's name (highest id) decides; it
|
|
65
|
+
passes when completed with `success`, `skipped` or `neutral`. With an app
|
|
66
|
+
id, only that app's runs count. With no app id and no run, the commit
|
|
67
|
+
status of that context must be `success`.
|
|
68
|
+
- Each list is read up to 10 pages of 100; more refuses the merge.
|
|
69
|
+
- Refusals raise `MergeRefusedError` naming every offending check.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Security Considerations
|
|
74
|
+
|
|
75
|
+
- Token mix-up between concurrent callers: nothing request-specific is
|
|
76
|
+
stored on the shared client or in a global.
|
|
77
|
+
- A malformed header never falls back to `GITHUB_TOKEN`: a caller that
|
|
78
|
+
meant to use its own token does not silently act as the server.
|
|
79
|
+
- The gate fails closed on unreadable protection, missing checks, oversized
|
|
80
|
+
lists and branches without required checks.
|
|
81
|
+
- Required reviews and other rules are not re-enforced; the bypass still
|
|
82
|
+
applies to them.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Module Changes
|
|
87
|
+
|
|
88
|
+
| File | Changes |
|
|
89
|
+
|------|---------|
|
|
90
|
+
| `config.py` | `GITHUB_ACCEPT_REQUEST_TOKEN`; `token` may be None |
|
|
91
|
+
| `client.py` | `_incoming_token`, `_auth_headers`; conditional default `Authorization` |
|
|
92
|
+
| `errors.py` | `request_token`, `scrub_tokens`, `MergeRefusedError` |
|
|
93
|
+
| `tools/pull_requests.py` | Merge gate helpers; `allow_no_required_checks` |
|
|
94
|
+
| `server.py` | `allow_no_required_checks` on `merge_pull_request_tool`; docstring |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Testing Requirements
|
|
99
|
+
|
|
100
|
+
- [x] stdio uses the env token; option off ignores the header (well-formed
|
|
101
|
+
or not); option on prefers the header; header absent falls back
|
|
102
|
+
- [x] Request-token-only instance: no default `Authorization` on the client
|
|
103
|
+
- [x] No token at all: error and no HTTP request
|
|
104
|
+
- [x] Malformed headers refused and not sent: empty, no scheme, other
|
|
105
|
+
scheme, extra spaces, tab, control, DEL, non-ASCII, over-long
|
|
106
|
+
- [x] Request token scrubbed from API and transport errors
|
|
107
|
+
- [x] Five concurrent requests with different tokens each send their own
|
|
108
|
+
(real httpx client over a mock transport)
|
|
109
|
+
- [x] Gate: all green merges pinned to head; failing, pending, missing;
|
|
110
|
+
newest status of a repeated context decides; unreadable rules, check
|
|
111
|
+
runs, statuses or PR never merge; the MCP tool forwards the flag;
|
|
112
|
+
head or base moving during the gate is refused;
|
|
113
|
+
status pending/failure/error; most recent run decides; app-bound
|
|
114
|
+
rule; no checks refused and allowed by flag; flag does not waive
|
|
115
|
+
existing checks; `sha` mismatch; protection 403; classic `checks`
|
|
116
|
+
and `contexts`; rulesets and classic combined; branch name encoded;
|
|
117
|
+
check runs, statuses and rules paginated; page cap refuses
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Dependencies
|
|
122
|
+
|
|
123
|
+
- Depends on: 001-pr-merge-logs-release
|
|
124
|
+
- Blocks: none
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Changelog
|
|
129
|
+
|
|
130
|
+
| Version | Date | Changes |
|
|
131
|
+
|---------|------|---------|
|
|
132
|
+
| 1.3.0 | 2026-10-14 | Implemented |
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/) and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.3.0] - 2026-10-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `GITHUB_ACCEPT_REQUEST_TOKEN` (default off): over HTTP, act with the token
|
|
14
|
+
in each request's `Authorization: Bearer <token>` header, the way GitHub's
|
|
15
|
+
hosted MCP server takes it, falling back to `GITHUB_TOKEN`. With it on,
|
|
16
|
+
`GITHUB_TOKEN` is optional. A malformed header is refused, never
|
|
17
|
+
forwarded; a call with no token at all fails before reaching GitHub.
|
|
18
|
+
stdio is unchanged.
|
|
19
|
+
- `merge_pull_request_tool` takes `allow_no_required_checks` (default
|
|
20
|
+
False).
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `merge_pull_request_tool` enforces the base branch's required status
|
|
25
|
+
checks itself before merging: from rulesets and classic branch
|
|
26
|
+
protection, every required check must have a passing check run (success,
|
|
27
|
+
skipped or neutral) or a successful status on the head commit, or the
|
|
28
|
+
merge is refused with each offending check named. A branch with no
|
|
29
|
+
required checks is refused unless `allow_no_required_checks=True`, and a
|
|
30
|
+
branch protection that cannot be read is refused. Classic protection is
|
|
31
|
+
read from the branch itself, so no Administration permission is needed.
|
|
32
|
+
The merge is always pinned to the checked head SHA. It now needs Pull
|
|
33
|
+
requests: read and Commit statuses: read as well as Contents: write.
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- An organization owner's token can bypass required checks on GitHub's
|
|
38
|
+
side; the merge gate closes that for this tool.
|
|
39
|
+
- The request token is scrubbed from error messages like `GITHUB_TOKEN`,
|
|
40
|
+
and is passed per call, never stored on the shared HTTP client.
|
|
41
|
+
|
|
42
|
+
## [1.2.0] - 2026-10-12
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- `create_pull_request_tool`: open a pull request, optionally as a draft; the
|
|
47
|
+
head may be `owner:branch` for a fork.
|
|
48
|
+
- `merge_pull_request_tool`: merge, squash or rebase a pull request. `sha` is
|
|
49
|
+
the expected head commit; GitHub refuses the merge if the branch has moved.
|
|
50
|
+
- `list_pull_request_comments_tool`: conversation comments, diff comments and
|
|
51
|
+
reviews in one chronological list, each tagged with its kind, author and id.
|
|
52
|
+
Each kind is capped at 300 items and the result says when one was cut.
|
|
53
|
+
- `reply_to_review_comment_tool`: reply in the thread of a diff comment.
|
|
54
|
+
- `get_workflow_run_tool`: one run's status, conclusion, head SHA and branch,
|
|
55
|
+
event and jobs, without logs.
|
|
56
|
+
- `get_failed_job_logs_tool`: the last lines of the log of each failed job in
|
|
57
|
+
a run, with ANSI escapes and timestamps removed. Bounded to 1000 lines per
|
|
58
|
+
job, 5 jobs and 256 KiB per result; the result says when something was cut.
|
|
59
|
+
- `create_release_tool`: create a release and, if needed, its tag.
|
|
60
|
+
- README and SECURITY.md list the fine-grained token permission each tool
|
|
61
|
+
needs.
|
|
62
|
+
|
|
63
|
+
### Security
|
|
64
|
+
|
|
65
|
+
- The API client is created with `follow_redirects=False`. GitHub redirects a
|
|
66
|
+
job log to a pre-signed storage URL; that URL is fetched by a separate
|
|
67
|
+
client that has no token, over HTTPS only, and is never logged or put in an
|
|
68
|
+
error message.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- README lists all 25 tools; it had stopped at the first eleven.
|
|
73
|
+
|
|
74
|
+
## [1.1.0] - 2026-10-10
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
|
|
78
|
+
- The eleven tools that only read (`list_issues`, `get_issue`,
|
|
79
|
+
`list_pull_requests`, `get_pull_request`, `get_pull_request_diff`,
|
|
80
|
+
`get_pull_request_checks`, `get_file_content`, `list_repo_tree`,
|
|
81
|
+
`search_code`, `search_issues`, `list_workflow_runs`) publish
|
|
82
|
+
`readOnlyHint: true`. A gateway uses it to decide whether an invalid optional
|
|
83
|
+
argument may be dropped or must refuse the call (crunchtools/constitution#35).
|
|
84
|
+
- Tests pin every registered tool into `READ_ONLY` or `WRITES`, and check that
|
|
85
|
+
each read-only tool sends GitHub nothing but GET requests.
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- Inherits constitution v1.22.0; the workflow pins and the pre-commit hook rev
|
|
90
|
+
move with it.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- `server.json` said 0.1.0 and the Containerfile label said 0.3.0; both carry
|
|
95
|
+
the release version again.
|
|
96
|
+
|
|
97
|
+
## [1.0.1] - 2026-10-02
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
|
|
101
|
+
- The package, `__version__` and the MCP server reported 0.4.0 while the
|
|
102
|
+
v1.0.0 release and image said 1.0.0; all three now carry the release version.
|
|
103
|
+
|
|
104
|
+
### Changed
|
|
105
|
+
|
|
106
|
+
- Constitution is now a v1.18.0 manifest: it holds only what is specific to
|
|
107
|
+
this repo; fleet and profile rules apply by reference.
|
|
108
|
+
- Constitution validation is pinned to the inherited release via
|
|
109
|
+
`.github/workflows/constitution.yml`.
|
|
110
|
+
- Dependabot auto-merges GitHub Actions minor and patch updates.
|
|
111
|
+
|
|
112
|
+
## [1.0.0] - 2026-09-20
|
|
113
|
+
|
|
114
|
+
First tagged release. This image has been running in production since before
|
|
115
|
+
it had version control. No changes are recorded prior to this point --
|
|
116
|
+
RT #1484 added this file on 2026-09-19, before this repo's first tag.
|