Skip to content

0 requirements.ja

Claude Lin & Lay edited this page Aug 6, 2026 · 7 revisions

要件仕様

目的

GitHub Webhook イベントを Cloudflare Worker で受信・永続化し、MCP プロトコル経由で AI エージェントに提供する。 エージェントが GitHub の状態変化をリアルタイムに検知し、自律的に対応できるようにする。

前提

  • GitHub Webhook は Cloudflare Worker に直接到達する(ローカルサーバー不要)
  • イベントは Durable Object 内の SQLite に永続化される
  • AI エージェントは MCP stdio トランスポート(ローカルブリッジ)または Streamable HTTP(リモート)で接続する
  • ローカルブリッジは Worker にツール呼び出しをプロキシし、SSE でリアルタイム通知を中継する

アーキテクチャ

GitHub --POST--> Cloudflare Worker --> TenantRegistry DO
                  | 署名検証                    | installation_id -> account_id
                  | UUID 付与                    |
                  |                             v
                  |                    WebhookStore DO (SQLite) [per-tenant]
                  |                             |
                  +-- /mcp (Streamable HTTP)     +-- WebSocket / SSE real-time stream
                  |    createMcpHandler          +-- REST endpoints
                  |    [stateless, per-request]       /pending-status
                  |    +-- tools -> WebhookStore       /pending-events
                  |                                    /webhook-events
                  +-- /events (WebSocket/SSE)           /event
                  |    +-- WebhookStore DO             /mark-processed
                  |
                  +-- /oauth/authorize --> github.com/login/oauth/authorize
                  +-- /oauth/callback  <-- github.com redirect_uri (Worker-hosted)
                  +-- /oauth/token (web_authorization_poll, refresh_token)
                  |
                  +-- /webhooks/github (POST)
                       +-- TenantRegistry -> WebhookStore DO /ingest

                          +-----------------------------+
                          |  Local MCP Bridge (.mcpb)    |
                          |  stdio <- Claude Desktop/CLI |
                          |  -> proxy tool calls to /mcp |
                          |  -> WebSocket listener -> channel |
                          |  -> browser: /oauth/authorize  |
                          |  -> poll:    /oauth/token      |
                          +-----------------------------+

システムは四つのコンポーネントで構成される:

  1. Cloudflare Worker — Webhook 受信、署名検証、テナントルーティング
  2. TenantRegistry Durable Object — installation_id → account_id マッピング管理、テナント単位クォータ管理(単一インスタンス)
  3. WebhookStore Durable Object — SQLite によるイベント永続化、REST/SSE エンドポイント(テナント別インスタンス: store-{accountId}
  4. ステートレス MCP ハンドラ — MCP Streamable HTTP サーバー、ツール定義。プロトコル版 2026-07-28 でセッションが消えたため Durable Object ではなく、リクエストごとに createMcpHandler が組み立てる(#249)

ローカルブリッジ(mcp-server/)は Worker に対するプロキシであり、データを保持しない。

プロトコル版と単レーン切替(#249)

Worker は MCP プロトコル版 2026-07-28(ステートレスコア)のみを提供する。createMcpHandlerlegacy: "reject" を渡しており、2025 系のリクエスト(initialize を含む)は、この endpoint が提供する唯一の版を名指しした unsupported-protocol-version エラーで返る。

Worker のプロトコル版は、本 repo の 2 成果物(Worker と npx ブリッジ)のあいだの私的な契約である。mcp-server/server.json は stdio トランスポートのみを宣言しており、リモートトランスポートの宣言が無いため、第三者クライアントが Worker に直接到達する経路は存在しない。したがって Claude Desktop 側の対応状況は Worker 側移行の条件ではない。ブリッジは二面を持ち、両面は独立している:

実装
Claude Desktop に対してサーバー SDK v1 stdio(Server クラス) 2025 系
Worker に対してクライアント SDK v2(@modelcontextprotocol/client 2026-07-28 に pin

互換レーンは実装しない。 保守されないフォールバック経路は放置されて死にコード化する。畳む条件が観測可能(旧レーンへの到達がゼロになる)であっても、観測できることと観測しに行くことは別であり、運用上その判断は忘れられる。破断の吸収はコードではなく、minor リリース + README 注意書きという別チャネルで行う。

非対称の所在: Worker は全員共有の 1 デプロイなので一斉に飛ぶ。ブリッジは利用者ごとで、各自の MCP クライアント再起動で追随する(@latest 指定でも起動済みプロセスは古い版を保持する — #247 / #248)。版を固定している外部利用者は再起動しても復旧しない。この層の切り捨てを受け入れるというのが本決定である。

機能要件

F1. Webhook 受信

ID 要件
F1.1 POST /webhooks/github で GitHub Webhook ペイロードを受信する
F1.2 X-Hub-Signature-256 ヘッダによる HMAC-SHA256 署名検証を行う
F1.3 署名不一致時は HTTP 403 を返す
F1.4 シークレット未設定時は署名検証をスキップする
F1.5 テナント単位クォータ超過時は HTTP 429 を返す(installation イベントはクォータチェックをスキップ)
F1.6 認証チェック順序: IP allowlist → per-IP rate limit → signature → tenant resolution → per-tenant quota → ingest

F2. イベント永続化

ID 要件
F2.1 イベントを UUID 付きで WebhookStore DO の SQLite に保存する
F2.2 各イベントは id, type, payload, received_at, processed フィールドを持つ
F2.3 trigger_status, last_triggered_at フィールドを保持する(将来の trigger 機能用)
F2.4 mark_processed 実行時に、processed=1 かつ received_at が保持期間(PURGE_AFTER_DAYS 日、既定 3)より古いイベントを自動削除する。処理済み死蔵行による DO ストレージ肥大を防ぐ(即時性のための補助経路。本質的な保証は F2.5 の Alarm sweep が担う)
F2.5 DO Alarm(ctx.storage.setAlarm + alarm() ハンドラ)による時間駆動の retention sweep を提供する。sweep は (a) processed=1 かつ received_atPURGE_AFTER_DAYS 日(既定 3)より古い処理済みイベントと、(b) processed=0 かつ received_atUNPROCESSED_PURGE_AFTER_DAYS 日(既定 90)より古い未処理イベントを、まとめて削除する。alarm() は実行後に次回 sweep を再スケジュールし、消費ゼロ(mark_processed が一度も呼ばれない放置テナント)でも周期実行(日次)される。未処理保持期間を処理済みより長く取るのは未読データ消失の安全域確保のため(3 日 / 90 日の非対称は意図的)。既知の限界(スコープ外): 時間窓は「古さ」を縛るが「量」は縛らないため、高頻度 × 無消費テナントは 90 日窓があっても Cloudflare の 1GB/DO 壁に先に到達しうる。量ベース hard cap は別 issue 候補

イベント構造:

{
  "id": "uuid",
  "type": "issues",
  "payload": {},
  "received_at": "ISO8601",
  "processed": false,
  "trigger_status": null,
  "last_triggered_at": null
}

F3. MCP ツール

ステートレス MCP ハンドラが以下のツールセットを提供する。ローカルブリッジはこれをプロキシする。

ID ツール名 引数 戻り値 要件
F3.1 get_pending_status なし pending_count, latest_received_at, types 未処理イベントの軽量スナップショットを返す
F3.2 list_pending_events limit (1-100, default 20) サマリー配列 未処理イベントのメタデータ一覧を返す(ペイロード含まず)
F3.3 get_event event_id 完全イベント or error UUID 指定で完全なペイロードを返す
F3.4 get_webhook_events limit (1-100, default 20) 未処理イベント配列 未処理イベントをフルペイロード付きで返す
F3.5 mark_processed event_id または event_ids (1-100) 単数形: success, event_id, purged/バッチ形: success, marked, failed, results (id 単位の判定), purged イベントを処理済みにマークし、保持期間超過の処理済みイベントを自動削除する。purged は今回削除された件数

F3.1 ローカルブリッジ整形: ローカルブリッジは get_pending_status の戻り値を Claude Code UserPromptSubmit hook の decision JSON shape (hookSpecificOutput.hookEventName="UserPromptSubmit" + additionalContext に pending_count / types / latest_received_at の自然文要約) にラップして返す。これは type: "mcp_tool" UserPromptSubmit hook 経由の呼び出しで戻り値が AI 文脈に注入されるための要件であり、手動 tool 呼び出し時も同 shape で返る。リモート (Worker + DO) 側の戻り値構造は変更しない。

F3.1 空状態の silent (empty silent, #221): pending_count == 0 の場合はラップせずリモート戻り値をそのまま返す。Claude Code 側で decision schema に一致しない JSON は silent discard されるため、hook 経由の呼び出しで additionalContext に何も注入されず、毎ターン空 reminder のノイズが消える。手動 tool 呼び出しではリモートの raw payload ({pending_count: 0, types: {}, latest_received_at: null}) が返り、AI は内容を直接判定できる。

F3.5 バッチ形 (#245): mark_processed が 1 呼び出しにつき 1 イベントしか受けなかったため、自己操作 1 回で 6〜10 件生じる到達確認イベントの消費が呼び出し回数に比例していた。event_ids: string[] を追加し、id を明示列挙したまま往復だけ畳む(filter 一括消費は外部イベントを誤って消しうるため採らない)。

  • 後方互換: 既存の単数 event_id 呼び出しは戻り値の形も含めて不変。存在しない id でも success: true を返す旧挙動を維持する。
  • id 単位の判定: バッチ形は id ごとに {event_id, success, error?} を返す。tool 表層で error が取る値は not found(どの store にも無い)のみ。空の id は store に届く前に tool schema が弾くため、誤解を招く not found に化けることはない。
  • 部分失敗: 1 件の失敗で全体を落とさない。成功した id のマークは確定済みで、呼び出し側は失敗した id だけ再送すればよい。部分失敗は tool error ではなく本文で報告する。
  • マルチアカウント: イベントは 1 つの store にのみ存在するため、アクセス可能な全 store に同じバッチを投げ、いずれかの store が一致した id を成功とする。全 store が取り逃した id のみ失敗。
  • purge 回数: 保持期間 purge はバッチ 1 回につき 1 回のみ走る(id ごとではない)。
  • MCP proxy: 静的スキーマ (mcp-server/server/index.js / local-mcp/src/index.ts) の更新が要る。新 param は再接続では反映されず npm 再公開が必要。

イベントサマリー構造:

{
  "id": "uuid",
  "type": "issues",
  "received_at": "ISO8601",
  "processed": false,
  "trigger_status": null,
  "last_triggered_at": null,
  "action": "opened",
  "repo": "owner/repo",
  "sender": "username",
  "number": 123,
  "title": "Issue title",
  "url": "https://github.com/..."
}

F4. SSE リアルタイムイベント配信

ID 要件
F4.1 GET /events で SSE ストリームを提供する
F4.2 Webhook ingest 時に接続中の全 SSE クライアントにイベントサマリーをブロードキャストする
F4.3 30 秒間隔でハートビートを送信する
F4.4 クライアント切断時にクリーンアップする

F5. チャンネル通知(ローカルブリッジ)

ID 要件
F5.1 ローカルブリッジが Claude Code の claude/channel experimental capability を宣言する
F5.2 Worker の SSE エンドポイントに接続し、新規イベント検出時に notifications/claude/channel を送信する
F5.3 通知内容はイベントサマリー(type, repo, action, title, sender)を含む
F5.4 meta フィールドに chat_id, message_id, user, ts を付与する
F5.5 WEBHOOK_CHANNEL=0 環境変数でチャンネル通知を無効化できる(デフォルト: 有効)
F5.6 チャンネル通知は one-way(読み取り専用)で、reply tool は提供しない

F6. 推奨ポーリングフロー

ステップ 操作
1 get_pending_status() を 60 秒間隔でポーリング
2 pending_count > 0 なら list_pending_events() でサマリー取得
3 フルペイロードが必要なイベントのみ get_event(event_id) で取得
4 処理完了後 mark_processed でマーク。複数件をまとめて処理した場合は event_ids で 1 呼び出しに畳む

F7. OAuth 認証(Worker-hosted web OAuth)

Worker は GitHub の web OAuth flow をホストする独自実装を備える(v0.11.0 の device authorization grant は v0.11.1 で撤去。GitHub 標準のログイン + 2FA UX に回帰しつつ、v0.10.x の chronic auth loop 原因である localhost callback 依存と refresh rotation desync を構造的に解消する)。

ID 要件
F7.1 GET /.well-known/oauth-authorization-server で RFC 8414 メタデータを返す(authorization_endpoint / token_endpoint / grant_types_supported=[urn:ietf:params:oauth:grant-type:web_authorization_poll, refresh_token]
F7.2 POST /oauth/register で RFC 7591 dynamic client registration を行う(public client、secret 発行なし)
F7.3 GET /oauth/authorize?client_id=<cid>&state=<state>[&scope=...]web_auth_state:{state} レコードを pending として作成し、redirect_uri=https://<worker>/oauth/callback を固定して https://github.com/login/oauth/authorize に 302 リダイレクトする
F7.4 GET /oauth/callback?code=<gh_code>&state=<state> で GitHub authorization code を confidential client として access token に交換し、fetchGitHubProps() で user profile + installations を取得、Worker 独自 bearer token pair を発行して web_auth_stateapproved に遷移させる。ユーザにはタブを閉じるよう案内する HTML を返す
F7.5 POST /oauth/tokengrant_type=urn:ietf:params:oauth:grant-type:web_authorization_poll を処理する。pending400 authorization_pendingapproved200 で bearer pair を返し state レコードを消費、denied400 access_denied、期限切れ → 400 expired_token(RFC 8628 §3.5 のエラー形式を再利用)
F7.6 POST /oauth/tokengrant_type=refresh_token を処理し、access token と refresh token を rotate する(ブリッジ側 RC1 修正と組み合わせて desync を解消)
F7.7 保護対象 API ルート(/mcp, /events)は Authorization: Bearer <access_token> ヘッダによる独自 token 検証 middleware で認可する
F7.8 KV schema は自前設計: client:{client_id} / web_auth_state:{state} / token:{access_token} / refresh:{refresh_token} / grant:{grant_id}。device flow 時代の device: / user_code: キーは撤去
F7.9 ローカルブリッジは authorize URL を platform 既定のブラウザで自動オープンする。Windows は cmd /c start、macOS は open、Linux は xdg-open を使う。オープン失敗は fatal にしない(stderr に警告を残し、URL は応答と stderr で伝える)
F7.10 ローカルブリッジは初回ツール呼び出しで web flow が完了していない場合、polling をバックグラウンドに維持したまま、authorize URL と残り有効秒数を本文に含む isError: true の構造化ツール応答を即座に返す。2 回目以降の同一ツール呼び出しは、承認完了なら通常処理、未完了なら同じ auth-required 応答を返す(ポーリングは 1 本に serialize)
F7.11 ローカルブリッジは refresh 時に invalid_grant を受けた場合、直ちに全面 re-auth に遷移せず tokens file を再読み込みする。別プロセスが既に rotation を完了していれば、その最新 refresh_token を採用して再試行する(RC1: refresh desync の最小 fix。file lock は導入しない)

Dynamic Client Registration の位置づけ(#249 で確認、撤去は本 issue の範囲外):

MCP 2026-07-28 の deprecated レジストリ 6 件のうち本 repo に該当するのは Dynamic Client Registration(F7.2)のみ。移行先は Client ID Metadata Documents。最短撤去は 2027-07-28 以降の最初のリビジョンであり、しかも「最短撤去」は撤去が可能になる時点にすぎず、実際の撤去は Core Maintainer 判断でそれ以降にずれうる。仕様からの撤去は SDK に削除義務を課さない。

本移行では撤去しない。理由は相乗りの前提が成立しないこと — 本移行は OAuth の実装に一切触れておらず(ブリッジ側は transport の authProvider に繋ぎ直しただけ)、DCR 撤去は両面の認証設計変更になる。プロトコル切替と同一リリースに載せると、利用者が再起動して降りてくる先で破断面が二つ同時に開く。

該当しないもの: Roots / Sampling / Logging / includeContext(未使用)、HTTP+SSE トランスポート(Streamable HTTP のみを提供しているため未露出)。

GitHub App 前提条件:

  • 使用する upstream endpoint: https://github.com/login/oauth/authorize(web), POST https://github.com/login/oauth/access_token
  • GitHub App の設定で redirect_uri = https://<worker>/oauth/callback を登録する必要がある(smgjp.com プレビュー + self-host 例示)

非機能要件

N1. セキュリティ

ID 要件
N1.1 Webhook シークレットは Cloudflare Worker の Secret(GITHUB_WEBHOOK_SECRET)で管理する
N1.2 HMAC-SHA256 による署名検証でスプーフィングを防止する
N1.3 ローカルブリッジは stdio トランスポートを使用し、ネットワーク露出しない
N1.4 Webhook エンドポイントは多層防御で DDoS/課金攻撃を防止する: IP allowlist → per-IP rate limit → signature → tenant resolution → per-tenant quota
N1.5 GitHub IP allowlist(api.github.com/meta の hooks フィールド)で非 GitHub IP を最外層でブロックする(github-ip.ts)
N1.6 Per-IP rate limit はインメモリ sliding window で Worker isolate 内に実装する(rate-limit.ts)
N1.7 Per-tenant quota は TenantRegistry DO で atomic check-and-increment により管理し、単一テナントの無制限ストレージ消費を防止する
N1.8 Cloudflare WAF カスタムルールによる外部 IP ブロックを推奨する(Worker 到達前にブロックし CPU 課金を削減)

N2. 構成

ID 項目 ソース デフォルト
N2.1 Worker URL WEBHOOK_WORKER_URL 環境変数 なし(必須)
N2.2 シークレット Cloudflare Secret GITHUB_WEBHOOK_SECRET なし(検証スキップ)
N2.3 チャンネル通知の有効/無効 WEBHOOK_CHANNEL 有効(0 で無効)
N2.4 カスタムドメイン github-webhook.smgjp.com Cloudflare Worker のカスタムドメインとして設定済み
N2.5 認証方式 Worker 自前認証 Cloudflare Access は使用しない。Worker が webhook secret + Worker-hosted web OAuth で認証を処理する
N2.6 プレビューインスタンス preview 環境 本番と同一構成の検証用インスタンス
N2.7 処理済みイベント保持期間 PURGE_AFTER_DAYS 環境変数(wrangler.toml [vars] 3(日)。mark_processed 時および DO Alarm sweep 時に保持期間超過の処理済みイベントを削除。0 で処理済みを即削除
N2.8 未処理イベント保持期間 UNPROCESSED_PURGE_AFTER_DAYS 環境変数(wrangler.toml [vars] 90(日)。DO Alarm sweep 時に保持期間超過の未処理イベントを削除。0 で未処理を即削除。処理済み(3 日)より長いのは未読データ消失の安全域(非対称は意図的)

N3. 制約

ID 制約
N3.1 WebhookStore DO はテナント別インスタンス(idFromName("store-{accountId}"))で動作する。MCP 側にテナント別インスタンスは無い(2026-07-28 でセッションが消え、リクエストごとの props から store 名を解決する)。TenantRegistry DO は単一インスタンスで全テナントの installation-account マッピングを管理する
N3.2 SSE 接続は DO のメモリ内で管理される(DO eviction 時に切断)
N3.3 Worker とブリッジのあいだにセッションは無い(2026-07-28 ステートレスコア、#249)。ブリッジが再利用するのは MCP クライアントとその transport であり、切断のコストは再接続一回に閉じる
N3.5 Worker は 2026-07-28 のみを提供し、2025 系リクエスト(initialize を含む)を legacy: "reject" で拒否する。互換レーンは持たない(#249 決定 1)。ブリッジのクライアント面も同じ版に pin する
N3.4 Web OAuth callback 処理時に GET /user/installations で取得した accessible_account_ids(ユーザー + org)を GitHubUserProps に保存し、MCP ツールが複数 store を並列クエリして結果をマージする。これにより org インストールのイベントもメンバーからも参照できる。accessible_account_ids はリクエストごとの props から読む

CI/CD

テスト(CI)

トリガー ジョブ 内容
PR to main / push to main test Node.js syntax check

リリース(CD)

トリガー ジョブ 内容
release published build-mcpb mcpb pack で .mcpb 生成
release published attach-mcpb gh release upload で .mcpb をリリースに添付(build-mcpb 後)
release published npm-publish npm レジストリに公開

リリースフロー:

  1. AI が gh release create でリリースを作成する(PAT 経由で release イベントが発火する)
  2. Release published イベントで CD ワークフローが発火: .mcpb 生成 → .mcpb リリース添付 → npm publish
  3. npm publish 時にリリースタグ名から自動でバージョンを同期する(package.json の手動更新不要)
  4. プレリリースタグ(- を含む)は next dist-tag で公開、正式リリースは latest で公開

配送鎖と完了条件

merge → gh release create → CD (npm-publish) → registry の latest 更新
      → 利用側 MCP クライアントの再起動 → npx が新版を解決 → 利用側に到達

後半 2 段は CD の外側にあり、リポジトリ側からは実行できない。npx がパッケージのバージョンを解決するの はプロセス起動時の一度きりであり(クライアント設定で @latest を指定していても同じ)、すでに起動して いるプロセスは registry がどう変わっても起動時のバージョンを保持し続ける(v0.11.9 のリリース直後に実 測: registry が 0.11.9 を返している間、稼働中のクライアントは 0.11.8 のままだった。0.11.9 に移ったの は Claude Desktop を再起動した時点である)。

したがって 「registry が新版を返す」はリリース完了の判定基準にならない。特にプロキシの静的ツール スキーマを変更したリリースは、利用側プロセスが再起動して初めて成果が現れる。リリース完了報告を registry の確認で締めると、届いていない状態を届いたと報告することになる。

registry の確認には --prefer-online を付ける。npm CLI は registry のメタデータをキャッシュするため、 publish 直後の npm view github-webhook-mcp version は旧版を返しうる(同じく v0.11.9 で実測)。

npm view github-webhook-mcp version --prefer-online

npx のキャッシュ解決挙動そのものは本リポジトリの管理外であり、規定できるのは 「再起動が要る」という事実と上記の確認手段までとする。

依存関係

Cloudflare Worker

パッケージ 用途
agents Cloudflare Agents SDK (createMcpHandler — Worker 向けステートレス MCP ハンドラ)
@modelcontextprotocol/server MCP SDK v2 サーバー(プロトコル版 2026-07-28。agents が非 optional peer として exact 2.0.0 を要求するため exact 固定)
zod スキーマバリデーション

OAuth 実装は自前(worker/src/oauth.ts + worker/src/oauth-store.ts)。@cloudflare/workers-oauth-provider は v0.11.0 で撤去済み。v0.11.1 で Worker-hosted web OAuth に切り替え(device authorization grant は撤去)。

ローカルブリッジ (mcp-server/)

パッケージ 用途
@modelcontextprotocol/sdk MCP SDK v1(Claude Desktop に対するサーバー面。Server クラス直接使用)
@modelcontextprotocol/client MCP SDK v2 クライアント(Worker に対するクライアント面。版 2026-07-28 に pin)
eventsource SSE クライアント

Node.js >= 18.0.0 が必要。

ファイル構成

パス 用途
worker/src/index.ts Cloudflare Worker エントリポイント
worker/src/mcp.ts MCP サーバー factory(ツール定義。リクエストごとに生成、テナントは props から解決)
worker/src/retired-do.ts 退役した WebhookMcpAgent クラス(過去 migration 制約のためだけに残す)
worker/src/store.ts WebhookStore DO(SQLite + SSE、テナント別インスタンス)
worker/src/tenant.ts TenantRegistry DO(installation-account マッピング、クォータ管理)
worker/src/oauth.ts Worker-hosted web OAuth 自前実装(metadata / register / authorize / callback / token / 独自 token 検証 middleware)
worker/src/oauth-store.ts OAuth KV schema helper(client / web_auth_state / token / refresh / grant レコード操作)
worker/wrangler.toml Worker デプロイ設定
shared/src/types.ts 共有型定義
shared/src/summarize.ts イベントサマリー生成
local-mcp/src/index.ts ローカルブリッジ(TypeScript、開発用)
mcp-server/server/index.js ローカルブリッジ(JS、.mcpb 配布用)
mcp-server/manifest.json MCPB マニフェスト
mcp-server/package.json npm パッケージ定義