前回の記事では、MCPサーバーに GitHub PR のコメント取得・レビュー投稿ツールを実装しました。
今回は実運用で見えてきた課題を解決する二つの改善を加えます。
- diff 取得をローカル clone 経由に変更する
- clone したリポジトリ以外をレビュー対象外にする
なぜ GitHub MCP を使わないのか
PR レビューの文脈では GitHub MCP を使う選択肢もあります。しかし今回は採用しませんでした。理由は二つです。
① Backlog にも対応する必要がある
このサーバーは GitHub だけでなく Backlog の PR もレビュー対象です。GitHub MCP を使うと GitHub 専用のツール体系になってしまい、Backlog 向けの実装と統一感が取れなくなります。独自クライアントで揃えることで、両プラットフォームを同じ設計パターンで扱えます。
② MCP 自体の挙動をコントロールしにくい
GitHub MCP のようなサードパーティ MCP サーバーをそのまま Claude に渡すと、どのツールをどの順番で呼ぶかを Claude が自律的に判断します。レビューフローでは「まず diff を取得し、次にコメントを取得してからレビューを投稿する」という順序を守ってほしいのですが、MCP サーバーをそのまま渡す形だとその制御が難しくなります。独自の MCP ツールとして定義し description で指示を与えることで、Claude の動作をフローに沿って誘導しています。
なぜローカル clone が必要なのか
前回まで diff は GitHub REST API の List pull requests files で取得していました。しかしこのエンドポイントにはいくつか制限があります。
- ファイル数上限が 300:大きな PR だとファイルが切り捨てられる
- 差分行数の上限:ファイルごとに
patchフィールドが省略されることがある
レビューコメントの行番号は unified diff の RIGHT 側の行番号と一致しなければなりません。API が返す情報が不完全だと行番号のズレが生じ、インラインコメントの投稿に失敗します。
そこで あらかじめリポジトリをローカルに clone しておき、git fetch → git diff でフルの unified diff を取得する アプローチに切り替えました。
実装の全体像
MCPツール (getPrDiff)
│
▼
GetPrDiffService
├── GitHubApiClient … PR の base/head ブランチ名を取得
└── LocalGitDiffClient … ローカル clone で git diff を実行
│
▼
DiffParser … unified diff から「コメント可能な行番号」を抽出
ディレクトリ構成(抜粋)
my-mcp-server/
├── module/
│ ├── domain/
│ │ └── DiffParser.java # unified diff パーサ
│ ├── repository/
│ │ ├── LocalGitDiffClient.java # git コマンド実行
│ │ ├── LocalGitException.java # 例外クラス
│ │ └── RepositoryClientConfig.java
│ └── service/
│ └── GetPrDiffService.java # 取得フローの統括
└── mcp/
└── PrInspectionComponent.java # MCPツール定義
LocalGitDiffClient
public class LocalGitDiffClient { // baseDir 配下に clone 済みのリポジトリが置いてある前提 public String getDiff(String repoName, String baseBranch, String headBranch) { ... } }
内部では ProcessBuilder で以下の 2 コマンドを順番に実行します。
git fetch --quiet origin <baseBranch> <headBranch> git diff origin/<baseBranch>...origin/<headBranch>
ポイント:ディレクトリ存在確認が「レビュー制限」を兼ねる
getDiff() は baseDir.resolve(repoName) にディレクトリが存在しなければ LocalGitException を投げます。「ローカルに clone していないリポジトリは diff が取れない」 という構造がそのままレビュー対象の絞り込みになります。
明示的な許可リストを持たなくても、clone してあるリポジトリだけがレビュー対象 になります。
GetPrDiffService
@Service @RequiredArgsConstructor public class GetPrDiffService { private final GitHubApiClient gitHubApiClient; private final LocalGitDiffClient localGitDiffClient; public record FileInfo(String path, List<String> commentableLineRanges) {} public record Result( boolean success, String prUrl, String diff, List<FileInfo> files, String error, ...) {} public Result getPrDiff(PrRef ref, String prUrl) { ... } }
① GitHub API で base/head ブランチ名を取得 → ② ローカル clone で diff を取得 → ③ DiffParser でコメント可能な行番号を抽出、という流れです。PR のメタデータは GitHub API、diff の中身はローカル git と役割を分担しています。
設定とリポジトリの準備
localRepos.dir プロパティで clone の置き場所を指定します。
export localRepos.dir=/home/user/repos java -jar mcp-server.jar
localRepos.dir 配下にリポジトリ名でディレクトリを作り、clone しておきます。
# github.com/dummy-org/my-app の PR をレビューする場合 cd /home/user/repos git clone https://github.com/dummy-org/my-app
PR URL https://github.com/dummy-org/my-app/pull/42 を渡したとき、内部では ref.repo() が返す "my-app" をディレクトリ名として解決します。
/home/user/repos/my-app ← ここを参照
clone していないリポジトリの PR を渡すと、getDiff() が例外を投げてサービス層がエラーレスポンスに変換します。
{ "success": false, "error": "failed to get local diff: local repository not found: /home/user/repos/another-app" }
MCP ツール定義
@Tool( name = "getPrDiff", description = """ GitHub PRの unified diff を取得し、各ファイルについて 「コメント可能な RIGHT 側の行番号範囲」のサマリも返す。 reviewExamToPrPending を呼ぶ前に、この結果を参照して正しい path と line を決定してください。 """) public GetPrDiffService.Result getPrDiff( @ToolParam(description = "PR URL (https://github.com/{owner}/{repo}/pull/{pullNumber})") String prUrl) { ... }
description の中で「インラインコメントを投稿する前に必ずこれを参照すること」と指示しています。LLM へのコンテキスト注入はツールの description で行うのがポイントです。
テスト戦略
LocalGitDiffClient は ProcessBuilder 経由で git を呼ぶため、mock では検証しきれません。@TempDir で bare リポジトリ + ワークツリーをテスト内に構築し、実際に git clone → git push → getDiff() を呼ぶ統合テストにしました。
GetPrDiffService はサービス層なので GitHubApiClient と LocalGitDiffClient を Mockito で mock し、「PR メタデータ取得失敗」「ローカルリポジトリ未存在」「ブランチ情報欠落」などの各シナリオを単体テストで網羅しています。
動作フロー
1. Claude が getPrDiff("https://github.com/dummy-org/my-app/pull/42") を呼ぶ
2. PrUrlParser.parse() → PrRef(owner="dummy-org", repo="my-app", pullNumber="42")
3. GitHubApiClient で PR メタデータ取得
→ baseBranch="main", headBranch="feature/add-login"
4. LocalGitDiffClient.getDiff("my-app", "main", "feature/add-login")
4a. /home/user/repos/my-app が存在するか確認
存在しない → LocalGitException → Result(success=false, error="...")
4b. git fetch --quiet origin main feature/add-login
4c. git diff origin/main...origin/feature/add-login
5. DiffParser でコメント可能な行番号を抽出
→ { "src/Auth.java": [10-12, 15, 20-25], ... }
6. Result(success=true, diff="...", files=[...]) を返す
7. Claude は diff と行番号範囲を使ってレビューコメントを生成・投稿
まとめ
| 変更点 | 内容 |
|---|---|
| diff 取得方法 | GitHub API → ローカル git diff |
| レビュー対象の制限 | ローカルに clone 済みのリポジトリのみ(許可リスト不要) |
| 設定 | localRepos.dir プロパティで clone 置き場を指定 |
ローカル clone 経由にすることで GitHub API の制限を回避しつつ、「clone していないリポジトリはレビューできない」という制約が自然に生まれます。 明示的な許可リスト管理が不要になるのは副次的なメリットです。