MCPサーバーで自動PRレビュー 第三弾:ローカルリポジトリでdiff取得 & レビュー対象の絞り込み

前回の記事では、MCPサーバーに GitHub PR のコメント取得・レビュー投稿ツールを実装しました。

今回は実運用で見えてきた課題を解決する二つの改善を加えます。

  1. diff 取得をローカル clone 経由に変更する
  2. 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 fetchgit 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 で行うのがポイントです。


テスト戦略

LocalGitDiffClientProcessBuilder 経由で git を呼ぶため、mock では検証しきれません。@TempDir で bare リポジトリ + ワークツリーをテスト内に構築し、実際に git clonegit pushgetDiff() を呼ぶ統合テストにしました。

GetPrDiffService はサービス層なので GitHubApiClientLocalGitDiffClient を 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 していないリポジトリはレビューできない」という制約が自然に生まれます。 明示的な許可リスト管理が不要になるのは副次的なメリットです。

誤解を生みそうだ。意味あるWhyの回答の説明文を書こう、ノットコメント

前回の記事では「コードにコメントは必要か?」にたいして「不要」っと記載したが、そもそもコメントとは?っと誤解されそうなので。

コードを読めばわかること(What/How)ではなく、コードを読んでもわからないこと(Why)は記載しよう。

  • 「なぜ」そのアルゴリズムを選んだのか
  • 「なぜ」あえて標準的ではない書き方をしているのか
  • 「なぜ」このチェックを省略できたのか

具体的には

https://github.com/openjdk/jdk/blob/90d21426d35e78b342f4126a02721e79c5916c0f/src/java.base/share/classes/java/lang/String.java#L4

を見てほしい。一度はみんな読むString.java

すべて理解できてないけど、説明文の書き方が参考になるなと思っている。

AIにコメント書いて -> コードを読めばわかること(What/How)書き出すので、コードを読んでもわからないこと(Why)は人間が記載しよう。

整形はAIにおまかせ。

「コードにコメントは必要か?」という議論。実は母国語の問題ではないかという仮説

「コメント不要論(コードが説明的であるべき)」「コメント必要論」

「良いコードならコメントがなくても読めるはずだ」と言う人もいれば、「いや、日本語の補足がないと意図が伝わらない」と言う人もいます。

「そもそもプログラミング言語が日本語(母国語)で書かれていないから、コメントが必要になるのではないか?」

っと思っています。


1. 一般的なプログラミング(英語命名 + 日本語コメント)

/**
 * 商品価格の計算管理クラス
 */
public class PriceCalculator {

    // 消費税率(10%)
    private static final double TAX_RATE = 0.10;

    /**
     * 税抜き価格から税込み価格を計算する
     * @param basePrice 税抜き価格
     * @return 税込み価格(整数)
     */
    public static int calculateTotalPrice(int basePrice) {
        // 税額を計算
        double taxAmount = basePrice * TAX_RATE;
        
        // 元の価格に税額を足して計算結果を返す
        return (int) (basePrice + taxAmount);
    }

    public static void main(String[] args) {
        int price = 1000;
        int totalPrice = calculateTotalPrice(price);
        System.out.println("合計: " + totalPrice + "円");
    }
}

2. 日本語プログラミング(日本語命名 + コメントなし)

public class 価格計算機 {

    private static final double 消費税率 = 0.10;

    public static int 税込み価格を計算する(int 税抜き価格) {
        double 税額 = 税抜き価格 * 消費税率;
        
        return (int) (税抜き価格 + 税額);
    }

    public static void main(String[] 引数) {
        int 元の価格 = 1000;
        int 最終的な税込み価格 = 税込み価格を計算する(元の価格);
        System.out.println("合計: " + 最終的な税込み価格 + "円");
    }
}

3. 一般的なプログラミング(英語命名 + 英語コメント)

/**
 * Class to manage price calculations.
 */
public class PriceCalculator {

    // Consumption tax rate (10%)
    private static final double TAX_RATE = 0.10;

    /**
     * Calculates the total price including tax.
     * @param basePrice Price excluding tax
     * @return Total price including tax
     */
    public static int calculateTotalPrice(int basePrice) {
        // Calculate tax amount
        double taxAmount = basePrice * TAX_RATE;
        
        // Return base price plus tax amount
        return (int) (basePrice + taxAmount);
    }

    public static void main(String[] args) {
        int price = 1000;
        int totalPrice = calculateTotalPrice(price);
        System.out.println("Total: " + totalPrice + " JPY");
    }
}

4. 日本語プログラミング(日本語命名 + 日本語コメント)

/**
 * 価格計算機クラス
 */
public class 価格計算機 {

    // 消費税率(10%)
    private static final double 消費税率 = 0.10;

    /**
     * 税込み価格を計算する
     * @param 税抜き価格 税抜き価格
     * @return 最終的な税込み価格
     */
    public static int 税込み価格を計算する(int 税抜き価格) {
        // 税額を計算
        double 税額 = 税抜き価格 * 消費税率;
        
        // 税抜き価格と税額を足す
        return (int) (税抜き価格 + 税額);
    }

    public static void main(String[] 引数) {
        int 元の価格 = 1000;
        int 最終的な税込み価格 = 税込み価格を計算する(元の価格);
        System.out.println("合計: " + 最終的な税込み価格 + "円");
    }
}

5. 日本語プログラミング(日本語命名 + 英語コメント)

/**
 * Class to manage price calculations.
 */
public class 価格計算機 {

    // Consumption tax rate (10%)
    private static final double 消費税率 = 0.10;

    /**
     * Calculates the total price including tax.
     * @param 税抜き価格 Price excluding tax
     * @return Total price including tax
     */
    public static int 税込み価格を計算する(int 税抜き価格) {
        // Calculate tax amount
        double 税額 = 税抜き価格 * 消費税率;
        
        // Return base price plus tax amount
        return (int) (税抜き価格 + 税額);
    }

    public static void main(String[] 引数) {
        int 元の価格 = 1000;
        int 最終的な税込み価格 = 税込み価格を計算する(元の価格);
        System.out.println("Total: " + 最終的な税込み価格 + " JPY");
    }
}

見慣れないなってコードは思うけど、読むのなら、結局日本人で日本語がメインだから、日本語あってほしいよね?

英語理解できたら英語だけでいいし。

つまり、自分はコメント不要です。

設計=コメント=実装=テスト

より

設計=実装=テスト

と一個でも減らして管理コスト減らしたい人です。

ポート開放不要のローカルLLM連携 実装編 — Google Chat × Pub/Sub × Spring Boot で自動PRレビューを動かす

前回の記事では、ポート開放も Tailscale も不要なアーキテクチャの設計と GCP 側のインフラ構築(Pub/Sub トピック/サブスクリプション、Chat API 設定、IAM 権限)を解説しました。

今回は 実装編として、ローカルPC上で動く Spring Boot アプリがどのように Pub/Sub からメッセージを受け取り、ローカル LLM(LM Studio)を使って PR レビューを自動化するかを説明します。


システム全体の流れ(おさらい)

Google Chat
   │  メンションやURLを投稿
   ▼
Pub/Sub Topic
   │  メッセージを蓄積(最大7日間)
   ▼
ローカルPC (batch アプリ)
   │  Pull でメッセージを取得
   ├─► LM Studio (ローカルLLM) でレビュー生成
   └─► GitHub / Backlog PR へ投稿
          │
          ▼
       Google Chat Webhook で完了通知

前回構築したインフラの上に、今回は batch アプリの部分を載せます。


プロジェクト構成

マルチプロジェクト構成の Gradle プロジェクトになっています。

my-tool/
├── web/        # Webサーバー(直接、呼び出す)
├── mcp/        # STDIO MCP サーバ(mcpクライアントから呼ばれる)
├── batch/      # バッチとして、Pub/Sub サブスクライバする
└── module/     # 共有するドメイン
        ├── domain
        ├── service
        ├── repository
        └── persistence

今回フォーカスするのは batch モジュールです。mcp モジュールは Claude Desktop や LM Studio の MCP ツールとして動作するもので、別の記事で詳しく取り上げる予定です。


batch アプリの仕組み

1. Pub/Sub からメッセージを Pull する — ライブラリと実装

依存関係の追加

Google の公式 Java ライブラリを使います。Spring Boot の BOM には含まれていないため、バージョンを直接指定します。

// batch/build.gradle
dependencies {
    implementation 'com.google.cloud:google-cloud-pubsub:1.142.0'
}

Subscriber の組み立て

Subscriber.newBuilder() にサブスクリプション名と MessageReceiver を渡すだけで基本的な Pull サブスクライバが完成します。

ProjectSubscriptionName subscriptionName =
    ProjectSubscriptionName.of(projectId, subscriptionId);

MessageReceiver receiver = (message, consumer) -> {
    String payload = message.getData().toStringUtf8();
    // 処理 ...
    consumer.ack();   // 成功 or 失敗に問わず消す
};

Subscriber subscriber = Subscriber.newBuilder(subscriptionName, receiver).build();
subscriber.startAsync().awaitRunning();

ローカル LLM に合わせた直列処理設定

LM Studio の推論は1リクエストあたり数十秒〜数分かかります。デフォルトだと複数メッセージが並行処理されてしまうため、1件ずつ順番に処理するよう FlowControl と Executor を絞ります。

ack / nack の判断基準

リッチにせず、成功しても失敗してもメッセージを消す。 chatに何が起きたか返す chatに出せなければログに

2. Google Chat のイベント JSON をそのまま処理できる

Google Chat Bot は以下の形式で Pub/Sub にメッセージを送ります。

{
  "chat": {
    "messagePayload": {
      "message": {
        "argumentText": " https://github.com/owner/repo/pull/123 をレビューして",
        "text": "@bot https://github.com/owner/repo/pull/123 をレビューして"
      }
    }
  }
}

batch アプリはこの JSON から PR の URL を自動抽出します。以下の順でフィールドを探し、最初に見つかったURLを使います。

  1. argumentText
  2. formattedText
  3. text

プレーンな PR URL や独自の JSON 形式も受け付けるので、Google Chat 以外の連携にも使えます。

3. ローカル LLM(LM Studio)でレビューを生成する

PR の URL が特定できたら、

  1. GitHub / Backlog から PR の diff とコメントを取得
  2. LM Studio の OpenAI 互換 API へプロンプトを送信
  3. レビュー結果(指摘コメント + インラインコメント)を受け取る

LM Studio は http://localhost:1234 で OpenAI 互換 API を提供しているため、モデルを切り替えても設定変更だけで対応できます。

# 環境変数で設定
export LM_STUDIO_BASE_URL=http://localhost:1234
export LM_STUDIO_MODEL=qwen3.6-35b-a3b

4. PR へ投稿してチャットに完了通知を送る

投稿が完了すると Google Chat の Webhook 経由で通知が届きます。


5. AIレビューツール比較マトリクス

「ローカルLLMでいいのか?」という疑問に答えるため、主要な AI レビューツールを比較します。

凡例: ◎ 優秀 / ○ 良好 / △ 制限あり / × 非対応

比較項目 GitHub Copilot Claude Code Review Codex (OpenAI) Gemini Code Assist ローカルLLM(本構成)
レビュー精度
セキュリティ脆弱性の指摘
PR メンションで自動レビュー ◎(@claude / Actions) ◎(@codex review ○(PR作成時に自動実行) ×
インラインコメント自動投稿
投稿前の内容確認 △(自動投稿) △(自動投稿) △(自動投稿) △(自動投稿) (カスタム可能)
プロンプトのカスタマイズ なんでもできる
コスト 有料(月額固定) 約$15〜25/レビュー 従量課金 無料枠あり 無料(電気代のみ)
コードのプライバシー
日本語対応
ローカル・オフライン動作 × × × ×
対応プラットフォーム GitHub のみ GitHub のみ GitHub 主体 GitHub + GitLab なんでも可能
レート制限 なし
モデルの差し替え容易性 × × × ×
  • コードのプライバシーは、規約に依存しますが、ローカルLLMは完全に社内で完結するため安心です。ただ、モデルの学習データにコードが含まれている可能性があるため、機密コードを扱う場合は注意が必要です(ほぼない)。

まとめ

今回実装したのは以下の3点です。

  1. Spring Boot batch アプリが Pub/Sub を Pull して PR URL を自動抽出
  2. LM Studio の OpenAI 互換 API を呼んでローカル LLM でレビューを生成
  3. 結果を GitHub / Backlog に投稿し、Google Chat で完了通知

次回はレビュー精度に直結する プロンプトの設計を深掘りします。diff をそのまま渡すのか要約するのか、システムプロンプトに何を含めるか、インラインコメントをどう指示するかなど、ローカル LLM ならではのチューニングポイントを紹介する予定です。MCP モジュールの解説はその次を予定しています。

ポート開放もTailscaleも不要!Google Chat × Pub/Sub でローカルLLM連携のセキュアなインフラを構築する

なぜこの構成なのか?(他アーキテクチャとの比較)

ローカルPCで動かすLLM(Ollama等)を外部から利用しようとした場合、チャットツールごとに通信方式やタイムアウトの仕様が大きく異なります。

今回は代表的なツールやフレームワーク(LINE、Discord、Slack、OpenClaw)を用いた連携パターンと、本記事の「Google Chat + Pub/Sub」構成を具体的な軸で比較しました。

比較構成 難易度 通信 費用 復旧 補足
LINE/Slack/Discord + Local PC
(Webhook / WebSocket型)
★★★☆☆
(普通)
★☆☆☆☆
(設定次第)
★★★☆☆
(無料枠少)
★☆☆☆☆
(消失)
PC切断中のメッセージは受け取れない。
Discord等 + OpenClaw(Local PC)
(OSSフレームワーク)
★★★★★
(超簡単)
★★★★☆
(ツール準拠)
★★★★★
(無料)
★★☆☆☆
(ツール依存)
OpenClaw怖い
Chat + Pub/Sub + Local PC ★★☆☆☆
(IAMの罠あり)
★★★★★
(超安全)
★★★★★
(ほぼ0円)
★★★★★
(自動復旧)
設定大変。

Google Chat + Pub/Sub + Local PC

Google Chatの入力をGCPの「Pub/Sub(メッセージキュー)」に一旦預け、自宅PCが自分のタイミングで「Pull(取得)」しに行く構成です。

  • セキュア通信(ポート開放・VPN不要): PCからGCPへデータを取りに行く「Pull型」のため、自宅ルーターの穴あけも、TailscaleのようなVPNの常時起動も一切不要です。
  • PCの電源を切っていてもOK: PCをスリープさせている間に話しかけられても、メッセージは失われません。 Pub/Subがクラウド上でデータを最大7日間保持してくれるため、帰宅してPCの電源を入れた瞬間に、溜まっていたメッセージを一気に処理(復旧)してくれます。

気になる「料金」について(個人アカウントでも無料?)

「Google Workspace(有料)やGCPの契約が必要なら高くなりそう…」と思われがちですが、無料の個人用 Google アカウント(@gmail.com)でも構築可能であり、クラウド費用も実質0円に収まります。

  • Google Chat: 個人アカウントでも無料で利用できます。
  • GCP Pub/Sub: 毎月 最初の 10 GB まで無料(以降、データ量に応じた従量課金)。Chatのテキストメッセージ(1通数KB)であれば、月に数百万回やり取りしてようやく無料枠に届くレベルです。
  • LLMの推論: 自分のPCのGPU/CPUで動かすため、OpenAIなどのAPI利用料は1円もかかりません。

つまり、かかるコストは**「自分のPCの電気代だけ」**。


手順1:Google Cloud Pub/Sub の作成

まずは、メッセージの受け皿となるメッセージキューを作成します。

  1. Google Cloud コンソールで [Pub/Sub] > [トピック] を開きます。
  2. 「トピックを作成」をクリックし、トピックIDを入力します(例: start-to-work)。
  3. 「デフォルトのサブスクリプションを追加する」にチェックを入れたまま、作成をクリックします。

これにより、メッセージの入り口(トピック)と出口(サブスクリプション)がセットで作成されます。この時点では外部からのアクセスは完全に遮断された状態です。


手順2:Google Chat API の設定

次に、Google Chatからメッセージを送り込むためのボットを設定します。

  1. GCPコンソールで 「Google Chat API」 を検索し、有効化します。
  2. 左メニューの 「構成 (Configuration)」 タブを開き、ボット名やアイコンを設定します。
  3. 「インタラクティブ機能」 をオンにし、以下の接続設定を行います。
    • 接続設定: Cloud Pub/Sub を選択。
    • Cloud Pub/Sub トピック名: 先ほど作成したトピックのフルパスを入力します。 (形式: projects/[自分のプロジェクトID]/topics/start-to-work
  4. 可視性の設定で、テストするユーザー(自分)を追加して「保存」します。

手順3:【要注意】IAM権限の罠を回避する

正しいサービスアカウントの特定と権限付与

  1. 手順2で開いた Google Chat API の「構成」画面の「接続設定」の項目をよく見ると、入力したトピック名のすぐ上に 「サービス アカウントのメール」 が表示されています。
  2. Pub/Sub の 「トピック」 画面に戻り、作成したトピックを開きます。
  3. 右側の情報パネル(権限タブ)で 「プリンシパルを追加」 をクリックします。
  4. 新しいプリンシパル: 先ほどコピーしたアドレスを貼り付けます。
  5. ロール: Pub/Sub 編集者 を選択し、保存します。

※重要: サブスクリプション側(XXX-sub)ではなく、必ず入り口である 「トピック側」 に対して権限を付与してください。


手順4:開通テスト

インフラが正しく繋がっているか確認します。

  1. Google Chat を開き、作成したボットと 1対1のDM(ダイレクトメッセージ) を開始します。 (※グループチャットのスラッシュコマンド等を使うと原因の切り分けが難しくなるため、最初は必ずDMでテストします)
  2. 「テスト送信」とメッセージを送ります。
  3. GCPコンソールの [Pub/Sub] > [サブスクリプション] から作成したサブスクリプションを開きます。
  4. 「メッセージ」タブ を開き、「PULL」 ボタンをクリックします。

表の中に、自分が送ったメッセージを含むJSONデータが表示されれば、Google Chat からPub/Subまでのトンネル開通は大成功です!


まとめ

これで Google Chat からのメッセージが、リアルタイムかつ安全にクラウド上のキューに溜まるようになりました。複雑なVPN構築やポート開放は一切必要ありません。

あとは、ローカルPCからこのサブスクリプションを Subscribe (Pull) してメッセージを読み取り、ローカルLLMに処理させるだけです。

次回は、このメッセージを実際にローカルLLM(Ollama等)に渡し、Google Chat APIを叩いてチャットルームへ「返信」を書き込む実装についてまとめたいと思います。

LM StudioからGitHubのPRレビューをやってみた 〜コメントも残せる自作MCPサーバー

はじめに

LM Studio + 自作MCPサーバーで、PRの内容取得からインラインコメント投稿までを完結させる環境を作りました。

ローカルLLMなので、無限に回せます。寝てる間にレビューさせられますね。(精度は考えないといけない)

本記事では、次のことを紹介します。

  • なぜLM Studio + MCPなのか
  • 自作MCPサーバー(Spring Boot製)が提供する3つのツール
  • 実際のレビューの流れ(diff取得 → レビュー生成 → コメント投稿)

なぜLM Studio + MCPなのか

  • ローカルで動く LLMにレビューさせたい
  • モデルを自由に差し替えたい(Qwen、Llama、DeepSeek Coder など)
  • コメントの最終投稿は人間が承認してから行いたい(暴走防止)

投稿するレビューはPENDING状態で作成するので、人間がGitHub上で内容を確認してから「Submit review」を押すまで、相手には一切通知されません。安全にAIレ ビューを試せる仕組みです。


自作MCPサーバーが提供する3つのツール

Spring BootとSpring AIの @Tool アノテーションで、3つのツールを公開しています。

ツール 役割
fetchPrComments PR本文・Issueコメント・インラインコメントを取得
getPrDiff 統一diffと、コメント可能な行範囲(RIGHT側)を返す
reviewExamToPrPending PENDING状態のレビュー(本文+インラインコメント)を投稿

細かい実装は省略します。

プロジェクト構成(レイヤードアーキテクチャ)

com.example.demo/
├── domain/         # 値オブジェクト、diffパーサ、URLパーサ
├── repository/     # GitHub REST APIクライアント
├── service/        # 土管
└── mcp/            # @Toolアダプタ

ドメイン層とリポジトリ層を分離しているので、GitHub API以外(GitLab等)への拡張もやりやすい構成になっています。


セットアップ

1. ビルド

./gradlew build

build/libs/my-mcp-server-0.0.1-SNAPSHOT.jar ができます。

2. GitHubトークンの用意

repo スコープ付きのPersonal Access Tokenを発行しておきます。

3. LM Studioからの接続

LM Studioの設定ファイル(mcp.json)に、サーバーをSTDIOで起動するエントリを追加します。

{
  "mcpServers": {
    "pr-review": {
      "command": "java",
      "args": ["-jar", "/absolute/path/to/my-mcp-server-0.0.1-SNAPSHOT.jar"],
    }
  }
}

LM Studioを再起動すると、チャット画面のツール一覧に fetchPrComments / getPrDiff / reviewExamToPrPending の3つが出てきます。


実際のレビューフロー

LM Studioのチャットに、こう投げます。

このPRをレビューしてほしい: https://github.com/owner/repo/pull/123

モデルは内部で次の順に動きます。

  1. getPrDiff を呼んでdiffを取得
  2. fetchPrComments で既存のレビュー状況を確認
  3. ビュー文面とインラインコメントを生成
  4. reviewExamToPrPending で投稿

ハマりどころ

インラインコメントが 422 で弾かれる

line がdiffに含まれない行を指している。getPrDiffcommentableLines を必ずモデルに渡すこと。

PENDINGレビューが重複する

→ 既存PENDINGを削除してから投稿するオプションを持たせてあります。


おわりに

LM Studio + 自作MCPサーバーで、ローカルLLMによるGitHub PRレビュー → インラインコメント投稿までを自動化できました。

精度を考えないといけないので100%信じてはいけませんが、 1. 静的解析 2. 表記ゆれや小さい修正

系は一旦良さそうです。細かいのを潰して、claude codeやcodexに最終レビューと、humanの最終レビューを投げると良いでしょう。


参考

GitHub Actionsでシンプルなリリースフローを考えてみた

はじめに

リリースフローの設計って、調べると「semverでメジャー・マイナー・パッチを管理して、Git-flowでブランチ運用して...」みたいな話がたくさん出てきます。

でも正直、そこまで必要ですか?

https://github.com/release-drafter/release-drafter

上記を見習ってリリースフローをシンプルに考えます。

前提

  • Java + Jib(コンテナイメージのビルド)
  • ECS(デプロイ先)
  • バージョンはv1, v2, v3... の単純な連番
  • GitHub Releases の draft / pre-release / release を活用

全体像

4つの環境を、GitHubのリリース状態と対応させています。 github actionsから操作します。 draftが常に1つしかない想定。自動で追加される。

env GitHub Releases info
dev - 開発者主体で勝手にやる、タイミングお任せ
test draft テストしたいタイミングでやる
stg(あれば) pre-release draftタグを指定する。同じdocker imageををstgに出してpre-releaseに変更
prod release (latest) pre-release or releaseタグ を指定する。同じdocker imageタグをprodに出してrelease(latest)に変更

各環境の役割

dev:ブランチ確認 & 既存バージョンの確認

PRのブランチをそのままデプロイして動作確認する環境です。workflow_dispatch で手動実行します。

GitHub Actionsの「Run workflow」画面で「Use workflow from」のドロップダウンからブランチとタグの両方を選べます

  • ブランチを選択 → チェックアウト & Jibビルド & デプロイ
  • タグを選択 → ビルドスキップ、既存イメージでそのままデプロイ

test:検証環境

dev同様。基本はdraftを上げて確認する環境。

stg:社内向け本番環境(あれば)

GitHub Actions から手動でワークフローを実行する。 対象のdraftタグに紐づくdocker imageをstg環境にデプロイして、成功するとpre-releaseに昇格します、

draftが存在しなければ処理をスキップします。

prod:本番リリース

GitHub Actions から手動でワークフローを実行する。 対象のpre-releaseに紐づくdocker imageをprod環境にデプロイして、成功するとpre-releaseに昇格します、 revertようにreleaseタグも選択できるように。

GitHub Actionsの Environment protection rules を使って、第三者の承認がないとデプロイできないようにしています。承認者がApproveしない限り、ワークフローは止まったまま進みません。

社内向けにもっと簡潔に

1. バージョニングは単純でいい

v1, v2, v3...の連番で十分です。ECSのtasc defもそうなってるよね?

2. 同じイメージを昇格させる

うん、、、そうだよね、外部から環境変数入れるのはこういうことだよね

3. github actionsから操作

githubにロックインされるけど良くない?

4. タグが多くなる

ええやん、社内だし。リリース前にリリースしたくなってv上がってもええやん。pre-releaseのまま放置もええやん。

本番リリース履歴の確認方法

ECSのデプロイ履歴みてください

まとめ

リリースフローは凝ろうと思えばいくらでも凝れますが、そんな頑張りたくない。

のでシンプルに