GitHubのIssueでタイトルだけ書いて本文を空にしてしまうのを、MCPを使ってなんとできないか考えてみた

こんにちは。
クリエーションラインのKodakです。

GitHubのIssueを書く時に、よくタイトルだけ書いて本文を空にして書いてしまうことってありませんか？
または、本文の内容に具体的なことが書かれていなくて、内容を読んでもイマイチよくわからなかったり、どのようなアクションをとれば良いか判断できなかったりしませんか？

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241956/fa69c4d3-ee26-4995-9185-11a58363e215.png" width="70%" alt="create_issue">

私は「ダメだなぁ〜」ってわかっていても、とりあえずIssueだけ作って、本文は時間があるときに書こうと思ったり、本文の内容が箇条書きになっていて、イマイチ何を書くんだったか思い出すのに時間が掛かったりすることが良くあります。😂

## AIの力が使えないかを考えてみる

こういった時に、AIの力が使えないか。。。と思うのですが、0->1（新しいことを生み出す時などで）でAIを活用していくことは難しく、やはり人が考えなければならないと個人的には思います。

なので、ここでのAIの活用場所としては、本文の内容が曖昧なIssueを教えてくれたり、人が本文を書く時のサポートを担ってくれると良いかな。。。と思いました。

## 流行りのMCPの力を確認してみる

そこでMCPの活用を考えてみます。
CursorにGitHubのMCPを利用できるようにして、以下のようなプロンプトを投げてみました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241956/fa8432a1-ec0d-43d9-811d-fff62cb0aa82.png" width="70%" alt="mcp1">

みごとに、中身があいまいなIssueを見つけて一覧にしてくれました。
さらに続けて、以下のようなプロンプトを投げてみました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241956/21fdc737-690f-4b8d-8778-a4bb43c9ce61.png" width="70%" alt="mcp2">

お、これはもしかすると、コメントしてくれた？？？

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241956/bb13031e-c3ed-4892-971c-4f1a04811fbc.png" width="70%" alt="mcp2">

すばらしい。。。いい感じにIssueのコメントを投稿してくれましたね。

MCPを使えば、本文の内容が曖昧なIssueを教えてくれたり、人がほんぶｎを書く時に、どのようなことに気をつけて書けば良いかをAIからアドバイスのコメントを貰える。。。というのはいいですね。

## MCPはすばらしい。しかし、実務で使うのは難しい

しかし、これを実務で使うのは正直難しいかな。。。と思いました。
というのも、AIに色々と任せすぎてしまっていて、変なプロンプトをAIに投げたりするとIssueの情報が消えてしまったり、勝手に改変されて書いていた内容がわからなくなったりするかもしれません💦

ならば、Issueを作った時に、それを読み取って勝手にコメントを残すのはどうだろうか🤔

## 実務で使えるようにしてみる

使うのは、GitHub ActionsとAmazon Bedrockです。
アイディアが思いついたので、後はすべてAIに書いてもらいます。

```yml
# .github/workflows/issue_comment.yml
name: Issue Auto Comment

on:
  issues:
    types: [opened]

jobs:
  comment_with_bedrock:
    runs-on: ubuntu-latest
    permissions:
      issues: write
      contents: read
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'

      - name: Install dependencies
        run: pip install boto3 requests

      - name: Run AI comment script
        run: python scripts/action_script/ai_issue_comment.py
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          AWS_REGION: ${{ secrets.AWS_REGION }}
          BEDROCK_MODEL_ID: ${{ secrets.BEDROCK_MODEL_ID }}

```

```python
# scripts/action_script/ai_issue_comment.py
import os
import json
import sys
import time
import boto3
import requests
from botocore.exceptions import ClientError

def check_required_env_vars():
    required_vars = ['GITHUB_EVENT_PATH', 'AWS_REGION', 'BEDROCK_MODEL_ID', 'GITHUB_TOKEN']
    missing_vars = [var for var in required_vars if not os.environ.get(var)]
    
    if missing_vars:
        print(f"エラー: 次の環境変数が設定されていません: {', '.join(missing_vars)}")
        sys.exit(1)

def get_issue_details():
    try:
        event_path = os.environ.get('GITHUB_EVENT_PATH')
        with open(event_path, 'r') as f:
            event_data = json.load(f)
        issue = event_data['issue']
        return issue['number'], issue['body'], issue['title']
    except (FileNotFoundError, KeyError, json.JSONDecodeError) as e:
        print(f"イベントデータの読み込みエラー: {str(e)}")
        sys.exit(1)

def generate_ai_comment(issue_body, issue_title, max_retries=3):
    client = boto3.client('bedrock-runtime', "us-east-1")
    prompt = f"\n\nHuman:GitHub Issueの内容です。以下パターン毎に、アドバイスを出してください。\n1. Bodyが空の場合、Titleから想像できるBodyに書くべき内容を提案してください。\n2. Bodyに内容が書かれている場合、Bodyの内容について、疑問点や課題などがあれば、アドバイスしてください。\n3. TitleとBodyの内容が十分である場合、特にアドバイスすることがない場合、「よく頑張りました！」と褒めてください。\n\nTitle: {issue_title}\n\nBody: {issue_body}\n\n"
    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 512,
        "temperature": 0.5,
        "messages": [
            {
                "role": "user",
                "content": [{"type": "text", "text": prompt}],
            }
        ]
    })
    
    for attempt in range(max_retries):
        try:
            response = client.invoke_model(
                body=body,
                modelId=os.environ['BEDROCK_MODEL_ID'],
                accept='application/json',
                contentType='application/json'
            )
            response_body = json.loads(response['body'].read())
            completion = response_body['content'][0]['text']
            
            if not completion:
                print("警告: AIからの応答が空です")
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)  # 指数バックオフ
                    continue
                return "申し訳ありませんが、このIssueに対するコメントを生成できませんでした。"
                
            return completion
            
        except ClientError as e:
            print(f"AWS Bedrock呼び出しエラー ({attempt+1}/{max_retries}): {str(e)}")
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # 指数バックオフ
            else:
                return "申し訳ありませんが、AIサービスとの通信中にエラーが発生しました。"

def post_comment(issue_number, comment_body, max_retries=3):
    repo = os.environ['GITHUB_REPOSITORY']
    token = os.environ['GITHUB_TOKEN']
    url = f"https://api.github.com/repos/{repo}/issues/{issue_number}/comments"
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github.v3+json"
    }
    data = {"body": comment_body}
    
    for attempt in range(max_retries):
        try:
            response = requests.post(url, headers=headers, json=data, timeout=10)
            if response.status_code == 201:
                print(f"コメントを正常に投稿しました: Issue #{issue_number}")
                return
            elif response.status_code == 403:
                # 権限エラーはリトライしても解決しない
                print(f"権限エラー: GitHub APIへのアクセス権がありません: {response.text}")
            elif response.status_code == 429:
                # レート制限に達した場合
                if attempt < max_retries - 1:
                    retry_after = int(response.headers.get('Retry-After', 60))
                    print(f"レート制限に達しました。{retry_after}秒後に再試行します。")
                    time.sleep(retry_after)
                    continue
            else:
                print(f"GitHub API呼び出しエラー ({attempt+1}/{max_retries}): ステータスコード {response.status_code}, {response.text}")
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)  # 指数バックオフ
                    continue

            raise Exception(f"コメント投稿に失敗しました: {response.status_code}, {response.text}")
        except requests.RequestException as e:
            print(f"HTTPリクエストエラー ({attempt+1}/{max_retries}): {str(e)}")
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # 指数バックオフ
            else:
                raise Exception(f"コメント投稿に失敗しました: {str(e)}")

def main():
    try:
        check_required_env_vars()
        issue_number, issue_body, issue_title = get_issue_details()
        ai_comment = generate_ai_comment(issue_body, issue_title)
        post_comment(issue_number, ai_comment)
        return 0
    except Exception as e:
        print(f"予期しないエラーが発生しました: {str(e)}")
        return 1

if __name__ == "__main__":
    sys.exit(main())

```

お願いすると、ほとんど書いてくれました。（私が書いたところは殆どないです😭）
あとは、Issueを作ると、GitHub Actionsが動いて、勝手にコメントをくれました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241956/ebba1b9f-8d43-4017-a825-ba63ee4a9fc9.png" width="100%" alt="ai_issue_comment">

良い感じですね。
タイトルから想像して、それっぽいアドバイスをいただけます。このアドバイスを拝借して後から概要を書く。。。なんてこともできそうですね。🎉🎉🎉

## まとめ
- GitHubのIssueでタイトルだけ書いて本文を空にしてしまうのを、MCPを使ってなんとできないか考えてみた
- しかし、MCPは自由すぎて実務で使うには「読み取り専用」くらいにまでしないと使えない
- もちろん、個人やチーム内ルール等でちゃんと定めて使う分には問題はない
- または、GitHub Actions等で仕組み化するのも考えてみるのも良いかも？？