-
-
Notifications
You must be signed in to change notification settings - Fork 0
0 requirements.ja
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 |
+-----------------------------+
システムは四つのコンポーネントで構成される:
- Cloudflare Worker — Webhook 受信、署名検証、テナントルーティング
- TenantRegistry Durable Object — installation_id → account_id マッピング管理、テナント単位クォータ管理(単一インスタンス)
-
WebhookStore Durable Object — SQLite によるイベント永続化、REST/SSE エンドポイント(テナント別インスタンス:
store-{accountId}) -
ステートレス MCP ハンドラ — MCP Streamable HTTP サーバー、ツール定義。プロトコル版 2026-07-28 でセッションが消えたため Durable Object ではなく、リクエストごとに
createMcpHandlerが組み立てる(#249)
ローカルブリッジ(mcp-server/)は Worker に対するプロキシであり、データを保持しない。
Worker は MCP プロトコル版 2026-07-28(ステートレスコア)のみを提供する。createMcpHandler に legacy: "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)。版を固定している外部利用者は再起動しても復旧しない。この層の切り捨てを受け入れるというのが本決定である。
| 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 |
| 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_at が PURGE_AFTER_DAYS 日(既定 3)より古い処理済みイベントと、(b) processed=0 かつ received_at が UNPROCESSED_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
}ステートレス 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/..."
}| ID | 要件 |
|---|---|
| F4.1 |
GET /events で SSE ストリームを提供する |
| F4.2 | Webhook ingest 時に接続中の全 SSE クライアントにイベントサマリーをブロードキャストする |
| F4.3 | 30 秒間隔でハートビートを送信する |
| F4.4 | クライアント切断時にクリーンアップする |
| 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 は提供しない |
| ステップ | 操作 |
|---|---|
| 1 |
get_pending_status() を 60 秒間隔でポーリング |
| 2 |
pending_count > 0 なら list_pending_events() でサマリー取得 |
| 3 | フルペイロードが必要なイベントのみ get_event(event_id) で取得 |
| 4 | 処理完了後 mark_processed でマーク。複数件をまとめて処理した場合は event_ids で 1 呼び出しに畳む |
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_state を approved に遷移させる。ユーザにはタブを閉じるよう案内する HTML を返す |
| F7.5 |
POST /oauth/token で grant_type=urn:ietf:params:oauth:grant-type:web_authorization_poll を処理する。pending → 400 authorization_pending、approved → 200 で bearer pair を返し state レコードを消費、denied → 400 access_denied、期限切れ → 400 expired_token(RFC 8628 §3.5 のエラー形式を再利用) |
| F7.6 |
POST /oauth/token で grant_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 例示)
| 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 課金を削減) |
| 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 日)より長いのは未読データ消失の安全域(非対称は意図的) |
| 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 から読む |
| トリガー | ジョブ | 内容 |
|---|---|---|
| PR to main / push to main | test | Node.js syntax check |
| トリガー | ジョブ | 内容 |
|---|---|---|
| release published | build-mcpb |
mcpb pack で .mcpb 生成 |
| release published | attach-mcpb |
gh release upload で .mcpb をリリースに添付(build-mcpb 後) |
| release published | npm-publish | npm レジストリに公開 |
リリースフロー:
- AI が
gh release createでリリースを作成する(PAT 経由で release イベントが発火する) - Release published イベントで CD ワークフローが発火: .mcpb 生成 → .mcpb リリース添付 → npm publish
- npm publish 時にリリースタグ名から自動でバージョンを同期する(package.json の手動更新不要)
- プレリリースタグ(
-を含む)はnextdist-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-onlinenpx のキャッシュ解決挙動そのものは本リポジトリの管理外であり、規定できるのは 「再起動が要る」という事実と上記の確認手段までとする。
| パッケージ | 用途 |
|---|---|
| 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 は撤去)。
| パッケージ | 用途 |
|---|---|
| @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 パッケージ定義 |
GitHub | npm | Discussions