mcp-github-crunchtools 1.2.0__tar.gz → 1.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.specify/memory/constitution.md +14 -5
  2. mcp_github_crunchtools-1.3.1/.specify/specs/002-request-token-merge-gate/spec.md +132 -0
  3. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/CHANGELOG.md +41 -0
  4. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/CLAUDE.md +10 -1
  5. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/Containerfile +1 -1
  6. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/PKG-INFO +68 -10
  7. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/README.md +67 -9
  8. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/SECURITY.md +86 -33
  9. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/pyproject.toml +1 -1
  10. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/server.json +2 -2
  11. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/__init__.py +5 -2
  12. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/client.py +90 -13
  13. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/config.py +23 -11
  14. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/errors.py +22 -4
  15. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/server.py +18 -7
  16. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/tools/pull_requests.py +274 -15
  17. mcp_github_crunchtools-1.3.1/tests/test_merge_gate.py +584 -0
  18. mcp_github_crunchtools-1.3.1/tests/test_request_token.py +325 -0
  19. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/tests/test_tools.py +23 -61
  20. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/uv.lock +1 -1
  21. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.gatehouse-ignore +0 -0
  22. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  23. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  24. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  25. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/dependabot.yml +0 -0
  26. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/workflows/ashigaru.yml +0 -0
  27. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/workflows/ci.yml +0 -0
  28. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/workflows/constitution.yml +0 -0
  29. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/workflows/container.yml +0 -0
  30. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/workflows/dependabot-automerge.yml +0 -0
  31. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/workflows/gatehouse-retriage.yml +0 -0
  32. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/workflows/gatehouse.yml +0 -0
  33. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/workflows/publish.yml +0 -0
  34. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.github/workflows/security.yml +0 -0
  35. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.gitignore +0 -0
  36. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.pre-commit-config.yaml +0 -0
  37. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.specify/specs/000-baseline/spec.md +0 -0
  38. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.specify/specs/001-pr-merge-logs-release/spec.md +0 -0
  39. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.specify/templates/plan-template.md +0 -0
  40. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/.specify/templates/spec-template.md +0 -0
  41. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/LICENSE +0 -0
  42. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/gourmand-exceptions.toml +0 -0
  43. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/gourmand.toml +0 -0
  44. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/__main__.py +0 -0
  45. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/models.py +0 -0
  46. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/tools/__init__.py +0 -0
  47. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/tools/actions.py +0 -0
  48. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/tools/files.py +0 -0
  49. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/tools/issues.py +0 -0
  50. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/tools/releases.py +0 -0
  51. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/src/mcp_github_crunchtools/tools/search.py +0 -0
  52. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/tests/__init__.py +0 -0
  53. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/tests/conftest.py +0 -0
  54. {mcp_github_crunchtools-1.2.0 → mcp_github_crunchtools-1.3.1}/tests/test_validation.py +0 -0
@@ -1,8 +1,8 @@
1
1
  # mcp-github-crunchtools Constitution
2
2
 
3
- > **Version:** 1.2.0
3
+ > **Version:** 1.3.0
4
4
  > **Ratified:** 2026-06-14
5
- > **Amended:** 2026-10-12
5
+ > **Amended:** 2026-10-14
6
6
  > **Status:** Active
7
7
  > **Inherits:** [crunchtools/constitution](https://github.com/crunchtools/constitution) v1.24.0
8
8
  > **Profile:** MCP Server
@@ -15,9 +15,17 @@ 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
30
  states are allowlisted (`open`, `closed`, `all`), as are merge methods
23
31
  (`merge`, `squash`, `rebase`); owner and repo names are
@@ -73,3 +81,4 @@ responses in `TestClientErrorHandling`.
73
81
  | 1.0.1 | 2026-09-25 | Inherit constitution v1.17.0 (Gatehouse gates) |
74
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 |
75
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,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 |
@@ -6,6 +6,47 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.1] - 2026-10-16
10
+
11
+ ### Fixed
12
+
13
+ - A primary rate limit reported GitHub's `x-ratelimit-reset` epoch as the
14
+ wait ("Retry after 1792152901 seconds"). It now reports the seconds left
15
+ until the reset.
16
+
17
+ ## [1.3.0] - 2026-10-14
18
+
19
+ ### Added
20
+
21
+ - `GITHUB_ACCEPT_REQUEST_TOKEN` (default off): over HTTP, act with the token
22
+ in each request's `Authorization: Bearer <token>` header, the way GitHub's
23
+ hosted MCP server takes it, falling back to `GITHUB_TOKEN`. With it on,
24
+ `GITHUB_TOKEN` is optional. A malformed header is refused, never
25
+ forwarded; a call with no token at all fails before reaching GitHub.
26
+ stdio is unchanged.
27
+ - `merge_pull_request_tool` takes `allow_no_required_checks` (default
28
+ False).
29
+
30
+ ### Changed
31
+
32
+ - `merge_pull_request_tool` enforces the base branch's required status
33
+ checks itself before merging: from rulesets and classic branch
34
+ protection, every required check must have a passing check run (success,
35
+ skipped or neutral) or a successful status on the head commit, or the
36
+ merge is refused with each offending check named. A branch with no
37
+ required checks is refused unless `allow_no_required_checks=True`, and a
38
+ branch protection that cannot be read is refused. Classic protection is
39
+ read from the branch itself, so no Administration permission is needed.
40
+ The merge is always pinned to the checked head SHA. It now needs Pull
41
+ requests: read and Commit statuses: read as well as Contents: write.
42
+
43
+ ### Security
44
+
45
+ - An organization owner's token can bypass required checks on GitHub's
46
+ side; the merge gate closes that for this tool.
47
+ - The request token is scrubbed from error messages like `GITHUB_TOKEN`,
48
+ and is passed per call, never stored on the shared HTTP client.
49
+
9
50
  ## [1.2.0] - 2026-10-12
10
51
 
11
52
  ### Added
@@ -44,7 +44,8 @@ claude mcp add mcp-github-crunchtools \
44
44
 
45
45
  | Variable | Required | Default | Description |
46
46
  |----------|----------|---------|-------------|
47
- | `GITHUB_TOKEN` | Yes | — | GitHub Personal Access Token |
47
+ | `GITHUB_TOKEN` | Yes, unless `GITHUB_ACCEPT_REQUEST_TOKEN` is on | — | GitHub Personal Access Token |
48
+ | `GITHUB_ACCEPT_REQUEST_TOKEN` | No | off | Over HTTP, use each request's `Authorization: Bearer` token (falls back to `GITHUB_TOKEN`) |
48
49
  | `GITHUB_API_URL` | No | `https://api.github.com` | API base URL (set for GHES) |
49
50
  | `GITHUB_DEFAULT_ORG` | No | — | Default owner when a tool omits `owner` |
50
51
  | `SSL_CERT_FILE` | No | — | Custom CA bundle for self-hosted instances |
@@ -68,6 +69,14 @@ fresh run (via `workflow_dispatch`) and is the way to *force* a build —
68
69
  `rerun_*` only re-run an existing run and GitHub rejects runs older than 30
69
70
  days.
70
71
 
72
+ `merge_pull_request_tool` enforces the base branch's required checks itself
73
+ (rulesets and classic protection, fail closed) before it merges, pinned to
74
+ the checked head SHA: an org owner's token can bypass the rules on GitHub's
75
+ side. Do not weaken that gate.
76
+
77
+ The per-request token goes into one call's headers and the `request_token`
78
+ context variable in `errors.py`; never onto the shared httpx client.
79
+
71
80
  `get_failed_job_logs_tool` is the one tool that talks to a host other than the
72
81
  API: GitHub redirects a job log to a storage URL. `GitHubClient.get_redirected_tail`
73
82
  follows it with a separate client that has no token. Keep it that way.
@@ -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.2.0" \
30
+ version="1.3.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.2.0
3
+ Version: 1.3.1
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
@@ -86,7 +86,7 @@ permission the tool needs (see [Token permissions](#token-permissions)).
86
86
  | `create_pull_request_tool` | Open a pull request, optionally as a draft (*write*) | Pull requests: write |
87
87
  | `update_pull_request_tool` | Edit, close or reopen a pull request; does not merge (*write*) | Pull requests: write |
88
88
  | `reply_to_review_comment_tool` | Reply in the thread of a diff comment (*write*) | Pull requests: write |
89
- | `merge_pull_request_tool` | Merge, squash or rebase a pull request, optionally only at an expected head SHA (*write*) | Contents: write |
89
+ | `merge_pull_request_tool` | Merge, squash or rebase a pull request, only once the base branch's required checks are green on its head (see [The merge gate](#the-merge-gate)) (*write*) | Contents: write; Pull requests and Commit statuses: read |
90
90
 
91
91
  ### Actions (6 tools)
92
92
 
@@ -162,12 +162,39 @@ podman run -e GITHUB_TOKEN=your_token \
162
162
 
163
163
  | Variable | Required | Default | Description |
164
164
  |----------|----------|---------|-------------|
165
- | `GITHUB_TOKEN` | Yes | — | GitHub Personal Access Token |
165
+ | `GITHUB_TOKEN` | Yes, unless `GITHUB_ACCEPT_REQUEST_TOKEN` is on | — | GitHub Personal Access Token |
166
+ | `GITHUB_ACCEPT_REQUEST_TOKEN` | No | off | `true`, `1` or `yes`: over HTTP, act with the token in each request's `Authorization: Bearer` header (see [Per-request token](#per-request-token)) |
166
167
  | `GITHUB_API_URL` | No | `https://api.github.com` | API base URL (set for GHES) |
167
168
  | `GITHUB_DEFAULT_ORG` | No | — | Default owner when a tool omits `owner` |
168
169
  | `SSL_CERT_FILE` | No | — | Custom CA bundle path, for self-hosted GHES with an internal CA |
169
170
  | `GITHUB_SSL_VERIFY` | No | `true` | Set `false` to disable TLS verification (not recommended) |
170
171
 
172
+ ### Per-request token
173
+
174
+ GitHub's own hosted MCP server takes the caller's token on every HTTP
175
+ request as `Authorization: Bearer <token>` and acts with it. With
176
+ `GITHUB_ACCEPT_REQUEST_TOKEN=true` this server does the same, so a client
177
+ configured for GitHub's server can point here instead with the same header.
178
+
179
+ - Over the `streamable-http` or `sse` transport, a request carrying
180
+ `Authorization: Bearer <token>` is sent to GitHub with that token, for
181
+ that request only. Concurrent requests with different tokens each use
182
+ their own; the token is never stored on the shared client.
183
+ - A request without the header falls back to `GITHUB_TOKEN`. With the option
184
+ on, `GITHUB_TOKEN` is optional: an instance without it serves only
185
+ requests that bring a token, and refuses the others before calling GitHub
186
+ ("No GitHub token for this request").
187
+ - The header must be exactly `Bearer <token>` (scheme in any case, one
188
+ space, a token of printable ASCII with no spaces). Anything else is
189
+ refused, never forwarded, even when `GITHUB_TOKEN` is set.
190
+ - The request token is scrubbed from error messages like `GITHUB_TOKEN` is.
191
+ - With the option off (the default) an incoming Authorization header is
192
+ ignored, exactly as before. stdio has no request headers and always uses
193
+ `GITHUB_TOKEN`.
194
+
195
+ The option makes the server act as whoever sends a token. Expose the HTTP
196
+ port only to callers you would hand that token to anyway, behind TLS.
197
+
171
198
  ### Creating a GitHub Personal Access Token
172
199
 
173
200
  1. **Navigate to token settings**
@@ -192,11 +219,11 @@ For a fine-grained personal access token, the repository permissions are:
192
219
  | Metadata | every tool (GitHub grants it with any other permission) | — |
193
220
  | Contents | `get_file_content_tool`, `list_repo_tree_tool` | `merge_pull_request_tool`, `create_release_tool` |
194
221
  | Issues | `list_issues_tool`, `get_issue_tool` | `create_issue_tool`, `update_issue_tool`, `create_issue_comment_tool` |
195
- | 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`, `create_issue_comment_tool` on a PR |
222
+ | 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 |
196
223
  | 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` |
197
- | Commit statuses | `get_pull_request_checks_tool` | — |
224
+ | Commit statuses | `get_pull_request_checks_tool`, `merge_pull_request_tool` | — |
198
225
 
199
- `get_pull_request_checks_tool` reads check-runs, and GitHub's permission table
226
+ `get_pull_request_checks_tool` and `merge_pull_request_tool` read check-runs, and GitHub's permission table
200
227
  for fine-grained tokens lists no permission for that endpoint. It is readable
201
228
  on public repositories; on a private repository use a classic token or a
202
229
  GitHub App token. The search tools likewise have no permission of their own:
@@ -212,9 +239,35 @@ Three things worth knowing before granting write:
212
239
  `.github/workflows/`, relative to the default branch, additionally needs
213
240
  Workflows: write; without it GitHub answers 404. Expect the same of a merge
214
241
  that brings workflow changes in.
215
- - **Branch rules still apply.** `merge_pull_request_tool` cannot merge past
216
- required checks or reviews; GitHub answers 405 and the tool returns its
217
- message.
242
+ - **The merge gate checks required status checks itself**, below. Other
243
+ branch rules (required reviews, for instance) are left to GitHub, which
244
+ answers 405 when it refuses; a token that may bypass the rules gets past
245
+ them.
246
+
247
+ ### The merge gate
248
+
249
+ An organization owner, or anyone on a ruleset's bypass list, can merge past
250
+ failing required checks: GitHub merges a red pull request for such a token.
251
+ So `merge_pull_request_tool` does not leave the checks to GitHub. Before it
252
+ merges it:
253
+
254
+ 1. reads the pull request's base branch and head SHA, and refuses if `sha`
255
+ was given and the head has moved;
256
+ 2. collects the base branch's required status checks from its active
257
+ rulesets and its classic branch protection, read from the branch itself
258
+ so no Administration permission is needed. An error reading either, or a
259
+ protected branch whose protection is not shown, refuses the merge rather
260
+ than count as none;
261
+ 3. refuses if no check is required, unless `allow_no_required_checks=True`;
262
+ 4. requires, for every required check on the head commit, a completed check
263
+ run of that name concluding `success`, `skipped` or `neutral` (the most
264
+ recent run of the name decides), or a commit status of `success`. A rule
265
+ that names an app accepts only that app's check runs. A missing, pending
266
+ or failed check refuses the merge, and the error lists each one and why;
267
+ 5. reads the pull request again and refuses if its head or base changed
268
+ meanwhile (GitHub's merge call pins the head but not the base), then
269
+ merges with the head SHA pinned, so a later push makes GitHub refuse
270
+ (409) rather than merge code nobody checked.
218
271
 
219
272
  With a classic token, `repo` covers all of the above (`public_repo` for public
220
273
  repositories only), plus `workflow` for the workflow-file case.
@@ -282,6 +335,7 @@ Assistant: [calls list_pull_request_comments_tool with pull_number=5]
282
335
  Assistant: [calls reply_to_review_comment_tool with comment_id=..., body="fixed in abc1234"]
283
336
  Assistant: [calls get_pull_request_checks_tool with pull_number=5]
284
337
  Assistant: [calls merge_pull_request_tool with merge_method="squash", sha=<head_sha from the checks>]
338
+ (the tool checks the required checks again itself before merging)
285
339
  ```
286
340
 
287
341
  ### Read a File
@@ -307,7 +361,9 @@ This server was designed with security as a primary concern. See [SECURITY.md](S
307
361
  1. **Token Protection**
308
362
  - Stored as SecretStr (never accidentally logged)
309
363
  - Environment variable only (never in files or args)
310
- - Sanitized from all error messages
364
+ - Optionally, a per-request token from the HTTP `Authorization` header,
365
+ used for that request only and never stored on the shared client
366
+ - Both sanitized from all error messages
311
367
 
312
368
  2. **Input Validation**
313
369
  - Pydantic models for write inputs
@@ -322,6 +378,8 @@ This server was designed with security as a primary concern. See [SECURITY.md](S
322
378
  - Response size limits (10MB)
323
379
  - Job-log downloads follow GitHub's redirect without the token, over
324
380
  HTTPS only, and keep just the tail (20MB download limit)
381
+ - Merges only when the required checks are green on the head commit,
382
+ even for a token GitHub would let bypass them
325
383
 
326
384
  4. **Automated CVE Scanning**
327
385
  - GitHub Actions scan dependencies
@@ -60,7 +60,7 @@ permission the tool needs (see [Token permissions](#token-permissions)).
60
60
  | `create_pull_request_tool` | Open a pull request, optionally as a draft (*write*) | Pull requests: write |
61
61
  | `update_pull_request_tool` | Edit, close or reopen a pull request; does not merge (*write*) | Pull requests: write |
62
62
  | `reply_to_review_comment_tool` | Reply in the thread of a diff comment (*write*) | Pull requests: write |
63
- | `merge_pull_request_tool` | Merge, squash or rebase a pull request, optionally only at an expected head SHA (*write*) | Contents: write |
63
+ | `merge_pull_request_tool` | Merge, squash or rebase a pull request, only once the base branch's required checks are green on its head (see [The merge gate](#the-merge-gate)) (*write*) | Contents: write; Pull requests and Commit statuses: read |
64
64
 
65
65
  ### Actions (6 tools)
66
66
 
@@ -136,12 +136,39 @@ podman run -e GITHUB_TOKEN=your_token \
136
136
 
137
137
  | Variable | Required | Default | Description |
138
138
  |----------|----------|---------|-------------|
139
- | `GITHUB_TOKEN` | Yes | — | GitHub Personal Access Token |
139
+ | `GITHUB_TOKEN` | Yes, unless `GITHUB_ACCEPT_REQUEST_TOKEN` is on | — | GitHub Personal Access Token |
140
+ | `GITHUB_ACCEPT_REQUEST_TOKEN` | No | off | `true`, `1` or `yes`: over HTTP, act with the token in each request's `Authorization: Bearer` header (see [Per-request token](#per-request-token)) |
140
141
  | `GITHUB_API_URL` | No | `https://api.github.com` | API base URL (set for GHES) |
141
142
  | `GITHUB_DEFAULT_ORG` | No | — | Default owner when a tool omits `owner` |
142
143
  | `SSL_CERT_FILE` | No | — | Custom CA bundle path, for self-hosted GHES with an internal CA |
143
144
  | `GITHUB_SSL_VERIFY` | No | `true` | Set `false` to disable TLS verification (not recommended) |
144
145
 
146
+ ### Per-request token
147
+
148
+ GitHub's own hosted MCP server takes the caller's token on every HTTP
149
+ request as `Authorization: Bearer <token>` and acts with it. With
150
+ `GITHUB_ACCEPT_REQUEST_TOKEN=true` this server does the same, so a client
151
+ configured for GitHub's server can point here instead with the same header.
152
+
153
+ - Over the `streamable-http` or `sse` transport, a request carrying
154
+ `Authorization: Bearer <token>` is sent to GitHub with that token, for
155
+ that request only. Concurrent requests with different tokens each use
156
+ their own; the token is never stored on the shared client.
157
+ - A request without the header falls back to `GITHUB_TOKEN`. With the option
158
+ on, `GITHUB_TOKEN` is optional: an instance without it serves only
159
+ requests that bring a token, and refuses the others before calling GitHub
160
+ ("No GitHub token for this request").
161
+ - The header must be exactly `Bearer <token>` (scheme in any case, one
162
+ space, a token of printable ASCII with no spaces). Anything else is
163
+ refused, never forwarded, even when `GITHUB_TOKEN` is set.
164
+ - The request token is scrubbed from error messages like `GITHUB_TOKEN` is.
165
+ - With the option off (the default) an incoming Authorization header is
166
+ ignored, exactly as before. stdio has no request headers and always uses
167
+ `GITHUB_TOKEN`.
168
+
169
+ The option makes the server act as whoever sends a token. Expose the HTTP
170
+ port only to callers you would hand that token to anyway, behind TLS.
171
+
145
172
  ### Creating a GitHub Personal Access Token
146
173
 
147
174
  1. **Navigate to token settings**
@@ -166,11 +193,11 @@ For a fine-grained personal access token, the repository permissions are:
166
193
  | Metadata | every tool (GitHub grants it with any other permission) | — |
167
194
  | Contents | `get_file_content_tool`, `list_repo_tree_tool` | `merge_pull_request_tool`, `create_release_tool` |
168
195
  | Issues | `list_issues_tool`, `get_issue_tool` | `create_issue_tool`, `update_issue_tool`, `create_issue_comment_tool` |
169
- | 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`, `create_issue_comment_tool` on a PR |
196
+ | 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 |
170
197
  | 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` |
171
- | Commit statuses | `get_pull_request_checks_tool` | — |
198
+ | Commit statuses | `get_pull_request_checks_tool`, `merge_pull_request_tool` | — |
172
199
 
173
- `get_pull_request_checks_tool` reads check-runs, and GitHub's permission table
200
+ `get_pull_request_checks_tool` and `merge_pull_request_tool` read check-runs, and GitHub's permission table
174
201
  for fine-grained tokens lists no permission for that endpoint. It is readable
175
202
  on public repositories; on a private repository use a classic token or a
176
203
  GitHub App token. The search tools likewise have no permission of their own:
@@ -186,9 +213,35 @@ Three things worth knowing before granting write:
186
213
  `.github/workflows/`, relative to the default branch, additionally needs
187
214
  Workflows: write; without it GitHub answers 404. Expect the same of a merge
188
215
  that brings workflow changes in.
189
- - **Branch rules still apply.** `merge_pull_request_tool` cannot merge past
190
- required checks or reviews; GitHub answers 405 and the tool returns its
191
- message.
216
+ - **The merge gate checks required status checks itself**, below. Other
217
+ branch rules (required reviews, for instance) are left to GitHub, which
218
+ answers 405 when it refuses; a token that may bypass the rules gets past
219
+ them.
220
+
221
+ ### The merge gate
222
+
223
+ An organization owner, or anyone on a ruleset's bypass list, can merge past
224
+ failing required checks: GitHub merges a red pull request for such a token.
225
+ So `merge_pull_request_tool` does not leave the checks to GitHub. Before it
226
+ merges it:
227
+
228
+ 1. reads the pull request's base branch and head SHA, and refuses if `sha`
229
+ was given and the head has moved;
230
+ 2. collects the base branch's required status checks from its active
231
+ rulesets and its classic branch protection, read from the branch itself
232
+ so no Administration permission is needed. An error reading either, or a
233
+ protected branch whose protection is not shown, refuses the merge rather
234
+ than count as none;
235
+ 3. refuses if no check is required, unless `allow_no_required_checks=True`;
236
+ 4. requires, for every required check on the head commit, a completed check
237
+ run of that name concluding `success`, `skipped` or `neutral` (the most
238
+ recent run of the name decides), or a commit status of `success`. A rule
239
+ that names an app accepts only that app's check runs. A missing, pending
240
+ or failed check refuses the merge, and the error lists each one and why;
241
+ 5. reads the pull request again and refuses if its head or base changed
242
+ meanwhile (GitHub's merge call pins the head but not the base), then
243
+ merges with the head SHA pinned, so a later push makes GitHub refuse
244
+ (409) rather than merge code nobody checked.
192
245
 
193
246
  With a classic token, `repo` covers all of the above (`public_repo` for public
194
247
  repositories only), plus `workflow` for the workflow-file case.
@@ -256,6 +309,7 @@ Assistant: [calls list_pull_request_comments_tool with pull_number=5]
256
309
  Assistant: [calls reply_to_review_comment_tool with comment_id=..., body="fixed in abc1234"]
257
310
  Assistant: [calls get_pull_request_checks_tool with pull_number=5]
258
311
  Assistant: [calls merge_pull_request_tool with merge_method="squash", sha=<head_sha from the checks>]
312
+ (the tool checks the required checks again itself before merging)
259
313
  ```
260
314
 
261
315
  ### Read a File
@@ -281,7 +335,9 @@ This server was designed with security as a primary concern. See [SECURITY.md](S
281
335
  1. **Token Protection**
282
336
  - Stored as SecretStr (never accidentally logged)
283
337
  - Environment variable only (never in files or args)
284
- - Sanitized from all error messages
338
+ - Optionally, a per-request token from the HTTP `Authorization` header,
339
+ used for that request only and never stored on the shared client
340
+ - Both sanitized from all error messages
285
341
 
286
342
  2. **Input Validation**
287
343
  - Pydantic models for write inputs
@@ -296,6 +352,8 @@ This server was designed with security as a primary concern. See [SECURITY.md](S
296
352
  - Response size limits (10MB)
297
353
  - Job-log downloads follow GitHub's redirect without the token, over
298
354
  HTTPS only, and keep just the tail (20MB download limit)
355
+ - Merges only when the required checks are green on the head commit,
356
+ even for a token GitHub would let bypass them
299
357
 
300
358
  4. **Automated CVE Scanning**
301
359
  - GitHub Actions scan dependencies