MCPとエージェントAI：自律型AIを構築する | 第2章：Google Calendarとの連携

## はじめに

第1章では、**エージェントAI**の概念と、**Model Context Protocol（MCP）**がどのようにAIを外部データやツールと接続し、自律的なタスク実行を可能にするかを学びました。簡単なメールサーバーを構築し、MCPの基本的な動作を体験しました。今回は、実際にエージェントAIを強化する第一歩として、**Google Calendar**とMCPを統合する方法を解説します。

この第2章では、Google Calendar APIを利用して予定を取得・作成するMCPサーバーを構築し、Claude Desktopと連携させます。これにより、「来週の空き時間を教えて」や「新しいミーティングを追加して」といったリクエストを処理できるエージェントAIの基盤を作ります。コード例とステップごとのガイドで、初心者でも実装できる内容を目指します。さっそく始めましょう！

## Google Calendar APIの準備

Google Calendarと連携するには、Google Cloud Platform（GCP）でAPIを有効化し、認証情報を設定する必要があります。以下の手順で準備します：

1. **Google Cloud Consoleでプロジェクトを作成**：
   - [Google Cloud Console](https://console.cloud.google.com)にアクセス。
   - 新しいプロジェクトを作成（例：`mcp-calendar-agent`）。
   - 「APIとサービス」→「ライブラリ」で「Google Calendar API」を検索し、有効化。

2. **OAuth 2.0認証情報の設定**：
   - 「APIとサービス」→「認証情報」→「認証情報を作成」→「OAuthクライアントID」。
   - アプリケーションの種類を「デスクトップアプリ」に設定。
   - クライアントIDとクライアントシークレットをダウンロード（`credentials.json`）。

3. **必要なライブラリのインストール**：
   PythonでGoogle Calendar APIを利用するため、以下のライブラリをインストール：
   ```bash
   pip install google-auth-oauthlib google-auth-httplib2 google-api-python-client
   ```

4. **トークンの生成**：
   初回実行時に、ブラウザでGoogleアカウントにログインし、アクセスを許可。トークンが`token.json`に保存されます。

## MCPサーバーの構築：Google Calendarとの統合

Google Calendarから予定を取得し、新しい予定を作成するMCPサーバーを構築します。以下のコードは、予定リストの取得と予定作成のリソースを提供します：

```python
from mcp import MCPServer
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from datetime import datetime, timedelta
import os

class CalendarServer(MCPServer):
    def __init__(self, host, port, credentials_file, token_file):
        super().__init__(host, port)
        self.credentials_file = credentials_file
        self.token_file = token_file
        self.service = self._init_calendar_service()
        self.register_resource("get_events", self.get_events)
        self.register_resource("create_event", self.create_event)

    def _init_calendar_service(self):
        SCOPES = ['https://www.googleapis.com/auth/calendar']
        creds = None
        if os.path.exists(self.token_file):
            creds = Credentials.from_authorized_user_file(self.token_file, SCOPES)
        if not creds or not creds.valid:
            flow = InstalledAppFlow.from_client_secrets_file(self.credentials_file, SCOPES)
            creds = flow.run_local_server(port=0)
            with open(self.token_file, 'w') as token:
                token.write(creds.to_json())
        return build('calendar', 'v3', credentials=creds)

    def get_events(self, params):
        try:
            time_min = datetime.utcnow().isoformat() + 'Z'
            time_max = (datetime.utcnow() + timedelta(days=7)).isoformat() + 'Z'
            events_result = self.service.events().list(
                calendarId='primary',
                timeMin=time_min,
                timeMax=time_max,
                maxResults=10,
                singleEvents=True,
                orderBy='startTime'
            ).execute()
            events = events_result.get('items', [])
            formatted_events = [
                {
                    'summary': event.get('summary', 'No title'),
                    'start': event['start'].get('dateTime', event['start'].get('date')),
                    'end': event['end'].get('dateTime', event['end'].get('date'))
                }
                for event in events
            ]
            return {"status": "success", "events": formatted_events}
        except Exception as e:
            return {"status": "error", "message": str(e)}

    def create_event(self, params):
        try:
            summary = params.get("summary", "New Meeting")
            start_time = params.get("start_time", (datetime.utcnow() + timedelta(hours=1)).isoformat())
            duration = params.get("duration", 60)  # 分単位
            event = {
                'summary': summary,
                'start': {'dateTime': start_time, 'timeZone': 'UTC'},
                'end': {'dateTime': (datetime.fromisoformat(start_time) + timedelta(minutes=duration)).isoformat(), 'timeZone': 'UTC'}
            }
            event = self.service.events().insert(calendarId='primary', body=event).execute()
            return {"status": "success", "event_id": event.get('id'), "link": event.get('htmlLink')}
        except Exception as e:
            return {"status": "error", "message": str(e)}

if __name__ == "__main__":
    server = CalendarServer(
        host="localhost",
        port=8088,
        credentials_file="credentials.json",
        token_file="token.json"
    )
    print("カレンダーMCPサーバーを起動中: http://localhost:8088")
    server.start()
```

### コードの説明
- **_init_calendar_service**：Google Calendar APIの認証を設定。初回はブラウザで認証し、トークンを保存。
- **get_events**：今後7日間の予定を取得し、タイトル、開始・終了時間を返す。
- **create_event**：新しい予定を作成。タイトル、開始時間、期間をパラメータとして受け取る。
- **register_resource**：リソースを登録し、AIがアクセス可能に。

### 前提条件
- `credentials.json`をプロジェクトフォルダに配置。
- 初回実行時に、ブラウザでGoogleアカウントにログインし、アクセスを許可。

## サーバーのテスト

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

1. **サーバー起動**：
   ```bash
   python calendar_server.py
   ```
   コンソールに「カレンダーMCPサーバーを起動中: http://localhost:8088」と表示。

2. **JSON-RPCリクエストの送信**：
   Pythonでテストリクエストを送信：
   ```python
   import requests
   import json

   url = "http://localhost:8088"
   payload = {
       "jsonrpc": "2.0",
       "method": "get_events",
       "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",
       "events": [
         {
           "summary": "チームミーティング",
           "start": "2025-04-16T10:00:00Z",
           "end": "2025-04-16T11:00:00Z"
         }
       ]
     },
     "id": 1
   }
   ```

3. **予定作成のテスト**：
   ```python
   payload = {
       "jsonrpc": "2.0",
       "method": "create_event",
       "params": {"summary": "テストミーティング", "duration": 30},
       "id": 2
   }
   response = requests.post(url, json=payload)
   print(json.dumps(response.json(), indent=2, ensure_ascii=False))
   ```

## Claude Desktopとの統合

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

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

2. **Claudeでテスト**：
   Claude Desktopを起動し、プロンプトを入力：
   ```
   来週の予定を教えてください。
   ```
   レスポンス例：
   ```
   来週の予定：
   - 2025-04-16 10:00: チームミーティング（1時間）
   ```

   予定作成のプロンプト：
   ```
   明日の14時に30分のミーティングを追加してください。
   ```
   レスポンス例：
   ```
   ミーティングを追加しました：
   - タイトル：テストミーティング
   - 時間：2025-04-16 14:00（30分）
   - リンク：https://calendar.google.com/...
   ```

## セキュリティの考慮

本番環境では、以下の対策を講じます：
- **OAuth 2.1の使用**：`auth`を`"none"`から適切なトークン認証に変更。
- **アクセス制限**：カレンダーの読み取り専用スコープ（`https://www.googleapis.com/auth/calendar.readonly`）を検討。
- **エラーハンドリング**：ユーザー向けにわかりやすいエラーメッセージを返す。
- **ログ記録**：リクエストとレスポンスを記録し、問題を追跡。

## 実装のコツ

- **キャッシュ**：頻繁にアクセスする予定データをキャッシュして、API呼び出しを削減。
- **タイムゾーン対応**：ユーザーのタイムゾーンを考慮し、適切に変換。
- **テストデータ**：本番前に、テスト用カレンダーを作成して動作確認。
- **コミュニティリソース**：GitHubで公開されているGoogle Calendar用MCPサーバーを参考に。

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

この第2章では、Google CalendarとMCPを統合し、スケジュール管理の基盤となるサーバーを構築しました。これにより、エージェントAIがユーザーの予定を把握し、新しい予定を追加する能力を獲得しました。

次の第3章では、NotionとSlackを統合し、プロジェクト管理を自動化するエージェントAIを構築します。たとえば、SlackのメッセージからNotionにタスクを自動追加する機能を実装します。実用的なエージェントAIに興味がある方は、ぜひお楽しみに！

---

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