JiraでAIを強化する | 第4章：リアルタイム管理AIを強化：Webhook連携

## はじめに

第3章では、**Jira**のTicketやSprintデータを活用してプロジェクト分析エージェントを構築しました。進捗やパフォーマンスを分析することで、AIがプロジェクト管理のインサイトを提供し、プロセス改善を支援できるようになりました。今回は、JiraのWebhook機能を利用して、Ticketやステータス変更のイベントをリアルタイムで検知する**リアルタイム管理AI**を構築します。

この第4章では、MCPサーバーにJiraのWebhookを統合し、Ticket作成やステータス変更などのイベントを即座に捕捉します。AIはこれを利用して、自動コメントを追加したり、イベントを他のツール（例：Slack）に通知したりできます。コード例とステップごとのガイドで、リアル thời gian管理AIの構築を体験しましょう。さあ、始めましょう！

## リアルタイム管理AIとは？

リアルタイム管理AIは、Jiraプロジェクトのイベント（Ticket作成、ステータス変更、コメント追加など）を監視し、プロジェクト管理を動的に支援するエージェントです。MCPサーバーとJiraのWebhookを組み合わせることで、以下のような機能を実現できます：
- **イベント検知**：新しいTicket、ステータス変更、コメントをリアルタイムで捕捉。
- **自動応答**：特定のイベント（例：Ticket作成）に基づいてコメントや優先度を追加。
- **外部連携**：イベントをSlackやメールに通知し、コラボレーションを強化。

### ユースケース
- プロジェクト管理：AIが「バグ」Ticket作成時に自動で「High」優先度を付ける。
- チーム通知：重要なイベント（例：Ticketの期限超過）にSlackで通知。
- プロセス監視：意図しないイベント（例：未レビューでステータス変更）を検出。

## 開発環境の準備

第3章の環境を基に、以下の追加準備を行います：
- **Python 3.8以降**、**mcpライブラリ**、**requestsライブラリ**、**Claude Desktop**：これまでと同じ。
- **python-dotenv**：環境変数の管理（既にインストール済み）。
- **ngrok**：ローカルサーバーを公開し、Webhookを受信。
- **Jiraプロジェクト**：Webhook対応の設定。

インストールコマンド（必要に応じて）：
```bash
pip install requests python-dotenv
```

### Jiraのセットアップ
1. **Jira APIトークンの確認**：
   - 第3章のトークンを使用。
   - 権限を確認：プロジェクトでのTicket操作（読み書き）、Webhook管理。
2. **Webhookの設定**：
   - Jiraインスタンス（例：`https://your-domain.atlassian.net`）の「System」→「Webhooks」に移動（管理者権限が必要）。
   - 新しいWebhookを追加：
     - **Name**：例：`MCP-Webhook`。
     - **URL**：ngrokで生成するURL（例：`https://abc123.ngrok.io/webhook`）。
     - **Events**：`Issue created`、`Issue updated`、`Comment created`を選択。
     - **JQL filter**：`project = MCP`（プロジェクトMCPに限定）。
     - **Status**：Enabled。
   - シークレット（オプション）：Webhook署名検証用の文字列を設定（例：`mysecret`）。
3. **プロジェクト準備**：
   - 第3章のプロジェクト（例：`MCP-TEST`、キー：`MCP`）を使用。
   - Ticketやコメントを作成し、イベントを生成（例：Ticketを開く、ステータスを変更）。
4. **ngrokの設定**：
   - ngrokをインストール（`brew install ngrok`または公式サイト）。
   - ローカルサーバーを公開：
     ```bash
     ngrok http 8135
     ```
   - 生成されたURLを記録（例：`https://abc123.ngrok.io`）。
   - JiraのWebhook設定にURLを設定（例：`https://abc123.ngrok.io/webhook`）。
5. **環境変数**：
   第3章の`.env`ファイルに以下を追加：
   ```bash
   JIRA_URL=https://your-domain.atlassian.net
   JIRA_EMAIL=your_email@example.com
   JIRA_API_TOKEN=your_jira_api_token
   JIRA_PROJECT_KEY=MCP
   JIRA_WEBHOOK_SECRET=mysecret
   ```

## コード例：リアルタイム管理用MCPサーバー

以下のMCPサーバーは、JiraのWebhookを介してイベントを検知し、自動コメントやイベント記録の機能を提供します。

```python
from mcp import MCPServer
import os
from dotenv import load_dotenv
import requests
from requests.auth import HTTPBasicAuth
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import hmac
import hashlib
import threading

class JiraRealtimeServer(MCPServer):
    def __init__(self, host, port, url, email, api_token, project_key, webhook_secret):
        super().__init__(host, port)
        self.url = url
        self.email = email
        self.api_token = api_token
        self.project_key = project_key
        self.webhook_secret = webhook_secret
        self.base_url = f"{url}/rest/api/3"
        self.auth = HTTPBasicAuth(email, api_token)
        self.headers = {
            "Accept": "application/json",
            "Content-Type": "application/json"
        }
        self.latest_event = None
        self.register_resource("get_latest_event", self.get_latest_event)
        self.register_tool("add_comment", self.add_comment)
        self.start_webhook_server()

    def verify_webhook_signature(self, body, signature):
        try:
            computed_sig = "sha256=" + hmac.new(
                self.webhook_secret.encode("utf-8"),
                body.encode("utf-8"),
                hashlib.sha256
            ).hexdigest()
            return hmac.compare_digest(computed_sig, signature)
        except Exception as e:
            return False

    def add_comment(self, params):
        try:
            ticket_key = params.get("ticket_key", "")
            comment = params.get("comment", "")
            if not ticket_key or not comment:
                return {"status": "error", "message": "Ticketキーとコメントが必要です"}
            
            url = f"{self.base_url}/issue/{ticket_key}/comment"
            payload = {"body": comment}
            response = requests.post(url, headers=self.headers, auth=self.auth, 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)}

    def get_latest_event(self, params):
        try:
            if self.latest_event:
                event_type = self.latest_event.get("event", "")
                event = self.latest_event.get("payload", {})
                event_info = {
                    "event_type": event_type,
                    "action": event.get("webhookEvent", ""),
                    "ticket_key": event.get("issue", {}).get("key", ""),
                    "user": event.get("user", {}).get("displayName", "unknown"),
                    "created_at": event.get("issue", {}).get("fields", {}).get("created", "")
                }
                return {"status": "success", "event_info": event_info}
            return {"status": "success", "event_info": None, "message": "イベントなし"}
        except Exception as e:
            return {"status": "error", "message": str(e)}

    def start_webhook_server(self):
        class WebhookHandler(BaseHTTPRequestHandler):
            def do_POST(self):
                content_length = int(self.headers["Content-Length"])
                post_data = self.rfile.read(content_length)
                signature = self.headers.get("X-Hub-Signature-256", "")

                # Webhook署名検証
                if not self.server.parent.verify_webhook_signature(post_data.decode("utf-8"), signature):
                    self.send_response(401)
                    self.end_headers()
                    self.wfile.write(b"Invalid signature")
                    return

                data = json.loads(post_data.decode("utf-8"))
                event_type = data.get("webhookEvent", "").split(":")[0]

                # イベント処理
                if event_type in ["jira:issue_created", "jira:issue_updated", "comment_created"]:
                    self.server.parent.latest_event = {"event": event_type, "payload": data}

                self.send_response(200)
                self.end_headers()
                self.wfile.write(b"Webhook received")

        server = HTTPServer(("localhost", 8135), WebhookHandler)
        server.parent = self
        threading.Thread(target=server.serve_forever, daemon=True).start()
        print("Webhookサーバーを起動中: http://localhost:8135")

if __name__ == "__main__":
    load_dotenv()
    server = JiraRealtimeServer(
        host="localhost",
        port=8135,
        url=os.getenv("JIRA_URL"),
        email=os.getenv("JIRA_EMAIL"),
        api_token=os.getenv("JIRA_API_TOKEN"),
        project_key=os.getenv("JIRA_PROJECT_KEY"),
        webhook_secret=os.getenv("JIRA_WEBHOOK_SECRET")
    )
    print("JiraリアルタイムMCPサーバーを起動中: http://localhost:8135")
    server.start()
```

### コードの説明
- **verify_webhook_signature**：JiraのWebhook署名（`X-Hub-Signature-256`）をHMAC-SHA256で検証。
- **add_comment**：指定したTicketにコメントを追加。Ticketキーとコメント内容を指定。
- **get_latest_event**：Webhookで受信した最新イベント（Ticket作成、更新、コメント）を取得。
- **start_webhook_server**：ローカルWebhookサーバーを起動し、Jiraからのイベントを処理。
- **WebhookHandler**：Jiraのイベント（Issue created、updated、comment created）を受信し、署名検証を行う。

### 前提条件
- JiraプロジェクトにWebhookが設定済み（`jira:issue_created`、`jira:issue_updated`、`comment_created`イベント）。
- ngrokでWebhook URLが公開され、Jiraに登録済み。
- `.env`ファイルに正しい`JIRA_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`、`JIRA_PROJECT_KEY`、`JIRA_WEBHOOK_SECRET`が設定済み。
- APIトークンにプロジェクトへの読み書き権限がある。

## サーバーのテスト

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

1. **ngrok起動**：
   ```bash
   ngrok http 8135
   ```
   ngrok URL（例：`https://abc123.ngrok.io`）を記録し、JiraのWebhook設定に設定（例：`https://abc123.ngrok.io/webhook`）。

2. **サーバー起動**：
   ```bash
   python jira_realtime_server.py
   ```
   コンソールに「JiraリアルタイムMCPサーバーを起動中: http://localhost:8135」と「Webhookサーバーを起動中: http://localhost:8135」が表示。

3. **イベント検知のテスト**：
   - JiraのUIでプロジェクトにTicketを作成（例：「バグ報告」）またはステータスを変更。
   - サーバーがイベントをWebhook経由で検知。

4. **最新イベント取得のテスト**：
   Pythonでリクエストを送信：
   ```python
   import requests
   import json

   url = "http://localhost:8135"
   payload = {
       "jsonrpc": "2.0",
       "method": "get_latest_event",
       "params": {},
       "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",
       "event_info": {
         "event_type": "jira:issue_created",
         "action": "jira:issue_created",
         "ticket_key": "MCP-5",
         "user": "Your Name",
         "created_at": "2025-04-22T14:00:00.000+0000"
       }
     },
     "id": 1
   }
   ```

5. **自動コメントのテスト**：
   ```python
   payload = {
       "jsonrpc": "2.0",
       "method": "add_comment",
       "params": {
           "ticket_key": "MCP-5",
           "comment": "Ticketを確認しました。詳細を教えてください。"
       },
       "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": "10002"
     },
     "id": 2
   }
   ```

## Claude Desktopとの接続

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

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

2. **Claudeでテスト**：
   Claude Desktopを起動し、プロンプトを入力：
   ```
   最新のプロジェクトイベントを教えてください。
   ```
   レスポンス例：
   ```
   最新のプロジェクトイベント：
   - イベント：Ticket作成
   - アクション：jira:issue_created
   - Ticketキー：MCP-5
   - ユーザー：Your Name
   - 作成日時：2025-04-22 14:00
   ```

   別のプロンプト：
   ```
   Ticket MCP-5に「Ticketを確認しました。詳細を教えてください。」とコメントしてください。
   ```
   レスポンス例：
   ```
   Ticket MCP-5にコメントを追加しました。
   ```

## 実装のコツと注意点

- **Webhook検証**：Jiraの署名検証（`X-Hub-Signature-256`）を正しく処理。
- **レートリミティング**：Jira APIの制限（例：600リクエスト/分、クラウドインスタンスによる）に注意。
- **セキュリティ**：本番環境では、HTTPSを有効化し、Webhookシークレットを必須化。
- **テスト**：テスト用プロジェクトを作成し、本番データに影響を与えない。
- **拡張性**：大量のイベントを処理する場合、キュー（例：Redis、RabbitMQ）を検討。

## 試してみよう：挑戦課題
以下の機能を追加して、エージェントを強化してみてください：
- 「バグ」キーワードを含むTicket作成時に「High」優先度を自動付与。
- ステータス変更イベントを検知し、Slackに通知する機能。
- イベントデータをJiraに新しいTicketとして記録するツール。

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

この第4章では、JiraのWebhookを活用してリアルタイム管理AIを構築しました。Ticketやステータス変更のイベントをリアルタイムで検知し、AIが自動コメントや通知を行えるようになりました。

次の第5章では、MCPサーバーの**最適化とコミュニティへの貢献**に焦点を当てます。サーバーのパフォーマンス向上、セキュリティ強化、そしてJira用MCPサーバーをオープンソースとして共有する方法を学びます。コミュニティAIの未来に興味がある方は、ぜひお楽しみに！

---

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