Skip to content

Commit 7938fbf

Browse files
committed
Initial commit: ReviewGuard MCP server
0 parents  commit 7938fbf

32 files changed

Lines changed: 8508 additions & 0 deletions

.github/dependabot.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: npm
4+
directory: "/"
5+
schedule:
6+
interval: weekly
7+
groups:
8+
minor-and-patch:
9+
update-types:
10+
- minor
11+
- patch
12+
13+
- package-ecosystem: github-actions
14+
directory: "/"
15+
schedule:
16+
interval: weekly

.github/workflows/ci.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
verify:
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
# 22 is the minimum consumer version (engines), 24 the recommended
17+
# development version (.nvmrc).
18+
node-version: [22, 24]
19+
steps:
20+
- uses: actions/checkout@v7
21+
22+
# check-latest avoids a stale runner tool cache serving a Node patch
23+
# older than what npm 12 supports (e.g. 22.x before 22.22.2).
24+
#
25+
# package-manager-cache: false is required. setup-node auto-enables npm
26+
# caching when it detects a lockfile, and that step runs npm config get
27+
# cache with the runner's bundled npm 10/11, which fails the repository's
28+
# devEngines check (npm >= 12) before the upgrade step below can run.
29+
# Simply leaving the cache option unset is not enough because auto-caching
30+
# is on by default.
31+
- uses: actions/setup-node@v7
32+
with:
33+
node-version: ${{ matrix.node-version }}
34+
check-latest: true
35+
package-manager-cache: false
36+
37+
# Runs outside the repository because the runner's bundled npm 11 would
38+
# otherwise fail the repository's devEngines check (npm >= 12) before it
39+
# can upgrade itself.
40+
- name: Use npm 12 (required by devEngines)
41+
run: npm install -g npm@12
42+
working-directory: ${{ runner.temp }}
43+
44+
- run: npm ci
45+
- run: npm run lint
46+
- run: npm run format:check
47+
- run: npm run typecheck
48+
- run: npm run build
49+
- run: npm test
50+
51+
- name: Verify publishable package contents
52+
run: npm pack --dry-run

.github/workflows/release.yml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Manual release: bumps the version, prepends auto-generated release notes to
2+
# CHANGELOG.md, tags, creates a GitHub release, and publishes to npm.
3+
#
4+
# Publishing uses npm trusted publishing (OIDC), so no npm token is stored in
5+
# the repository. One-time setup and the first manual publish are documented
6+
# in the repository wiki.
7+
name: Release
8+
9+
on:
10+
workflow_dispatch:
11+
inputs:
12+
version:
13+
description: "Version to release, e.g. 0.2.0 (tags v0.2.0)"
14+
required: true
15+
type: string
16+
17+
concurrency: release
18+
19+
permissions:
20+
contents: write
21+
id-token: write # npm trusted publishing (OIDC)
22+
23+
jobs:
24+
release:
25+
runs-on: ubuntu-latest
26+
env:
27+
VERSION: ${{ inputs.version }}
28+
steps:
29+
- uses: actions/checkout@v7
30+
with:
31+
# Full history so release notes and the previous tag are found.
32+
fetch-depth: 0
33+
34+
# package-manager-cache: false is required. setup-node auto-enables npm
35+
# caching when it detects a lockfile, and that step runs npm config get
36+
# cache with the runner's bundled npm 10/11, which fails the repository's
37+
# devEngines check (npm >= 12) before the upgrade step below can run.
38+
# Simply leaving the cache option unset is not enough because auto-caching
39+
# is on by default.
40+
- uses: actions/setup-node@v7
41+
with:
42+
node-version-file: .nvmrc
43+
registry-url: "https://registry.npmjs.org"
44+
package-manager-cache: false
45+
46+
# Runs outside the repository because the runner's bundled npm 11 would
47+
# otherwise fail the repository's devEngines check (npm >= 12) before it
48+
# can upgrade itself.
49+
- name: Use npm 12 (required by devEngines and trusted publishing)
50+
run: npm install -g npm@12
51+
working-directory: ${{ runner.temp }}
52+
53+
- name: Validate version
54+
run: |
55+
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
56+
echo "::error::'$VERSION' is not plain X.Y.Z semver"
57+
exit 1
58+
fi
59+
if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
60+
echo "::error::tag v$VERSION already exists"
61+
exit 1
62+
fi
63+
64+
- run: npm ci
65+
- run: npm run lint
66+
- run: npm run format:check
67+
- run: npm run typecheck
68+
- run: npm run build
69+
- run: npm test
70+
71+
- name: Bump version
72+
run: npm version --no-git-tag-version "$VERSION"
73+
74+
- name: Generate release notes and update CHANGELOG.md
75+
env:
76+
GH_TOKEN: ${{ github.token }}
77+
run: |
78+
PREVIOUS_TAG=$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || true)
79+
gh api "repos/${{ github.repository }}/releases/generate-notes" \
80+
-f tag_name="v$VERSION" \
81+
-f target_commitish="${{ github.sha }}" \
82+
${PREVIOUS_TAG:+-f previous_tag_name="$PREVIOUS_TAG"} \
83+
--jq .body > /tmp/release-notes.md
84+
{
85+
echo "# Changelog"
86+
echo
87+
echo "## v$VERSION ($(date -u +%Y-%m-%d))"
88+
echo
89+
cat /tmp/release-notes.md
90+
echo
91+
tail -n +3 CHANGELOG.md
92+
} > /tmp/changelog.md
93+
mv /tmp/changelog.md CHANGELOG.md
94+
95+
- name: Commit, tag and push
96+
run: |
97+
git config user.name "github-actions[bot]"
98+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
99+
git add package.json package-lock.json CHANGELOG.md
100+
git commit -m "chore: release v$VERSION"
101+
git tag "v$VERSION"
102+
git push origin "HEAD:$GITHUB_REF_NAME"
103+
git push origin "v$VERSION"
104+
105+
- name: Create GitHub release
106+
env:
107+
GH_TOKEN: ${{ github.token }}
108+
run: gh release create "v$VERSION" --title "v$VERSION" --notes-file /tmp/release-notes.md
109+
110+
- name: Publish to npm (trusted publishing, provenance attached)
111+
run: npm publish

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
*.tgz
5+
.env

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24

.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dist/
2+
coverage/
3+
package-lock.json
4+
CHANGELOG.md

.prettierrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"printWidth": 100
3+
}

AGENTS.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
For everything not covered here, the human-facing docs are authoritative: see
2+
`README.md` for modes, tools, flags, and usage, and `CONTRIBUTING.md` for the
3+
development setup, the script list, and the release process.
4+
5+
## Project overview
6+
7+
ReviewGuard MCP (npm package `@eclipsesource/review-guard-mcp`, binary
8+
`review-guard-mcp`, MCP server name `review-guard`) is a TypeScript MCP server
9+
that lets AI agents work on GitHub PR reviews behind a safety boundary. In the
10+
default **pending mode** agents read PR discussion context and create draft
11+
(pending) reviews but cannot submit them. In the opt-in **submit mode**
12+
(`--allow-submit`) an agent may also submit the review, restricted to an allowed
13+
action set and always prefixed with a fixed disclaimer. Two transports: stdio
14+
(for IDE-managed lifetime) and HTTP (remote server).
15+
16+
## Architecture
17+
18+
- **`src/github.ts`**: `GitHubReviewClient` class. The ONLY file that touches GitHub APIs. Uses GraphQL for resolved review-thread context and mutations, and REST for review summaries and general PR comments. Review-thread comments (GraphQL `reactionGroups`) and conversation comments (REST `reactions`) expose a normalized `reactions` map (content -> count, e.g. `THUMBS_DOWN`) so agents can see downvotes. Thread `isResolved`/`resolvedBy` are also returned. Safety boundary: comment/thread mutations exclude the `event` field, a post-creation tripwire verifies `PENDING` state (its error message tells the agent to stop and alert the human, since many MCP clients do not surface tool errors), and write operations are limited to the authenticated user's review. Submission is gated: `submitReview` (the only place `submitPullRequestReview` is called) refuses any action not in the `allowSubmit` set passed to the constructor, and always prefixes the review body with the fixed `submitBody` (an optional caller `additionalBody` is appended below it, never replacing it). Thread resolution is gated too: `resolveReviewThread` runs only when `allowResolve` is set AND the thread's first comment is authored by the authenticated user (it refuses others' threads), so a bot can tidy up its own now-fixed findings but not close anyone else's conversations. PR scoping is enforced here too: when the client is constructed with a `scope` (owner/repo/PR), every public method calls `assertInScope` and refuses input targeting a different PR/repo, and `resolveReviewThread` verifies the thread's PR matches the scope. This is the authoritative boundary. The server-side schema change is convenience on top of it.
19+
- **`src/server.ts`**: `createMcpServer(client)` factory. Registers `get_pr_review_context`, `list_pending_review`, `add_review_comments`, `modify_review_comment`, and `delete_pending_review` with Zod schemas. Additionally registers `submit` **only when** `client.allowedSubmitActions` is non-empty (its `action` enum is restricted to that set), and `resolve_review_thread` **only when** `client.resolveEnabled` (started with `--allow-resolve`). When `client.scopedPullRequest` is set, the PR tools omit their `owner`/`repo`/`pull_number` arguments and act on the scoped PR implicitly. Shared by both transports.
20+
- **`src/stdio.ts`**: Stdio transport entry point. Connects the MCP server to stdin/stdout for IDE-managed lifetime (Theia, VS Code).
21+
- **`src/http.ts`**: HTTP transport. A plain `node:http` server exposing stateless Streamable HTTP at `/mcp` (POST only, GET/DELETE return 405, other paths 404). No web framework: the MCP transport parses the request body and enforces the Host header itself. Receives `{ port, host }` from the entry point and binds that address (default `127.0.0.1`). Validates the Host header of incoming requests (DNS rebinding protection): loopback aliases only for a loopback bind, plus the bind address and the container-runtime host names (`host.docker.internal`, `host.containers.internal`) for a non-loopback bind.
22+
- **`src/index.ts`**: Thin dispatcher. Parses the CLI via `parseCliOptions`, resolves the GitHub token, applies the default submit disclaimer, creates the client, then delegates to `--stdio` or HTTP mode.
23+
- **`src/args.ts`**: `parseCliOptions`, the single place that knows every CLI flag. Accepts `--flag value` and `--flag=value`. Throws `CliUsageError` on unknown flags, missing values, or duplicates (the entry point prints it and exits non-zero), so a typo'd hardening flag (e.g. the `--repo`/`--pr` scope) can never be silently ignored.
24+
- **`test/`**: vitest suite. `args.test.ts` covers CLI validation. `github.test.ts` covers the safety gates (submit refusal, fixed-body prefixing, scope assertion, own-thread resolve, PENDING tripwire) with mocked `gql`/`octokit` internals. `server.test.ts` covers tool registration and dispatch over an in-memory MCP transport. Changes to the safety boundary must keep this coverage. `test/integration/review-guard.itest.ts` is a manual end-to-end suite (`npm run test:integration`, own config in `vitest.integration.config.ts`) that drives every tool against a real GitHub repository with two accounts and verifies the results through direct API reads. It is excluded from `npm test` and CI, setup is documented in CONTRIBUTING.md.
25+
26+
## Key conventions
27+
28+
- ESM (`"type": "module"` in package.json), NodeNext module resolution
29+
- Strict TypeScript. ESLint (type-checked) and Prettier are enforced in CI, so run `npm run lint` and `npm run format` before committing
30+
- No em dashes and no semicolons in prose. This applies to docs, code comments, and user-facing strings. Write plain sentences instead
31+
- Tool errors returned as `{ isError: true }` content, not thrown
32+
- Server binds to `127.0.0.1` by default, or the one specific address given with `--host`. Unspecified addresses (`0.0.0.0`, `::`) are refused at CLI parsing. The HTTP transport validates the Host header (DNS rebinding protection)
33+
- `submitPullRequestReview` lives ONLY in `submitReview` and runs ONLY when `allowSubmit` is non-empty and the requested action is in it. Do not add submission anywhere else, do not bypass the `allowSubmit` gate. The `submitBody` prefix stays server-controlled: a caller may pass `summary` (the `submit` tool) which is APPENDED below the prefix, but the prefix is always present and can never be replaced or removed.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Changelog

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

0 commit comments

Comments
 (0)