前回は、PR本文、GitHub Issue、Backlog課題から要求を整理し、固定15観点で確認するPRレビュースキルを作りました。
第1~5回における、PRレビュースキルについては下記のリポジトリにありますので、よければ見てみてください
https://github.com/htakumi08/mcp-pr-review
今回やりたいのは、GitHubのPR URLを1つ渡すだけで、必要な情報を集め、Backlogの要求と差分を突き合わせ、レビュー結果をPRへ書き込むところまでつなげることです。
$pr-review を使って、次のPRをレビューしてください。
https://github.com/OWNER/REPOSITORY/pull/123
一見すると、最後にGitHubへコメントを投稿する処理を足せば終わりそうです。
しかし、実際に考えることはそれだけではありません。
- レビュー中にPRが更新されたら、古い指摘を投稿してよいのか
- 同じレビューをもう一度頼まれたら、同じコメントを投稿するのか
- Backlog MCPのtoolが読み込まれていなかったらどうするのか
- PR内で変更されたスクリプトを、そのPRのレビュー中に実行してよいのか
-
must (blocking)があったら、自動でREQUEST_CHANGESにするのか
「投稿できる」と「投稿してよい」は別の話でした。
今回は、レビュー結果をGitHubへ届けるまでの流れと、その途中で設けた安全条件を整理します。
今回完成させる流れ
最終的な処理は次のようになります。
PR URLを指定してレビューを依頼
↓
GitHubからPR本文・関連Issue・コメント・差分・CIを取得
↓
PR本文または関連IssueにBacklog URLがあるか確認
↓
URLがあれば get_issue_context(backlog_url) を呼ぶ
↓
GitHubとBacklogの情報から要求を整理
↓
要求と実装・テスト・設定・ドキュメントを突き合わせる
↓
固定15観点でレビュー結果を作る
↓
投稿直前にPR state・base SHA・head SHA・既存コメントを再確認
↓
PR Conversationへ通常コメントを投稿
↓
投稿されたコメントを再取得して確認
前半は情報を集める処理、中央は前回作ったレビュー判断、後半は安全に投稿する処理です。
順番に見ていきます。
PR URLがあれば自動で投稿するのか
今回のスキルでは、GitHub PR URLを指定したレビュー依頼を投稿モードとして扱います。
レビュー結果を作ったあと、追加で「投稿して」と言われるのを待たず、PR Conversationへ通常コメントを投稿します。
一方、次の依頼はプレビューモードです。
- ユーザーが「投稿しない」「プレビューのみ」と指定した
- PR URLのない一般的なコードレビュー
- ローカルのbranch、commit、差分を対象にしたレビュー
- 対象PRを一意に決められない依頼
自動投稿する条件を曖昧にすると、確認だけのつもりだったレビューが外部へ書き込まれるかもしれません。そのため、最初にモードを決めてから情報取得を始めます。
MCPサーバをこのプロジェクトで使えるようにする
まず、CodexからBacklog MCPサーバを起動できるようにします。
このプロジェクトでは、リポジトリ直下の .codex/config.toml にMCPサーバを登録しました。
# Project-scoped read-only Backlog MCP. Secrets remain in the gitignored .env file.
[mcp_servers.backlog]
command = "docker"
args = ["compose", "run", "--rm", "--no-deps", "-T", "backlog-mcp"]
cwd = "mcp-server-backlog"
enabled = true
required = false
enabled_tools = ["get_issue_context"]
default_tools_approval_mode = "approve"
startup_timeout_sec = 60
tool_timeout_sec = 60
ユーザー全体の設定ではなく、プロジェクト内の設定にしたのは、このMCPサーバがこのリポジトリの構成や .env に依存しているからです。
注目したいのは、公開するtoolを get_issue_context だけに絞っている点です。
第1回から扱ってきたとおり、このMCPサーバはBacklogの読み取り専用です。課題の更新やコメント投稿はできません。GitHubへの投稿も、MCPサーバではなくPRレビュースキル側が行います。
required = false にしているのは、Backlog URLがないPRならGitHubの情報だけでレビューできるためです。MCPサーバを起動できないだけで、すべてのレビュー依頼を失敗させる必要はありません。
ただし、Backlog URLがあるのに課題を取得できなかった場合は別です。この違いは後で扱います。
PR URLを受け取ったら、まず何を取得するのか
PRレビューなのだから、最初に差分を取ればよいのでしょうか。
今回のスキルでは、差分だけでなく次の情報を取得します。
- repository、PR番号、canonical URL
- PRのtitle、body、author、state
- base/headのbranch名とSHA
- changed filesとpatch
- commit
- Conversation上のコメント
- inline commentとsubmitted review
- 未解決のreview thread
- head SHAに対するCI/check
- mergeable state
- PR本文から直接参照されたGitHub Issue
ここでbase SHAとhead SHAも記録します。
repository: OWNER/REPOSITORY
pull_request_number: 123
reviewed_base_sha: 111111...
reviewed_head_sha: 222222...
base_ref: main
head_ref: feature/example
SHAまで持つのは、何のためでしょうか。
レビューは「PR番号123」だけに対するものではありません。その時点のbaseとheadの差分に対するものです。
レビュー中に新しいcommitがpushされれば、同じPR番号でも差分は変わります。PR番号だけを見て投稿すると、すでに直った問題を指摘したり、新しく入った変更を見ないまま「問題なし」と書いたりする可能性があります。
そのため、レビュー開始時点のSHAを固定しておきます。
また、ローカルに同じrepositoryがあっても、checkout中の内容が対象SHAと一致するとは限りません。一致を確認できないローカルファイルは、PR差分の根拠には使わないことにしました。
Backlog URLはどこから拾うのか
GitHubの情報を取得したら、Backlog URLを探します。
対象は次の2か所だけです。
- PR本文
- PRが直接参照しているGitHub Issueの本文
PRのコメント欄に書かれたURLは、自動では採用しません。
コメントまで無条件に対象にすると、レビュー途中で第三者が貼ったURLや、単なる参考リンクまで外部アクセスの入力になってしまうからです。
見つけたURLは、課題キーへ変換せず、そのままMCP toolへ渡します。
get_issue_context(
backlog_url="https://your-space.backlog.jp/view/PROJECT-123"
)
URLのscheme、host、port、pathを検証し、許可されたBacklogスペースの課題キーを取り出すのはMCPサーバの役割です。
スキル側で PROJECT-123 だけを抜き出してAPIへ渡す形にはしません。どのBacklogスペースのURLだったのかという検証材料が失われるためです。
toolが読み込まれていなかったらどうするか
ここで、実際に動かしていて困ったことがありました。
.codex/config.toml にMCPサーバを書いていても、そのセッションで get_issue_context がcallable toolとして読み込まれていない場合があります。
toolがないなら、Backlog APIを直接呼べばよいのでしょうか。
それでは、URL検証、ページング、partial result、secretを出さないエラー処理など、MCPサーバへまとめた境界を別経路で作り直すことになります。
そこで、登録済みのtoolを最優先にし、利用できない場合だけone-shotのfallback scriptを使うことにしました。
python3 mcp-server-backlog/scripts/run_issue_context.py \
"https://your-space.backlog.jp/view/PROJECT-123"
このスクリプトもBacklog APIを独自に呼ぶわけではありません。
Docker Composeで一時コンテナを起動し、stdio MCP clientから同じ get_issue_context を呼び出します。つまり入口が違うだけで、URL検証やBacklog取得の実装は同じMCPサーバを通ります。
通常:
Codex -> 登録済み get_issue_context -> Backlog MCP -> Backlog API
fallback:
one-shot script -> 一時コンテナの get_issue_context -> Backlog MCP -> Backlog API
「toolがなければ別実装へ切り替える」のではなく、「同じtoolへ到達する別の起動経路を用意する」という形です。
一時コンテナをどう片付けるか
one-shot scriptでは、毎回同じ名前のコンテナを使いません。
container_name = f"backlog-mcp-review-{uuid.uuid4().hex[:12]}"
imageがなければ初回だけbuildし、名前付きの一時コンテナを --rm で起動します。
docker compose images -q backlog-mcp
↓ imageがない場合だけ
docker compose build backlog-mcp
↓
docker compose run --rm --no-deps -T \
--name backlog-mcp-review-xxxxxxxxxxxx ...
さらに、正常終了でも異常終了でも、最後に名前を指定して削除を試みます。
try:
result = _run_command(command, ...)
finally:
_remove_container(project_dir, container_name)
--rm があるのに、なぜ明示的な削除も入れるのでしょうか。
timeoutや起動途中の失敗など、Compose側の自動削除だけでは後始末を確認しにくい経路があるためです。名前が分かっていれば、そのコンテナだけを対象にcleanupできます。
反対に、ここで docker compose down は使いません。
down は今回起動した一時コンテナだけでなく、同じCompose projectの共有networkや別の開発用コンテナまで巻き込む可能性があります。片付けたい対象より、操作の範囲が広すぎます。
PR内のスクリプトをそのまま実行してよいのか
fallbackには、もう1つ条件を付けました。
対象PRが次の場所を変更している場合、PRのhead側にあるfallback scriptやDocker構成は実行しません。
mcp-server-backlog/
.codex/config.toml
.agents/skills/pr-review/
レビュー対象のPRには、まだ信頼していないコードが含まれています。
そのコードを「レビューに必要だから」という理由で自動実行すると、PRを開いただけで任意の処理を実行できる経路になってしまいます。
この場合は、信頼済みの登録済み get_issue_context が使えるならそれを利用します。利用できなければBacklog取得失敗として扱い、head側のスクリプトを実行して解決しようとはしません。
同じ理由で、レビュー目的ではPR由来のbuild、test、Docker、Terraformも自動実行しません。CIの結果は確認しますが、自分で実行していない検証は未確認として残します。
ユーザーからテスト実行を別途依頼された場合は、その依頼の範囲で実行します。
GitHubとBacklogの情報をどう統合するか
Backlogの取得に成功したら、前回作った手順へ渡します。
GitHub
- PR本文
- 関連Issue
- discussion
- diff
- CI
Backlog
- 課題本文
- コメント
- 変更履歴
- 親・子・関連課題
- partial / warnings
↓
要求 R-001, R-002, ...
↓
実装・テスト・設定・ドキュメントと突合
↓
指摘 F-01, F-02, ...
大事なのは、情報を1つの文章へ混ぜて出所を消さないことです。
PR本文には「正常系を実装した」とあり、Backlogのコメントには「一般利用者からは実行できないこと」と書かれているかもしれません。両方を要求へ整理しつつ、どこから得た条件なのかは残します。
情報同士が矛盾する場合も、都合よく新しい方を正解にはしません。回答がなければ実装の妥当性を決められないものは、question (blocking) として確認します。
Backlog URLがない場合は、GitHubの情報だけで通常どおりレビューします。
一方、URLがあるのにMCP取得が失敗した場合や、retrieval.partial が true の場合は、「Backlog確認済み」とは扱いません。要求ベースの確認が完了できないなら、完全なレビューとしてGitHubへ自動投稿しないことにしました。
なぜPR Conversationへの通常コメントなのか
レビュー結果の標準投稿先は、PR Conversationの通常コメントにしました。
GitHubには、通常コメントのほかにReview submissionやinline commentがあります。
| 投稿方法 | 今回の扱い |
|---|---|
| PR Conversation通常コメント | 標準。5セクションの結果を1コメントにまとめる |
| Review submission | 権限がありAPIが成功する場合だけ COMMENT で使用する |
| Inline review comment | 正確なpatch行へ紐付けられる指摘だけ補助的に使う |
通常コメントなら、要求表、固定15観点、指摘詳細、未確認事項を1か所へまとめられます。
inline commentは具体的な行の議論には向いていますが、複数ファイルにまたがる問題や、CI、要求、テスト不足の指摘は1行へ結び付けにくいです。また、PR更新後のdiffでは行位置が変わることもあります。
そのため、inline commentを付ける場合でも、通常コメント側の15観点表から指摘を消さないようにしました。
blocking なら REQUEST_CHANGES にするのか
前回、指摘には次のラベルを付けました。
must (blocking)
question (blocking)
suggestion (non-blocking)
nitpick (non-blocking)
ここでいう blocking は、「merge前に修正または回答が必要」というレビュー上の分類です。
GitHubの REQUEST_CHANGES actionを自動で選ぶ、という意味ではありません。
今回のスキルは、次の操作を自動では行わないようにしました。
APPROVEREQUEST_CHANGES- merge
- branchの更新
Review submissionを使う場合もactionは COMMENT だけです。
レビューの指摘を作ることと、repository上の正式な承認状態を変更することは分けています。後者には、チームの運用ルールや担当者としての権限が関わるためです。
投稿直前に、なぜもう一度SHAを見るのか
レビューが完成したら、すぐに投稿したくなります。
しかし、ここでもう一度PRのstate、base SHA、head SHAを取得します。
レビュー開始
head = 222222...
↓
情報取得・要求整理・レビュー
↓
投稿直前
head = 222222... なら投稿可能
head = 333333... なら投稿しない
head SHAが変わっていたら、作成済みのレビューは古い差分に対するものです。そのまま投稿せず、新しい差分でレビューをやり直します。
PRがclosedまたはmergedになっていた場合も、自動投稿しません。
これは、レビュー処理と投稿処理の間にある競合をなくすための確認です。完全に同時更新を防げるわけではありませんが、少なくとも古いSHAだと分かっているレビューを投稿することは避けられます。
同じレビューを二重投稿しないために
SHAが変わっていなくても、すでに同じレビューを投稿済みかもしれません。
そこで、投稿本文の末尾にHTML comment形式のmarkerを付けます。
<!-- pr-review:v1 repo=OWNER/REPO pr=123 base=FULL_BASE_SHA head=FULL_HEAD_SHA -->
画面上では目立ちませんが、コメント本文を取得すれば、どのrepository、PR、base/head SHAに対するレビューか判定できます。
投稿前に既存のreviewとcommentを検索し、次がすべて一致するmarkerがあれば再投稿しません。
- 投稿者
- repository
- PR番号
- base SHA
- head SHA
投稿者も確認するのは、他者が同じ形式のmarkerを書いた場合に、それだけで「自分は投稿済み」と判断しないためです。
head SHAが変わればmarkerも変わるので、新しい差分に対するレビューは別物として扱えます。
投稿APIがtimeoutしたら再送してよいのか
投稿リクエストがtimeoutしたとき、単純に再送すると何が起きるでしょうか。
サーバ側では投稿に成功していて、応答だけ届かなかった可能性があります。その状態で再送すれば、同じコメントが2つ並びます。
そのため、timeoutやpartial failureではすぐに再投稿しません。
既存コメントを再取得し、予定していたmarkerと投稿者を確認します。
投稿結果が不明
↓
既存コメントを再取得
├─ markerあり -> 投稿成功として扱う
└─ markerなし -> 投稿結果不明として報告する
確認できない場合も、成功したふりはしません。投稿結果不明 としてユーザーへ伝えます。
投稿しない条件を先に決めておく
ここまでの条件をまとめると、次の場合は自動投稿しません。
| 条件 | 扱い |
|---|---|
| ユーザーが「投稿しない」「プレビューのみ」と指定 | チャットにだけ表示する |
| PR URLがなく、対象PRを一意に決められない | チャットにだけ表示する |
| PRがopenではない | 投稿しない |
| GitHubへの書き込み権限がない | 投稿せず、必要な権限を報告する |
| base/head SHAが変わった | 古い結果を捨て、新しい差分でレビューし直す |
| 同じmarkerの投稿がある | 重複投稿しない |
| Backlog取得が不完全で要求レビューを完了できない | 限定レビューをチャットへ表示する |
| 投稿結果を確認できない | 再送せず、結果不明と報告する |
Backlogを取得できないときも、GitHub情報だけで分かる範囲のレビューはできます。
ただし、それをBacklogまで確認した完全なレビューとして自動投稿しない、という線引きです。ユーザーが未確認範囲を理解したうえで限定レビューの投稿を改めて依頼した場合だけ、その範囲を目立つ形で書いて投稿します。
投稿後も確認して終わりにしない
コメント投稿APIが成功を返したら完了、としたくなりますが、最後にPR Conversationを再取得します。
確認するのは次の内容です。
- 投稿者が想定どおりか
- 5セクションの本文が欠けていないか
- markerが入っているか
- markerのbase/head SHAがレビュー対象と一致しているか
- inline commentを使った場合、予定した件数が投稿されたか
投稿前の検証と投稿後の確認を分けることで、「正しい内容を作った」と「GitHubへ正しく届いた」を別々に確認できます。
実際にどこまで確認したか
MCPサーバ側は、unit test、HTTP adapter test、stdio MCP integration test、one-shot lifecycle testに分けて確認しています。
cd mcp-server-backlog
docker compose run --rm --no-deps backlog-mcp pytest -q
実行結果は次のとおりでした。
......................................... [100%]
41件のテストがすべて成功しています。
このほか、実際のBacklog課題を使った取得、partial: false、一時コンテナが残らないことを確認しました。
PRレビュースキルについても、実際のPRで次の流れを確認しています。
GitHub PRを取得
↓
Backlog課題・コメント・変更履歴を取得
↓
要求と差分を突合
↓
固定15観点のレビュー結果を作成
↓
PR Conversationへ通常コメントを投稿
一方で、スキルの判断品質を同じ入力で継続比較する自動evalは、まだ用意できていません。
「動いたか」はテストできますが、「以前より良いレビューになったか」を機械的に比較する仕組みは別に必要です。今後、観点やプロンプトを変えていくなら取り組みたい部分です。
5回作ってみて分かったこと
第1回では、MCPサーバを「AIが外部の機能やデータを利用するための接続先」として整理しました。
第2回では、Pythonで get_issue_context を公開し、Backlog APIから課題を取得しました。
第3回では、MCPサーバは情報取得、スキルは判断という責務の境界を決めました。
第4回では、要求整理、固定15観点、ラベル、出力形式をPRレビュースキルへまとめました。
そして今回は、GitHubからの取得と投稿をつなぎ、PR URLからレビューコメントまでを一連の流れにしました。
最初は、「Backlog APIをAIから呼べればレビューに使えるのでは」と考えていました。
実際に作ってみると、APIを呼ぶ部分は全体の一部でした。
MCPサーバ
外部データを安全に取得し、不完全さも含めて返す
スキル
情報を読む順番、判断基準、出力形式、停止条件を決める
GitHub連携
レビュー対象を固定し、重複や更新競合を避けて結果を届ける
この3つを分けたことで、Backlog MCPサーバにレビュー判断を持たせず、PRレビュースキルにBacklog APIの通信処理を持たせずに済みました。
もう1つ分かったのは、自動化では成功時の流れより、止まる条件を決めることが大切だという点です。
Backlogを取得できない。PRが更新された。投稿済みか分からない。レビュー対象のコードを実行しないとfallbackできない。
こうした場面で無理に先へ進まず、「何を確認できていないか」を残して止まれることが、レビュー結果の信頼性につながります。
これで、自作MCPサーバを使ったPRレビュースキルの実装はひと区切りです。
完成したのは、何でも自動で判断してくれるレビュアーではありません。
外部情報を取る場所、判断する手順、GitHubへ書き込む条件を明示し、確認できなかったことを確認済みにしないレビューの仕組みです。