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.
Files changed (62) hide show
  1. mcp_github_crunchtools-1.3.0/.github/workflows/ashigaru.yml +98 -0
  2. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/ci.yml +1 -1
  3. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/constitution.yml +1 -1
  4. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/dependabot-automerge.yml +1 -1
  5. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/gatehouse-retriage.yml +1 -1
  6. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/gatehouse.yml +2 -2
  7. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.pre-commit-config.yaml +1 -1
  8. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.specify/memory/constitution.md +30 -10
  9. mcp_github_crunchtools-1.3.0/.specify/specs/001-pr-merge-logs-release/spec.md +141 -0
  10. mcp_github_crunchtools-1.3.0/.specify/specs/002-request-token-merge-gate/spec.md +132 -0
  11. mcp_github_crunchtools-1.3.0/CHANGELOG.md +116 -0
  12. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/CLAUDE.md +31 -8
  13. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/Containerfile +3 -3
  14. mcp_github_crunchtools-1.3.0/PKG-INFO +432 -0
  15. mcp_github_crunchtools-1.3.0/README.md +406 -0
  16. mcp_github_crunchtools-1.3.0/SECURITY.md +362 -0
  17. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/pyproject.toml +2 -2
  18. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/server.json +3 -3
  19. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/__init__.py +5 -2
  20. mcp_github_crunchtools-1.3.0/src/mcp_github_crunchtools/client.py +465 -0
  21. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/config.py +23 -11
  22. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/errors.py +22 -4
  23. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/models.py +80 -14
  24. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/server.py +252 -2
  25. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/tools/__init__.py +14 -0
  26. mcp_github_crunchtools-1.3.0/src/mcp_github_crunchtools/tools/actions.py +389 -0
  27. mcp_github_crunchtools-1.3.0/src/mcp_github_crunchtools/tools/pull_requests.py +785 -0
  28. mcp_github_crunchtools-1.3.0/src/mcp_github_crunchtools/tools/releases.py +82 -0
  29. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/tests/conftest.py +30 -3
  30. mcp_github_crunchtools-1.3.0/tests/test_merge_gate.py +584 -0
  31. mcp_github_crunchtools-1.3.0/tests/test_request_token.py +325 -0
  32. mcp_github_crunchtools-1.3.0/tests/test_tools.py +1949 -0
  33. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/tests/test_validation.py +89 -0
  34. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/uv.lock +1 -1
  35. mcp_github_crunchtools-1.1.0/CHANGELOG.md +0 -51
  36. mcp_github_crunchtools-1.1.0/PKG-INFO +0 -263
  37. mcp_github_crunchtools-1.1.0/README.md +0 -237
  38. mcp_github_crunchtools-1.1.0/SECURITY.md +0 -219
  39. mcp_github_crunchtools-1.1.0/src/mcp_github_crunchtools/client.py +0 -271
  40. mcp_github_crunchtools-1.1.0/src/mcp_github_crunchtools/tools/actions.py +0 -187
  41. mcp_github_crunchtools-1.1.0/src/mcp_github_crunchtools/tools/pull_requests.py +0 -287
  42. mcp_github_crunchtools-1.1.0/tests/test_tools.py +0 -941
  43. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.gatehouse-ignore +0 -0
  44. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  45. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  46. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  47. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/dependabot.yml +0 -0
  48. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/container.yml +0 -0
  49. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/publish.yml +0 -0
  50. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.github/workflows/security.yml +0 -0
  51. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.gitignore +0 -0
  52. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.specify/specs/000-baseline/spec.md +0 -0
  53. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.specify/templates/plan-template.md +0 -0
  54. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/.specify/templates/spec-template.md +0 -0
  55. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/LICENSE +0 -0
  56. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/gourmand-exceptions.toml +0 -0
  57. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/gourmand.toml +0 -0
  58. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/__main__.py +0 -0
  59. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/tools/files.py +0 -0
  60. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/tools/issues.py +0 -0
  61. {mcp_github_crunchtools-1.1.0 → mcp_github_crunchtools-1.3.0}/src/mcp_github_crunchtools/tools/search.py +0 -0
  62. {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 }}
@@ -40,7 +40,7 @@ jobs:
40
40
 
41
41
  gourmand:
42
42
  name: Code Quality (Gourmand)
43
- uses: crunchtools/gatehouse/.github/workflows/gourmand.yml@v0.15.3
43
+ uses: crunchtools/gatehouse/.github/workflows/gourmand.yml@v0
44
44
 
45
45
  build-container:
46
46
  runs-on: ubuntu-latest
@@ -18,4 +18,4 @@ permissions:
18
18
  jobs:
19
19
  constitution:
20
20
  name: Constitution
21
- uses: crunchtools/constitution/.github/workflows/validate.yml@v1.22.0
21
+ uses: crunchtools/constitution/.github/workflows/validate.yml@v1.24.0
@@ -15,4 +15,4 @@ permissions:
15
15
  jobs:
16
16
  automerge:
17
17
  name: Dependabot
18
- uses: crunchtools/constitution/.github/workflows/dependabot-automerge.yml@v1.22.0
18
+ uses: crunchtools/constitution/.github/workflows/dependabot-automerge.yml@v1.24.0
@@ -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.15.3
23
+ uses: crunchtools/gatehouse/.github/workflows/retriage.yml@v0
@@ -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.15.3
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.15.3
110
+ uses: crunchtools/gatehouse/.github/workflows/triage.yml@v0
@@ -12,7 +12,7 @@ repos:
12
12
  types: [python]
13
13
  stages: [pre-commit, pre-merge-commit]
14
14
  - repo: https://github.com/crunchtools/constitution
15
- rev: v1.22.0
15
+ rev: v1.24.0
16
16
  hooks:
17
17
  - id: validate-constitution
18
18
 
@@ -1,10 +1,10 @@
1
1
  # mcp-github-crunchtools Constitution
2
2
 
3
- > **Version:** 1.1.0
3
+ > **Version:** 1.3.0
4
4
  > **Ratified:** 2026-06-14
5
- > **Amended:** 2026-10-02
5
+ > **Amended:** 2026-10-14
6
6
  > **Status:** Active
7
- > **Inherits:** [crunchtools/constitution](https://github.com/crunchtools/constitution) v1.22.0
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` (required), held as `SecretStr`, read from
19
- the environment only. `GitHubApiError` scrubs it from messages, and
20
- `Config.__repr__()`/`__str__()` never expose it.
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`); owner and repo names are
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 `/`). `NotFoundError`
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. Client error handling is
46
- tested for 401, 404, 429 and 204 responses in `TestClientErrorHandling`.
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.