GitHubでAIを強化する | 第2章：タスクを自動化：AIによるIssueとPR操作

## はじめに

第1章では、**GitHub**と**Model Context Protocol（MCP）**の基本を学び、GitHubリポジトリからIssueとコミットデータを取得するMCPサーバーを構築しました。これにより、AIがリポジトリの情報を取得できるようになりました。今回は、この基盤を進化させ、GitHubにIssueを作成したり、Pull Request（PR）にコメントしたりする機能を実装します。これにより、**タスクを自動化**するエージェントを構築します。

この第2章では、MCPサーバーを通じてAIがIssueにラベルを付けたり、PRにコメントを追加したりできるようにします。たとえば、ユーザーが特定のキーワードを含むIssueを作成すると、AIが自動で適切なラベル（例：「bug」）を付けることができます。コード例とステップごとのガイドで、開発タスク自動化の可能性を体感しましょう。さあ、始めましょう！

## タスク自動化エージェントとは？

タスク自動化エージェントは、GitHubのIssueやPRを操作し、開発プロセスを効率化するAIです。MCPサーバーを介して、以下のような機能を実現できます：
- **Issue管理**：Issueの作成、ラベル付与、コメント追加。
- **PR操作**：PRへのコメント追加、レビューリクエスト。
- **自動化ルール**：特定の条件（例：キーワードやユーザー）に基づいてアクションを実行。

### ユースケース
- バグ管理：AIが「エラー」キーワードを含むIssueに「bug」ラベルを自動付与。
- コードレビュー：AIがPRに初期コメントを追加し、レビュアーに通知。
- プロジェクト管理：AIが期限付きIssueを作成し、チームに割り当て。

## 開発環境の準備

第1章の環境を基に、以下の準備を行います：
- **Python 3.8以降**、**mcpライブラリ**、**requestsライブラリ**、**Claude Desktop**：第1章と同じ。
- **python-dotenv**：環境変数の管理（既にインストール済み）。
- **GitHubリポジトリ**：IssueやPRを操作するための設定。

### GitHubのセットアップ
1. **GitHub Personal Access Tokenの拡張**：
   - 第1章のトークンを使用。
   - スコープを確認：`repo`（Issue、PR、コミット操作）、必要に応じて`admin:repo_hook`（Webhook用）。
   - トークンがIssueとPRの書き込み権限を持つことを確認。
2. **リポジトリ準備**：
   - 第1章のリポジトリ（例：`yourusername/mcp-test-repo`）を使用。
   - 新しいブランチを作成し、PRを1つ作成（例：「機能追加」ブランチ）。
   - Issueラベルを追加（例：「bug」「enhancement」）。
3. **環境変数**：
   第1章の`.env`ファイル（`GITHUB_TOKEN`、`GITHUB_REPO`）を再利用：
   ```bash
   GITHUB_TOKEN=your_token
   GITHUB_REPO=yourusername/mcp-test-repo
   ```

## コード例：タスク自動化用MCPサーバー

以下のMCPサーバーは、GitHubからIssueやコミットを取得し、Issue作成やPRコメント追加の機能を提供します。

```python
from mcp import MCPServer
import os
from dotenv import load_dotenv
import requests

class GitHubTaskServer(MCPServer):
    def __init__(self, host, port, token, repo):
        super().__init__(host, port)
        self.token = token
        self.repo = repo
        self.base_url = "https://api.github.com"
        self.headers = {
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github.v3+json"
        }
        self.register_resource("get_issues", self.get_issues)
        self.register_tool("create_issue", self.create_issue)
        self.register_tool("add_pr_comment", self.add_pr_comment)

    def get_issues(self, params):
        try:
            url = f"{self.base_url}/repos/{self.repo}/issues"
            query = {"state": params.get("state", "open"), "per_page": params.get("limit", 10)}
            response = requests.get(url, headers=self.headers, params=query)
            response.raise_for_status()
            issues = response.json()
            issue_list = [
                {
                    "title": issue["title"],
                    "number": issue["number"],
                    "state": issue["state"],
                    "creator": issue["user"]["login"],
                    "created_at": issue["created_at"]
                }
                for issue in issues
            ]
            return {"status": "success", "issues": issue_list}
        except Exception as e:
            return {"status": "error", "message": str(e)}

    def create_issue(self, params):
        try:
            title = params.get("title", "")
            body = params.get("body", "")
            labels = params.get("labels", [])
            if not title:
                return {"status": "error", "message": "タイトルが必要です"}
            
            url = f"{self.base_url}/repos/{self.repo}/issues"
            payload = {
                "title": title,
                "body": body,
                "labels": labels
            }
            response = requests.post(url, headers=self.headers, json=payload)
            response.raise_for_status()
            issue = response.json()
            return {"status": "success", "issue_number": issue["number"]}
        except Exception as e:
            return {"status": "error", "message": str(e)}

    def add_pr_comment(self, params):
        try:
            pr_number = params.get("pr_number", 0)
            comment = params.get("comment", "")
            if not pr_number or not comment:
                return {"status": "error", "message": "PR番号とコメントが必要です"}
            
            url = f"{self.base_url}/repos/{self.repo}/issues/{pr_number}/comments"
            payload = {"body": comment}
            response = requests.post(url, headers=self.headers, json=payload)
            response.raise_for_status()
            comment = response.json()
            return {"status": "success", "comment_id": comment["id"]}
        except Exception as e:
            return {"status": "error", "message": str(e)}

if __name__ == "__main__":
    load_dotenv()
    server = GitHubTaskServer(
        host="localhost",
        port=8128,
        token=os.getenv("GITHUB_TOKEN"),
        repo=os.getenv("GITHUB_REPO")
    )
    print("GitHubタスクMCPサーバーを起動中: http://localhost:8128")
    server.start()
```

### コードの説明
- **get_issues**：第1章から再利用。リポジトリのIssueを取得。
- **create_issue**：新しいIssueを作成。タイトル、本文、ラベルを指定。
- **add_pr_comment**：指定したPRにコメントを追加。PR番号とコメント内容を指定。
- **register_resource/tool**：Issue取得をリソース、Issue作成・PRコメントをツールとして登録。

### 前提条件
- GitHubトークンに`repo`スコープが付与されている。
- リポジトリにPRとラベル（例：「bug」「enhancement」）が存在。
- `.env`ファイルに正しい`GITHUB_TOKEN`と`GITHUB_REPO`が設定済み。

## サーバーのテスト

サーバーが正しく動作するか確認します：

1. **サーバー起動**：
   ```bash
   python github_task_server.py
   ```
   コンソールに「GitHubタスクMCPサーバーを起動中: http://localhost:8128」と表示。

2. **Issue作成のテスト**：
   Pythonでリクエストを送信：
   ```python
   import requests
   import json

   url = "http://localhost:8128"
   payload = {
       "jsonrpc": "2.0",
       "method": "create_issue",
       "params": {
           "title": "新しいバグ報告",
           "body": "アプリがクラッシュする問題が発生。",
           "labels": ["bug"]
       },
       "id": 1
   }
   response = requests.post(url, json=payload)
   print(json.dumps(response.json(), indent=2, ensure_ascii=False))
   ```
   期待されるレスポンス：
   ```json
   {
     "jsonrpc": "2.0",
     "result": {
       "status": "success",
       "issue_number": 3
     },
     "id": 1
   }
   ```

3. **PRコメント追加のテスト**：
   ```python
   payload = {
       "jsonrpc": "2.0",
       "method": "add_pr_comment",
       "params": {
           "pr_number": 1,
           "comment": "コードレビュー：良好ですが、ドキュメントを追加してください。"
       },
       "id": 2
   }
   response = requests.post(url, json=payload)
   print(json.dumps(response.json(), indent=2, ensure_ascii=False))
   ```
   期待されるレスポンス：
   ```json
   {
     "jsonrpc": "2.0",
     "result": {
       "status": "success",
       "comment_id": 123456789
     },
     "id": 2
   }
   ```

## Claude Desktopとの接続

サーバーをClaude Desktopに接続します：

1. **設定ファイルの編集**：
   Claude Desktopの設定ファイル（例：`claude_desktop_config.json`）に以下を追加：
   ```json
   {
     "mcp_servers": [
       {
         "name": "GitHubTaskServer",
         "url": "http://localhost:8128",
         "auth": "none"
       }
     ]
   }
   ```

2. **Claudeでテスト**：
   Claude Desktopを起動し、プロンプトを入力：
   ```
   リポジトリに「新しいバグ報告」というIssueを作成し、「bug」ラベルを付けてください。
   ```
   レスポンス例：
   ```
   Issue #3「新しいバグ報告」を作成し、「bug」ラベルを付けました。
   ```

   別のプロンプト：
   ```
   PR #1に「コードレビュー：良好ですが、ドキュメントを追加してください。」とコメントしてください。
   ```
   レスポンス例：
   ```
   PR #1にコメントを追加しました。
   ```

## 実装のコツと注意点

- **エラーハンドリング**：無効なPR番号やラベルが存在しない場合のエラーを適切に処理。
- **レートリミティング**：GitHub APIの制限（例：5000リクエスト/時間）に注意。
- **セキュリティ**：本番環境では、`auth: none`を避け、トークン認証を導入。
- **テスト**：テスト用リポジトリを作成し、本番データに影響を与えない。
- **拡張性**：大量のIssueやPRを処理する場合、キャッシュ（例：Redis）を検討。

## 試してみよう：挑戦課題
以下の機能を追加して、エージェントを強化してみてください：
- 特定のキーワード（例：「エラー」）を含むIssueに自動で「bug」ラベルを付ける機能。
- PRにレビュアーを割り当てるツール。
- Issueをクローズする機能。

## まとめと次のステップ

この第2章では、GitHubにIssueを作成し、PRにコメントを追加するMCPサーバーを構築し、開発タスク自動化エージェントを実現しました。AIがIssueやPRを操作できるようになり、チームの効率が向上しました。

次の第3章では、GitHubのコミットデータを活用して**コード分析エージェント**を構築します。たとえば、AIがコミット履歴から変更パターンを分析し、コード品質のインサイトを提供します。コード分析AIに興味がある方は、ぜひお楽しみに！

---

役に立ったと思ったら、「いいね」や「ストック」をしていただけると嬉しいです！次の章でまたお会いしましょう！