Hub について
Hubは、複数のGoogleアカウントを1つの接続先にまとめ、Gmail、Calendar、Drive、Contacts、Tasksを利用できるリモートMCPサーバーです。AOLメールにも対応し、リンク済みアカウントを横断して検索できます。
主な機能
- 01複数のGoogleアカウントへのアクセス
- 02GmailとAOLの横断検索
- 03メールと下書きの読み取り・管理
- 04カレンダー管理と横断スケジュール調整
- 05Driveファイルの検索・管理
- 06連絡先、タスク、エージェントスレッドへのアクセス
活用シーン
- 01個人用と仕事用アカウントのメール検索
- 02複数カレンダーを横断した空き時間の確認
- 03添付ファイルやDrive文書の読み取り
- 04AIエージェント間のメッセージ調整
MCP 対応
Hubは、GoogleとAOLのメール、カレンダー、Drive、連絡先、タスク、エージェント間通信のツールをリモートMCPサーバーとして公開します。Streamable HTTPとBearer認証に対応しています。
設定
リモートサーバーはインストール不要です。クライアントを選んで設定をコピーするか、ワンクリックで導入できます。
https://hub.grounders.app/mcpclaude mcp add --transport http itsbreeze-hub https://hub.grounders.app/mcp{"mcpServers":{"itsbreeze-hub":{"type":"http","url":"https://hub.grounders.app/mcp"}}}Cursor で開く{"servers":{"itsbreeze-hub":{"type":"http","url":"https://hub.grounders.app/mcp"}}}VS Code で開く{"mcpServers":{"itsbreeze-hub":{"type":"http","url":"https://hub.grounders.app/mcp"}}}{"mcpServers":{"itsbreeze-hub":{"serverUrl":"https://hub.grounders.app/mcp"}}}コマンドはターミナルで実行し、JSON は各クライアントの MCP 設定に貼り付けて再起動します。認可が必要なサーバーは初回接続時にログインを求めます。
ツール
詳細
Hub — Claude 向けマルチアカウント Google コネクター
Claude が複数の Google アカウントに同時にアクセスできるリモート MCP サーバーです。Gmail、Calendar、Drive、Contacts、Tasks に対応しています。Claude に組み込まれた Google コネクターは、それぞれ1つのアカウントしか保持できず、2つ目を接続すると1つ目が置き換えられます。このサーバーでは、リンクしたアカウントをいくつでも保持でき、すべての検索ツールが1回の呼び出しで全アカウントを検索します。
AOL のメールボックスもリンクできます。 アプリパスワードを使って IMAP と SMTP 経由で接続し、同じメールツールから利用でき、Gmail とまとめて検索できます。詳しくはAOL のメールボックスをご覧ください。
自分で実行すると、Google トークンは暗号化して自分のデータベースに保存され、Google とは直接通信します。第三者を経由することはありません。まずはホスト版を試すこともできます。
Node と Postgres を使用し、テーブルは6つです。依存関係は Express、pg、jsonwebtoken に加え、AOL 用の imapflow、mailparser、nodemailer です。IMAP、MIME、SMTP は解析作業ではなくプロトコルであり、それぞれのライブラリは各分野で保守されている標準的なものです。PDF、Word、Excel、PowerPoint、OpenDocument の文書テキスト抽出には、Node の標準ライブラリのみを使用します。
ステータス: 実際のメールボックスを使って本番環境で稼働中で、自動チェックを564件実施しています。1人のニーズに合わせて構築し、1アカウント制限に困っているのが自分だけではないため公開しました。ユーザー数の少ない実用ソフトウェアとして扱い、堅牢化された製品とは見なさないでください。
ホスト版を試す
hub.grounders.app で稼働しています。現在テスト中で、テスト期間中は無料です。
- Claude で、
https://hub.grounders.app/mcpを指定して Settings → Connectors → Add custom connector を選択します。ChatGPT、Cursor、その他の MCP クライアントでも、同じ URL をリモート MCP サーバーとして指定できます。 - 求められたら、Google に一度サインインします。
- hub.grounders.app/gmail/connect で各アカウントをリンクします。
Google によるアプリの審査はまだ進行中のため、初回は「未確認のアプリ」という画面が表示されます。
クイックスタート
git clone https://github.com/ItsBreeze/google-multi-account-mcp.git
cd google-multi-account-mcp
npm install
cp .env.example .env # fill it in — see Setup below
npm run migrate # creates the six tables
npm start
その後、パブリック HTTPS URL を利用できる場所(Railway、Fly、Render、VPS など、Node を実行でき、Postgres に接続できる環境)にデプロイして、セットアップに従ってください。
エンドポイント
| メソッド | パス | 説明 |
|---|---|---|
| POST | /mcp | MCP Streamable HTTP エンドポイント(Bearer 認証) |
| GET | /mcp/oauth/authorize | 同意画面 — 運用者のパスワード |
| POST | /mcp/oauth/token | トークンと更新用グラント |
| POST | /mcp/oauth/register | RFC 7591 動的クライアント登録 |
| GET | /.well-known/oauth-protected-resource | RFC 9728 ディスカバリー |
| GET | /.well-known/oauth-authorization-server | RFC 8414 ディスカバリー |
| GET | /gmail/connect | アカウントをリンク(アカウントごとに繰り返す) |
| GET/POST | /gmail/aol | アプリパスワードで AOL のメールボックスをリンク(保存前に AOL で検証) |
| POST | /gmail/unlink | 1つのアカウントのリンクを解除し、Google のグラントを取り消す(または保存済みの AOL アプリパスワードを削除) |
| GET/POST | /ai/k/<link> | MCP を使わない AI エージェント用の返信リンク。GET でスレッドを読み込み、POST {"text"} で返信 |
ツール(59)
すべてのツールで account は省略可能です。検索時にこれを省略すると、すべてのリンク済みアカウントを検索して結果を統合します。これは、単一アカウントのコネクターではできないことです。書き込み時には操作対象のアカウントを指定するため、「この予定を作成して」という指示で、どのアカウントのカレンダーに登録されるのかが曖昧になることはありません。
メール — Gmail と AOL(23)
| 領域 | ツール | 備考 |
|---|---|---|
| アカウント | list_accounts | リンクされているアカウントとトークンの状態を確認できます |
| 検索 | search_messages、search_threads | Gmail のクエリ構文に対応し、アカウントをまたいで結果を統合して日付順に並べます。search_threads は会話ごとに 1 行を返します。件名、すべての参加者、メッセージ数と未読数、最終アクティビティを確認できるため、本文を取得せずに「X とのスレッドはどうなっているか」を把握できます |
| 読み取り | get_message、get_thread、get_attachment | 本文はテキストに変換され、上限は 60 KB です。添付ファイルのメタデータも含まれます。get_attachment は PDF、Word、Excel、PowerPoint、OpenDocument の添付ファイルからテキストを抽出するため、メールで届いた契約書や請求書を直接読み取れます。それ以外は base64 で返されます(上限 2 MB) |
| 送信 | send_message、reply_to_message、forward_message | 返信は In-Reply-To/References を使ってスレッドに紐付けられます。転送には添付ファイルが含まれます(上限 10 MB。省略されたファイル名は通知されます)。オプションの from と fromName を指定すると、確認済みの Gmail の送信元エイリアスとして送信できます |
| 下書き | create_draft、list_drafts、get_draft、update_draft、send_draft、delete_draft | reply_to_message_id を指定した create_draft は、レビュー用にスレッド内の返信を下書きとして作成します。AI が作成したメールを安全に扱う方法です。create_draft と send_draft では from/fromName も指定できます(send_draft は送信前に下書きの From を書き換えます。Gmail のみ) |
| ラベル | modify_labels、list_labels、create_label、update_label、delete_label | modify_labels には message_id または thread_id を指定します。INBOX を削除するとアーカイブされます |
| ゴミ箱と迷惑メール | trash_message、untrash_message、mark_spam | いずれも message_id または thread_id を指定します。ゴミ箱内のメールは 30 日間復元できます。unmark: true を指定して mark_spam を実行すると、迷惑メールを元に戻します |
カレンダー(9)
| 領域 | ツール | 備考 |
|---|---|---|
| 読み取り | list_calendars、list_events、search_events、get_event | list_events はリンクされているすべてのカレンダーを 1 つの予定表に統合し、デフォルトで今後 7 日間を表示します。繰り返し予定は実際の開催日時に展開されるため、毎週のスタンドアップミーティングは開催日にそれぞれ表示されます |
| 書き込み | create_event、update_event、delete_event | 時刻は ISO 8601 形式です。YYYY-MM-DD のみを指定すると終日予定になります。update_event はパッチとして動作し、指定されていないフィールドの値は保持されます。send_updates で指定しない限り、参加者にメールは送信されません |
| 繰り返し | create_event、update_event、delete_event | repeat: "weekly"(repeat_count または repeat_until と併用)で一般的なケースに対応できます。recurrence では完全な RFC 5545 ルールを指定できます。繰り返し予定では、scope で単一の開催回かシリーズ全体かを選択します。詳細は以下を参照してください |
| 出欠回答 | respond_to_event | 招待を受けたアカウントとして、承諾/辞退/仮承諾を回答します。デフォルトでは主催者に通知されます |
| 日程調整 | suggest_time | リンクされているすべてのカレンダーを対象に、空き時間をまとめて検索します。どこか 1 つのカレンダーでも予定があれば、その時間は空き時間として扱われません。3 時間の空きを 30 分枠 6 つに分割せず、まとまった空き時間を返します |
Drive(15)
| 領域 | ツール | 備考 |
|---|---|---|
| 検索 | search_files, list_recent_files, get_file_metadata, list_shared_drives | 名前と内容を対象にテキスト検索します。filter では、任意で Drive の生のクエリ構文を使用できます。マイドライブとすべての共有ドライブを一度に対象にします。drive_id を指定すると、対象を1つに絞れます。ゴミ箱内のファイルは、指定しない限り除外されます |
| 読み取り | read_file_content, download_file_content | Docs、Sheets、Slides はエクスポートされます(Sheets は CSV)。PDF、Word、Excel、PowerPoint、OpenDocument はテキストに抽出されます。ocr: true を指定すると、スキャンデータや画像が Google 独自の変換処理に渡されます。include_comments を指定すると、コメントのスレッドが返されます。download_file_content では export_as を指定できます。Doc を PDF または docx に、Sheet を xlsx に変換できます。テキストは最大 60 KB、バイナリは base64 で最大 2 MB です |
| 書き込み | create_file, update_file, copy_file | テキストには content、バイナリには content_base64 を使用します。convert_to を指定すると、アップロードしたファイルを編集可能な Doc、Sheet、Slides、またはフォルダーに Drive が変換します。update_file では名前の変更、移動、説明の変更ができます。内容の上書きには、さらに replace_content: true が必要です。所有していないファイルでは、非公開のコピーが編集されます。詳しくは以下を参照してください |
| コメント | comment_on_file | 下書きにコメントを残すか、スレッドに返信します。ドキュメントに変更を加えずにレビューできます |
| 共有 | get_file_permissions, share_file, unshare_file | 指定した1人と、閲覧者、コメント投稿者、または編集者として共有します。ドメイン全体での共有と公開リンクでの共有は DRIVE_ALLOW_PUBLIC_SHARING=true を設定しない限り無効です。また、設定した場合も confirm_public が必要です。unshare_file はアクセス権を取り消します。ユーザー単位またはドメイン単位で取り消すか、公開リンクを削除できます |
| 削除 | trash_file, untrash_file | 30日間の期限内で、ゴミ箱への移動と復元ができます。詳しくは以下の適用範囲に関する注記を参照してください |
Contacts(2)および Tasks(5)
| 領域 | ツール | 備考 |
|---|---|---|
| Contacts | search_contacts, list_contacts | 読み取り専用です。保存済みの連絡先とアカウントがやり取りした相手を検索するため、「Ann にメールして」と指示すると、推測ではなく実際のメールアドレスが特定されます |
| Tasks | list_task_lists, list_tasks, create_task, update_task, delete_task | list_id はデフォルトでアカウントの最初のリストになります。completed: true を指定して update_task を呼び出すと、タスクを完了としてマークします。false を指定すると未完了に戻します。Google Tasks にはゴミ箱がないため、delete_task での削除は取り消せません |
AI エージェント(5)
AI 同士はメールではなく Hub を介してやり取りします。すべてのメッセージにはスレッド、転送回数、既読時刻が記録されるため、件名の照合によって情報が失われることはなく、送信者は既読かどうかを確認できます。
| 領域 | ツール | 備考 |
|---|---|---|
| 会話 | ai_send, ai_inbox, ai_thread | thread を指定して ai_send を呼び出すと、そのスレッドに返信します。to はデフォルトで相手側に設定されます。wait_seconds(最大50)を指定して ai_inbox を呼び出すと、返信が届くまで待機します。スレッドの転送回数は10回までです |
| エージェント | ai_agents, ai_set_agent | エージェントはメール(リンク済みメールボックスからの1つの JSON オブジェクト。件名テンプレートは Claude -> Lute (hop {hop}) など)またはWebhook({"text"} の POST。ヘッダーは暗号化して保存)で起動できます。起動に失敗すると、メッセージは未読のままになり、送信者に通知されます |
MCP を利用しないエージェントは、アカウントがなくても使用できます。起動時に署名付きの reply_url(有効期限30日、所有者1人、エージェント1つ)が渡されます。そこに {"text":"..."} を POST して返信し、GET してスレッドを読み取るか、{"new_thread":true,"to":"claude","text":"..."} を POST して新しい会話を開始できます。
AOL のメールボックス
アドレスと、AOL Account Security で生成したアプリパスワードを使って、/gmail/aol で接続します。AOL は承認済みパートナーの一覧に含まれないサードパーティ製メールクライアント向けの OAuth に対応していないため、これが唯一の接続方法です。保存前に IMAP へのログインに使ってパスワードを検証するため(1 つのアプリパスワードで IMAP と SMTP の両方を使用できます)、入力ミスは Claude が最初のツールを呼び出すときではなく、接続ページで報告されます。Google のトークンと同様に、TOKEN_ENC_KEY を使って AES-256-GCM で暗号化して保存します。ホストはコード内のプリセット(imap_providers.js)から取得し、リクエストから取得することはありません。
SMTP がブロックされている環境では、送信にリレーを使います。 Railway は Pro プラン未満では外向き SMTP をブロックするため、hub.grounders.app は AOL への送信を HTTPS 経由で Cloudflare Worker(smtp-relay/)に渡し、その Worker が smtp.aol.com:465 に SMTP で接続します。Worker は何も保存・記録せず、プリセットされたホストにのみ接続します。SMTP_RELAY_URL と SMTP_RELAY_TOKEN が設定されている場合に使用します。設定されていない場合は、ポート 465 に接続できるセルフホスト版と同様に、サーバーが SMTP に直接接続します。Gmail の送信には影響せず、引き続き Google の API を使います。
23 個のメールツールは AOL のメールボックスでも変更なく動作します。account を指定しない検索では、Gmail と AOL の結果を 1 つにまとめ、日付順に並べて返します。異なるのは IMAP 自体の仕様です。ツールはそれを隠さず、そのまま伝えます。
| Gmail | IMAP 経由の AOL |
|---|---|
| ラベルを複数、各メッセージに付与 | フォルダーを各メッセージに 1 つ付与。UNREAD/STARRED はフラグです。INBOX、SPAM、TRASH およびフォルダー名を使ってメッセージを移動します。2 つのフォルダーが必要になる変更は拒否されます。IMPORTANT とカテゴリはありません |
| ID は恒久的 | ID はフォルダー + UID です。そのため、移動するとメッセージの ID が変わります。移動を行う各呼び出しは new_message_id として新しい ID を返します。サーバーが番号を振り直したフォルダーの ID は古いものとして拒否され、別のメッセージの ID として解釈されることはありません |
| Gmail のクエリ構文 | IMAP SEARCH に変換されます:from: to: cc: bcc: subject: is:unread/read/starred has:attachment newer_than: older_than: after: before: in: label: larger: smaller:、フレーズ、OR、-、括弧。これ以外は名前を示して拒否されます。category:promotions を無視すると、検索結果が対象の一部ではなくすべてのメッセージになってしまうためです |
| ID によるスレッド | サーバーに OBJECTID がある場合は、サーバーの THREADID によるスレッド(AOL は対応しています)。各フォルダーの最近のメールから読み取り、祖先メッセージは Message-ID で検索します。フォルダーを横断して集めるため、Sent にある返信も含まれます。THREADID に対応しないサーバーでは、References/In-Reply-To の連鎖にフォールバックします。AOL では References を検索できないため、検索を 1 回で済ませられません |
| 送信済みコピーは自動的に保存 | SMTP 送信後、Sent でメッセージの Message-ID を確認し、AOL が自動で保存していなかった場合にのみコピーを追加します。Bcc は SMTP エンベロープにのみ含め、送信されるヘッダーには含めません |
update_draft は下書きをその場で編集 | IMAP では編集できません。新しいバージョンを保存してから古いものを削除するため、下書きの ID は変わります |
| ラベルを削除してもメールは残る | フォルダーを削除すると、その中のメールも削除されます。そのため、空のフォルダーのみ削除できます |
恒久削除はコードで禁止されています。 Gmail の保証は構造によるもので、対象範囲内のメッセージを完全削除できません。一方、アプリパスワードではあらゆる操作が可能なため、ここでは imap_api.js の仕様として制限しています。expunge を行う箇所は 1 つだけで、removeDraft が Drafts フォルダー内の単一 UID に対して UID EXPUNGE を実行します。サーバーがこれに対応していない場合は、その下書きを Trash に移動します。通常の EXPUNGE を使うと、ほかのすべての \Deleted メッセージも削除されてしまうためです。サーバーがメールボックス全体の expunge で MOVE を再現する場合、移動は拒否されます。npm run test:imap はスクリプト化されたサーバーに対してクライアントを実行し、クライアントが行ったすべての expunge が Drafts 内の単一の下書きに対するものだったことを検証します。
ドキュメントのテキスト抽出
read_file_content と get_attachment は、Node の標準ライブラリのみを使用して、PDF、Word、Excel、PowerPoint、OpenDocument ファイルのテキストを返します。ベンダー提供の解析依存関係を導入したり、監査したり、最新の状態に保ったりする必要はありません。
| 形式 | 方法 | 備考 |
|---|---|---|
| .docx / .odt | ZIP + XML | 段落、タブ、改行を保持します。スタイルと変更履歴マークは破棄されます |
| .xlsx / .ods | ZIP + XML | シートごとに CSV ブロックを作成し、ブックのリレーションシップに基づいて名前を付けます。セルは列文字で配置されるため、空白の列もそのまま残ります |
| .pptx / .odp | ZIP + XML | プレゼンテーションの順序で、スライドごとに 1 つのブロックを作成します |
| コンテンツストリームの解析 | Flate、ASCII85、ASCIIHex のフィルターチェーンに対応します。リテラル文字列、16 進文字列、8 進文字列に対応し、位置指定演算子は改行に変換されます |
抽出できない場合と、そのときの動作。 PDF のうち 2 種類は、抽出できるテキストがありません。ページを画像化したスキャンと、CID キー付きまたはサブセット化されたフォントを使用し、コンテンツストリーム内のバイトが文字に変換するためにフォント固有のテーブルを必要とするグリフ番号になっているものです。どちらも一見もっともらしい無意味な結果にデコードされるため、読みやすさを評価し、結果を返すのではなく拒否します。拒否時には原因を示し、解決方法を案内します。
解決方法は read_file_content の ocr: true です。この設定ではファイルを Google Doc としてコピーし(その変換中に Drive が OCR を実行します)、テキストをエクスポートして、finally ブロックでコピーを削除します。スキャン、写真、画像を読み取れます。知っておくべき点が 2 つあります。読み取り中にこのサーバーが Drive に書き込む唯一の処理であり、この削除はサーバー全体で唯一の完全削除です。削除対象は同じ呼び出しが数秒前に作成したファイルであり、ユーザーがそこに置いたファイルではありません。クリーンアップ自体に失敗した場合は、その旨とファイル名を結果に示します。
他のすべての本文と同様、抽出結果は 60 KB で打ち切られ、その箇所にフラグが付けられます。
共有ドライブ
共有ドライブは個人ではなく組織が所有するもので、会社の実際のドキュメントが保存されています。Drive のデフォルト設定では共有ドライブが非表示になります。corpora=allDrives を指定せずに files.list を呼び出すと、マイドライブとアカウントに直接共有されたファイルだけが検索されます。また、共有ドライブ内のファイルを指定したリクエストには supportsAllDrives が含まれていないと 404 File not found が返されます。このエラーは、必要な機能がないのではなく、ID が間違っているように見えてしまいます。
このパラメーターを受け取るすべての Drive リクエストに、十八か所の呼び出し元それぞれではなく一か所で適用するようになりました。これにより、新しいエンドポイントを追加したときにこの不備がひそかに再発するのを防げます。どのメソッドがこのパラメーターを受け取るかは推測ではなく、v3 discovery document に基づいています。files.export、comments、replies はこのパラメーターを受け取らないため、意図的に対象から除外しています。
そのため、検索ではデフォルトでマイドライブとすべての共有ドライブが対象となり、検索結果にはファイルの ID だけでなく、そのファイルが属するドライブも表示されます。Google は検索範囲を可能な限り絞り込むことを推奨しています。そのため、list_shared_drives で ID を取得し、drive_id を指定して検索対象を一つのドライブに絞り込めます。
これにより、読み取りだけでなく書き込みの対象範囲も広がります。これまでは共有ドライブ内のファイルに対して update_file、share_file、trash_file を実行できませんでしたが、今は実行できます。共有ドライブには個人ではなく組織のドキュメントが保存されているため、まさにその状況に対応するために、以下の所有権ルールが設けられています。
可能な場合はドライブ名を取得し、取得できない場合はその旨を示します。drives.list は、このアカウントがメンバーになっているドライブをすべて取得し、Drive の一回あたり百件という上限で打ち切らずにページ分割して返します。そうしなければ、百件を超えるドライブに属するアカウントでは、あふれた分がメンバーではないように見えてしまいます。
フォルダーが直接共有されていることで、自分がメンバーではないドライブ内のファイルにアクセスできることもあります。そのようなドライブの名前は取得できません。ドライブの ID はルートフォルダーの ID でもありますが、ドライブ内のファイルにアクセスできても、その上位にあるルートフォルダーへのアクセス権は得られないため、問い合わせると File not found が返されます。実際の Workspace アカウントで検証したところ、最近アクセスされたファイルを含む三つのドライブのうち二つには、この方法でアクセスしていました。これらには単なる ID ではなく shared_drive_member: false が付くため、「名前を取得できない」と「検索に失敗した」を区別できます。
書き込みは自分が所有するファイルに対して行う
他の人が所有するファイル、または組織が所有するファイル(作成者が自分であっても、共有ドライブ内のすべてのファイル)は、モデルの判断だけでこのコネクターが変更してよいものではありません。
| 状況 | 動作 |
|---|---|
| 自分が所有している | 書き込みが実行されます |
| 自分が所有していない | マイドライブにプライベートコピーが作成され、そのコピーが編集されます |
| 元のファイルを編集したい | edit_original: true を指定すると下書きが返され、書き込みは行われません |
| ユーザーが承認した | confirm_edit: true を指定すると変更が適用されます |
重要なのは下書きです。下書きには各フィールドが from → to の形式で示されます。内容を置き換える場合は、現在の内容を読み取り、単に「40 KB を上書きします」と伝えるのではなく、実際に変更される行を表示します。バイナリコンテンツの場合は、プレビューを偽って表示するのではなく、プレビューできないことを示します。二回目の呼び出しが届くまで書き込みは一切行われないため、同僚のドキュメントが変更されたあとにその内容を知らされるのではなく、変更が適用される前に具体的な変更内容を確認できます。
プライベートコピーでは、意図的に parents: ['root'] を指定しています。files.copy で親を指定しないと、コピーは元のファイルと同じ場所に作成されます。共有ドライブ内のファイルの場合、コピーも同じ共有ドライブに残り、プライベートにはなりません。
share_file と trash_file でも下書きが作成されますが、コピーを作成する方法はありません。同僚のドキュメントをコピーして共有しても、その内容が広まるだけで、広がる範囲は狭まりません。
共有の取り消しと復元には、確認は必要ありません。 unshare_file と untrash_file は、自分が所有するかどうかにかかわらず、どのファイルに対しても確認なしで実行できます。アクセス範囲を広げるには承認が必要ですが、狭めるのに承認は不要です。許可が必要なブレーキは、ブレーキとは言えません。
繰り返しイベント
list_events はシリーズを各回に展開します。「火曜日に何がある?」と尋ねた場合、返されるのはその火曜日の定例会議であり、それを生成するルールではありません。そのため、手元にある ID はほぼ常に個々の回を指しており、Google には編集がその回だけを対象とするのか、シリーズ全体を対象とするのかを示すフラグがありません。
そこで、update_event と delete_event は scope を受け取ります。
| scope | 効果 |
|---|---|
this_event(デフォルト) | event_id が指定する回だけを変更します。「明日の定例会議を移動する」 |
series | その回をシリーズに解決し、すべての回を変更します。「今後はずっと午前10時にする」 |
読み取り結果には recurring_event_id が含まれ、これによって初めてその解決が可能になります。また、すべての書き込み結果には、実際にどちらの操作が行われたかが示されます。特に次のケースでは重要です。シリーズ自体を指す ID は、this_event を指定してもすべての回を変更します。シリーズへのパッチ適用はそのように機能するためです。そのため、結果には黙って処理するのではなく、applies_to: "series" と示されます。
繰り返しイベントを作成するには、repeat: "daily" | "weekly" | "monthly" | "yearly" を指定し、終了条件として repeat_count または repeat_until を指定します。「毎月第2火曜日」や「平日のみ」など、これらでは表現できないルールは、RFC 5545 の行として recurrence に指定します。こうしたルールは Google に送る前にここで検証します。FREQ のないルール、認識できない頻度、または DTSTART 行がある場合は、Google が一般的な 400 を返すのとは異なり、問題の内容が示されます。繰り返しルールはシリーズに属するもので、個々の回に書き込むと黙って破棄されるため、その場合も拒否されます。
セットアップ
- Google Cloud — プロジェクトを作成し、Gmail、Calendar、Drive、People
and Tasks API を有効にします。OAuth 同意画面を External として設定し、リンクする予定のすべての Google アドレスをテストユーザーとして追加します。タイプが
Web application の OAuth クライアントを作成し、承認済みのリダイレクト URI に
<PUBLIC_BASE_URL>/gmail/oauth/callbackを正確に設定します。 - 環境変数 —
DATABASE_URL、PUBLIC_BASE_URL、GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET、TOKEN_ENC_KEY、JWT_SECRETを設定し (.env.exampleを参照)、デプロイします。スキーマは起動時に自動適用されます。 - アカウントをリンク —
<PUBLIC_BASE_URL>/gmail/connectにアクセスし、Google に サインインしてから、各アカウントを一度ずつリンクします。サインインに使うアカウントと リンクするアカウントは別々です。自分のアカウントでサインインし、その ID のもとで 好きなメールボックスをリンクできます。 - Claude に追加 — Settings → Connectors → Add custom connector →
<PUBLIC_BASE_URL>/mcp。Claude が自身を登録し、同意画面に移動するので、そこで一度だけ Google にサインインします。
誰が何を見られるか
すべてのメールボックスはリンクした ID に紐付けて保存され、すべての MCP トークンには、最初の
許可時だけでなく更新時にも、その ID がサブジェクトとして含まれます。同じデプロイを使う 2 人は
それぞれ自分のアカウントにしかアクセスできず、ツール呼び出しで他人のアドレスを指定すると、
そのアカウントが提供されるのではなく拒否されます。npm run test:isolation はまさにこれを検証します。
すべてのトークンが同じサブジェクトを持っていた、単一オペレーター向けビルドからアップグレードする場合:
LEGACY_OWNER_EMAIL に、そのメールボックスが属するアドレスを設定してください。そのアドレスで
最初にサインインすると、メールボックスが引き継がれます。再リンクは不要です。その後、設定を解除してください。
プロダクトを追加する前にアカウントをリンクした場合: 許可されたスコープはリンク時に固定されるため、 Calendar、Drive、Contacts、Tasks が存在する前にリンクしたアカウントには、Gmail のみの許可が残っています。 Google がこれを遡って拡張することはありません。各アカウントで再度
/gmail/connectにアクセスしてください。不足しているアクセス権が追加され、それ以外は何も変わりません。 その間、新しいツールはアカウント名と再リンクするよう案内を示すため、内容の分からない 403 エラーにはなりません。
デプロイ
Node を実行でき、Postgres に接続できるホストであれば動作します。ビルドもコンテナ定義も不要で、 スキーマは起動時に自動適用されます。新規デプロイに必要なのは環境変数だけです。
環境
| 変数 | 説明 |
|---|---|
DATABASE_URL | Postgres。6つのテーブルは初回起動時に自動作成されます |
PUBLIC_BASE_URL | 公開 URL のオリジン部分。スキームとホストのみを指定し、末尾にスラッシュを付けないでください。OAuth の発行者 URL と Google のリダイレクト URI に使用されるため、Google に登録した値と一致させる必要があります |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | Web アプリケーションの OAuth クライアントから取得します |
TOKEN_ENC_KEY | 32 バイトの Base64 文字列。Google トークンと AOL アプリパスワードを保存時に暗号化します。これを紛失または変更すると、保存済みの認証情報をすべて復号できなくなります |
JWT_SECRET | 任意の長いランダム文字列。個別に導出された2つのキーを使って、MCP トークンとブラウザーセッションに署名します。変更すると、全員が再認証する必要があります |
LEGACY_OWNER_EMAIL | 任意。一度だけ使用します。ユーザーごとのサインイン機能が導入される前にリンクされたメールボックスを、そのアドレスで初めてサインインしたときに引き継ぎます |
PORT | 任意。デフォルトは3000です。ほとんどのプラットフォームでは自動設定されます |
STRIPE_SECRET_KEY / STRIPE_PRICE_ID | 任意。両方を設定すると、ツール呼び出しに対してサブスクリプション料金を請求します(サインイン、リンク、削除は引き続き無料です)。未設定の場合、課金は無効になります |
STRIPE_WEBHOOK_SECRET | <PUBLIC_BASE_URL>/billing/webhook エンドポイントの署名シークレット(イベント:checkout.session.completed、customer.subscription.*) |
BILLING_PRICE_LABEL | 公開ページに表示する料金。デフォルトは $0.99/month です。Stripe の料金と一致させてください |
BILLING_EXEMPT_EMAILS | カンマ区切り。これらのメールボックスをリンクしたユーザーには、料金が発生しません |
/gmail/check は、Google クライアントの設定不備を一連の同意フローを実行せずに診断し、Google が拒否した2つの変数のうちどちらかを報告します。値がそのまま表示されることはありません。
Railway の場合
このリポジトリからプロジェクトを作成し、Postgres サービスを追加します。これにより、アプリサービスから参照する DATABASE_URL が提供されます。上記の残りの変数を設定し、生成されたドメインを PUBLIC_BASE_URL として指定したうえで、OAuth クライアントに <PUBLIC_BASE_URL>/gmail/oauth/callback を承認済みのリダイレクト URI として登録します。デフォルトブランチにプッシュすると、自動的に再デプロイされます。
既存のデプロイを移行する場合
次の2点により、ダウンタイムも再リンクも不要な切り替えが可能になります。
- 新しいデプロイから同じデータベースを参照します。 スキーマは
CREATE TABLE IF NOT EXISTSを使用しているため、すでに存在するテーブルには何も行われず、リンク済みのすべてのアカウントがそのまま引き継がれます。 TOKEN_ENC_KEYは変更せずに引き継ぎます。 トークンには AES-256-GCM を使用しています。認証付き暗号方式のため、異なるキーではタグチェックに失敗してエラーが発生し、不正なデータが返されることはありません。ただし、元のキーを復元するまで、すべてのアカウントが使用できなくなります。
OAuth クライアントには複数のリダイレクト URI を設定できるため、古い URI を置き換えるのではなく、新しいデプロイのコールバックを追加してください。こうすると、2つのデプロイを同じデータベースに対して並行稼働させられます。この5つのテーブルに書き込みを行うものはほかにないため、古いデプロイを終了する前に新しいデプロイを検証できます。
JWT_SECRET を変更した場合、Claude はその後、もう一度認証する必要があります。リンク済みの Google アカウントは維持されます。アカウントは特定のトークンではなく、固定のサブジェクトに紐付けられているためです。
セキュリティに関する注意事項
- Google のリフレッシュトークンとアクセストークンは、
TOKEN_ENC_KEYを使って保存時に AES-256-GCM で暗号化されます。データベースに使用可能なトークンが保存されることはありません。 - メールは完全に削除できません。 Gmail のスコープは
gmail.modifyで、読み取り、送信、ラベル付け、アーカイブ、ゴミ箱への移動が可能です。意図的にmail.google.comは含めていないため、trash_messageでゴミ箱に移動したメールは30日間復元可能であり、この機能でメールを破棄することはできません。 - Drive は異なり、その違いを知っておく価値があります。 Drive には同等の中間スコープがありません。
drive.fileではこのアプリが作成したファイルしか表示されないため、「自分の賃貸契約書を探す」といった用途には対応できません。そのため、既存ファイルの検索や編集には、完全削除も可能なdriveが必要です。Drive では、ツールの提供範囲によって制限を設けています。trash_fileはゴミ箱に移動しますが、Drive の完全削除エンドポイントにアクセスできるツールはありません。可能な操作ではなく、公開されている操作の範囲による制限であるため、Gmail よりも保証は弱くなります。 - 外部への操作には必ず取り消し手段があり、それが操作を提供する条件です。
unshare_fileはshare_fileが設定した共有権限を取り消し、untrash_fileはtrash_fileでゴミ箱に移動したものを復元します。ファーストパーティのコネクタにはどちらもないため、付与した権限を同じインターフェースから取り消すことはできません。ここだけは、こちらのほうが操作範囲が広く、より安全です。 - AOL のアプリパスワードは、保存される認証情報のうち OAuth の付与ではない唯一のものです。 トークンと同様に暗号化され、保存前に検証されます。ログに記録されることはありません(どちらのプロトコルクライアントもログ機能を無効にして実行されます)。このサービスから取り消すことはできません。リンク解除時に保存済みのコピーは削除されます。また、リンクページと削除ページでは、AOL 側でもアプリパスワードを削除するよう案内しています。リンクフォームにはサインインのレート制限が適用されるため、このサーバーを使って AOL のパスワードを推測することはできません。
- Contacts はスコープによって読み取り専用になっており、単に書き込み機能を省略しているわけではありません。
- ファーストパーティのコネクタではできない操作に対して、Drive の書き込みをガードしています。 Claude の Drive コネクタでは、メールアドレスとロールを指定して共有できますが、ファイルを公開する方法はありません。また、
update_fileで変更できるのはタイトルと親フォルダーだけです。次の2つのガードは、この既定の動作に合わせています。どちらも一方向の操作であり、ツールの結果からは危険に見えない場合があるためです。share_fileで指定できるのは、名前を指定した個人のみです。ドメイン全体への共有や公開リンクによる共有にはDRIVE_ALLOW_PUBLIC_SHARING=trueが必要です。これが設定されていない場合、より安全な操作へ暗黙に切り替えるのではなく、明示的に拒否されます。有効にした場合も、2つ目の確認手段としてconfirm_public: trueが必要です。拒否時には、共有権限を付与すると実際にどうなるかが明記され、その後あらためて操作を試みるよう促されます。- ファイルの内容を上書きするには、
contentとともにreplace_content: trueを指定する必要があります。これにより、名前の変更に伴って文書が誤って破壊されることはありません。
- 認可コードは1回限りで、ハッシュ化されます。PKCE S256 が必須です。リフレッシュトークンは使用するたびにローテーションされ、
redirect_uriは登録内容と完全に一致する必要があります。 - Google アプリが Testing のままの場合、リフレッシュトークンは7日後に期限切れとなり、アカウントを再リンクする必要があります。アプリを公開すれば期限切れは発生しませんが、Gmail、Drive、Contacts はいずれも制限付きスコープであるため、Google による検証が適用されます。
アーキテクチャ
src/services/google_http.js shared transport: URL building, bearer auth,
error unwrapping, bounded-concurrency mapLimit
src/services/gmail_api.js one thin client per Google API, each returning
src/services/calendar_api.js shaped results rather than raw payloads
src/services/drive_api.js
src/services/people_api.js
src/services/tasks_api.js
src/services/office_text.js .docx/.xlsx/.pptx/.odt text, via a ZIP reader
src/services/pdf_text.js PDF content-stream text, with a readability guard
src/services/text_extract.js one entry point both Drive and Gmail read through
src/services/gmail_accounts.js linked accounts, token refresh, scope gating
src/services/imap_api.js AOL over IMAP/SMTP, the same surface as gmail_api
src/services/imap_query.js Gmail search syntax → IMAP SEARCH, refusing what it can't map
src/services/imap_accounts.js linked AOL mailboxes, app passwords encrypted at rest
src/services/imap_providers.js host presets — the only place a mail host comes from
src/mcp/shared.js resolveAccount / tokenFor / fanOut / mergeSearch, and
mailFor / fanOutMail, which pick Gmail or IMAP per mailbox
src/mcp/tools/ one module per product, assembled by index.js
レート制限は回避するのではなく、遵守します。Google はユーザーごとに秒単位でクォータを管理するため、アイテムごとの詳細取得は一度に5件ずつ実行します。また、それでも取得に失敗した場合は、結果から黙って除外せず、unavailable_* に計上します。
テスト
すべてプレーンな Node で実行し、フレームワークは使用しません。564件のチェックがあります。
# No database, no network:
npm run test:tokens # MCP and user tokens cannot be swapped, in either direction
npm run test:query # Google query-string construction (repeated array params)
npm run test:mime # entity decoding, body truncation, MIME construction
npm run test:threads # address parsing, thread summaries, cross-account fan-out
npm run test:products # free-slot arithmetic, Drive query quoting, multipart
# upload framing, task dates, scope gating, tool-surface shape
npm run test:drive-safety # the sharing and overwrite guards, both env states
npm run test:extract # PDF and Office text extraction, against real fixtures
npm run test:drive-parity # upload framing, conversion, export and comments, on the wire
npm run test:reach # shared-drive request parameters, recurrence rules, and which
# event a scoped write actually lands on
npm run test:ownership # which file id a write reaches, that a draft writes nothing,
# and that revoking is never gated
npm run test:imap # AOL: query translation, and the client against a scripted
# IMAP/SMTP server — moves, ids, Bcc, Sent copies, no expunge
npm run test:isolation # one person's mailboxes, Gmail or AOL, are unreachable by another
npm run test:ai # AI messages on in-memory Postgres: threads, wakes, reply links, hops
# Needs a database:
DATABASE_URL=postgres://… npm run test:accounts # resolution + encryption at rest
DATABASE_URL=postgres://… npm run test:imap-accounts # AOL app passwords at rest, owner scoping
# Needs the API running:
npm start &
TEST_BASE_URL=http://127.0.0.1:3000 npm run test:mcp # OAuth + MCP protocol
既存のアプリ内に組み込む
src/app.js はスタンドアロンサーバー全体で、コネクター以外は含まれていません。そのため、組み込む際はこのファイルの本体をコピーします。2 つのルーター(routes/mcp、routes/gmail_link)を読み込み、レート制限と 4 つの .well-known ディスカバリールートを維持し、マイグレーションを独自のものと並行して実行してください。
省略してはいけない点が 1 つあります。MCP トークンは JWT_SECRET 自体ではなく、HMAC によって JWT_SECRET から派生したキーで署名されます。アプリが独自のユーザートークンをこのシークレットで署名し、オーディエンスをチェックせずに検証している場合、キーを共有すると、すべてのコネクタートークンがアプリで有効なユーザートークンになってしまいます。npm run test:tokens では、両方向で分離されていることを検証します。
コントリビューション
Issue やプルリクエストを歓迎します。まず知っておいていただきたい規約が 2 つあります。
- テストスイートはプレーンな Node です — フレームワークも新しい依存関係も使いません。それぞれ
test/配下のファイルで、npm run test:<name>スクリプトを使い、チェックごとに 1 行で記述し、失敗時はゼロ以外の終了コードを返します。 - テストでは通信で送信される内容を検証します。
global.fetchをスタブし、クエリパラメーター、リクエストパス、マルチパートの構成をチェックします。ここで検出すべき失敗は、誤った内容をリクエストしているのに呼び出しが成功するケースです。これを検出できるのは、通信レベルの検証だけです。
PROJECT-LOG.md には、却下した代替案も含め、さまざまな決定の理由が記録されています。決定を変更する前にお読みください。
ライセンス
MIT — LICENSE を参照してください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。