はじめに
OpenAI は 2026年9月29日の DevDay にあわせて、ChatGPT のプラグインで MCP Events を使えるようにしました(OpenAI 開発者ドキュメント: MCP Events)。MCP Events は、MCP サーバーが接続先アプリの変化を ChatGPT へ プッシュで 知らせるための仕組みです。これまで MCP は ChatGPT 側からツールを呼んだときにしか動けませんでしたが、MCP Events を使うと「ドキュメントにレビューコメントが付いた」「プロジェクトボードに新しいタスクが入った」といった出来事をきっかけに、ChatGPT がユーザー不在でも処理を始められます。
対象読者は、ChatGPT 向けの MCP サーバー(プラグイン)をすでに作っているか作ろうとしていて、「MCP Events を実装するには何が要るのか」を知りたい方です。公式ドキュメントと MCP の設計ドラフトを整理したうえで、配信の要となる callback 検証と Standard Webhooks 署名 を Node.js で実際に動かした結果を載せます。ChatGPT 本体への接続は試していないため、その部分は公式ドキュメントの記述に基づく説明です。
MCP Events とは何か
MCP Events は、MCP の Triggers and Events Working Group が策定中の拡張です。同 WG の憲章は 2026年3月24日に作られ、AWS の Clare Liguori 氏と Anthropic の Peter Alexander 氏がリードを務めています(Triggers and Events Charter)。WG の目的は「クライアントがポーリングや SSE 接続の維持でサーバー側の更新を知る」現状を、標準化されたコールバックの仕組みに置き換えることです。
設計の原案は experimental-ext-triggers-events の design sketch にあり、ステータスは Draft proposal です。原案は配信方式を3つ定義しています。
| 方式 | メソッド | 仕組み |
|---|---|---|
| ポーリング | events/poll |
クライアントが cursor を付けて定期的に問い合わせる |
| ストリーム | events/stream |
長時間接続で notifications/events/event を受け取る |
| Webhook | events/subscribe |
サーバーがクライアント指定の URL へ署名つき POST を送る |
ChatGPT が対応したのは、このうち Webhook 方式とその callback 検証だけ です。公式ドキュメントは、ポーリング・ストリーム、そして原案にある gap(取りこぼし通知)と terminated(購読終了通知)の制御メッセージには対応していないと明記しています。
利用条件
公式ドキュメントに書かれている条件は次のとおりです。
- 使える場所: ChatGPT Web の Work チャット、デスクトップアプリで Cloud を選んだ Work チャット、dots(常時稼働エージェント)
- プロトコル: MCP 2.0(プロトコルバージョン
2026-07-28)が必須 - サーバー側に必要なもの: 購読情報を永続化するストレージ、callback URL へ出ていける HTTPS の外向き通信、認証つきの MCP エンドポイント
- ワークスペースのプラグイン管理設定がそのまま適用される
2026-07-28 はステートレスなプロトコルコアや拡張フレームワークを導入した版で、MCP Events はこの拡張フレームワークの上に載る形です(The 2026-07-28 Specification)。
全体の流れ
ユーザーが「このドキュメントにコメントが付いたら対応して」と ChatGPT に頼んだときの流れを図にします。
ポイントは、ChatGPT がイベントを受け取る場所が「購読したチャット」だという点です。イベントの中身をどう扱うかはユーザーが最初に与えた指示で決まり、サーバーはイベントを届けることに専念します。
サーバーが実装する3つのメソッド
認証済みの MCP エンドポイントに、次の3メソッドを実装します。あわせて server/discover の capabilities に events を追加します。
{
"capabilities": {
"tools": {},
"events": {}
}
}
events/list: イベントの一覧を返す
イベント定義は、購読時の絞り込み条件を表す inputSchema と、配信する data の形を表す payloadSchema を持ちます。公式ドキュメントの例を引用します。
{
"name": "comment.created",
"description": "A new review comment was added to the specified document.",
"delivery": ["webhook"],
"inputSchema": {
"type": "object",
"properties": { "document_id": { "type": "string" } },
"required": ["document_id"],
"additionalProperties": false
},
"payloadSchema": {
"type": "object",
"properties": {
"document_id": { "type": "string" },
"comment_id": { "type": "string" },
"text": { "type": "string" },
"url": { "type": "string" }
},
"required": ["document_id", "comment_id", "text", "url"],
"additionalProperties": false
}
}
イベント名は安定したもの(comment.created のような形)にし、description は具体的に書くよう推奨されています。カタログが大きい場合は nextCursor でページングします。返してよいのは、接続しているアカウントが知ってよいイベントだけです。
events/subscribe: 購読を作る・更新する
ChatGPT からは次のようなリクエストが届きます。
{
"jsonrpc": "2.0",
"id": 2,
"method": "events/subscribe",
"params": {
"name": "comment.created",
"arguments": { "document_id": "doc_123" },
"delivery": {
"mode": "webhook",
"url": "https://receiver.example.com/mcp-events/callback_123",
"secret": "whsec_<base64-encoded-signing-key>"
},
"cursor": null
}
}
シークレットは whsec_ 接頭辞つきで、base64 部分をデコードすると 24〜64 バイトになります。サーバーは購読 ID と有効期限を返します。
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
実装上の要点は次の3つです。
- 購読 ID は決定論的に作る: 認証主体・callback URL・イベント名・引数から導出し、同じ組み合わせなら既存の購読を更新する(引数は正規化した JSON で比較する)
-
有効期限を返す:
refreshBeforeを過ぎたら配信を止める。ChatGPT はその前に同じ内容でevents/subscribeを呼び直す。ttlMsで寿命が要求されたら、それを超えない範囲で付与する -
cursor で再開できるようにする: 再生できるイベントなら、受け取った cursor の続きから配信する。履歴が残っていなければ
truncated: trueを返す。再生できないイベントはcursor: nullを返す
再購読でシークレットが差し替えられた場合は、切り替え期間中は新旧両方の鍵で署名し、署名をスペース区切りで並べます(Standard Webhooks の複数署名)。
events/unsubscribe: 購読を止める
{
"jsonrpc": "2.0",
"id": 3,
"method": "events/unsubscribe",
"params": {
"name": "comment.created",
"arguments": { "document_id": "doc_123" },
"delivery": {
"mode": "webhook",
"url": "https://receiver.example.com/mcp-events/callback_123"
}
}
}
レスポンスは空の result です。何度呼ばれても同じ結果になるようにし、接続アカウントで認可を確認します。
配信の仕組みを手元で動かした結果
MCP Events の配信は Standard Webhooks の署名方式に乗っています。この部分は ChatGPT がなくても検証できるので、npm の standardwebhooks 1.1.1 と Node.js v22.22.0 で、受信側(ChatGPT 役)と配信側(MCP サーバー役)を1プロセスで動かしました。
配信側のコード
公式ドキュメントのサンプルに沿って、送信関数を次のように書きました。本文は 1回だけシリアライズして同じバイト列を送る のが重要です。署名は eventId・タイムスタンプ・本文のバイト列をまとめて対象にするため、送信直前に JSON を作り直すと検証に失敗します。
import { Webhook } from "standardwebhooks";
async function deliver(sub, bodyObj, id) {
const body = JSON.stringify(bodyObj); // 1回だけシリアライズする
const at = new Date();
const res = await fetch(sub.url, {
method: "POST",
redirect: "error", // リダイレクトは追わない
signal: AbortSignal.timeout(10_000),
headers: {
"Content-Type": "application/json",
"webhook-id": id,
"webhook-timestamp": String(Math.floor(at.getTime() / 1000)),
"webhook-signature": new Webhook(sub.secret).sign(id, at, body),
"X-MCP-Subscription-Id": sub.id,
},
body,
});
return res.status;
}
購読 ID は、キーを並べ替えた JSON を SHA-256 にかけて作りました。
function canonical(v) {
if (Array.isArray(v)) return `[${v.map(canonical).join(",")}]`;
if (v && typeof v === "object")
return `{${Object.keys(v).sort()
.map((k) => JSON.stringify(k) + ":" + canonical(v[k])).join(",")}}`;
return JSON.stringify(v);
}
function subscriptionId(principal, url, name, args) {
const h = crypto.createHash("sha256")
.update(canonical({ principal, url, name, args })).digest("hex");
return "sub_" + h.slice(0, 16);
}
受信側は new Webhook(secret).verify(body, headers) で署名を検証し、type: "verification" なら challenge をそのまま返し、それ以外のイベントには 202 を返すようにしました。
実行結果
callback 検証、正常配信、改ざん、別シークレット、サイズ超過の5ケースを流した出力です。
subscriptionId: sub_e84d5638d789fdcf idempotent: true
verification: 200 challenge matched: true
deliver ok: 202
tampered body: 401
wrong secret: 401
payload bytes: 300222 over 256KiB: true
received events: 1 comment.created sub_e84d5638d789fdcf
同じ引数から同じ購読 ID が出ること、challenge の往復が成立すること、本文を1文字変えただけ・鍵が違うだけで受信側が拒否することを確認できました。署名ヘッダーの値は v1,CWnd13Xc... のように v1, で始まる HMAC-SHA256 の base64 で、design sketch に書かれた形式と一致しています。
続けて、受信側のタイムスタンプ許容幅とシークレット切り替え時の複数署名を確かめました。
60s ago: OK
299s ago: OK
301s ago: NG Message timestamp too old
600s ago: NG Message timestamp too old
rotation multi-sig: OK
standardwebhooks の検証は、タイムスタンプが5分を超えてずれると拒否します。公式ドキュメントが「再送のたびに eventId は保ったまま、タイムスタンプと署名は作り直す」と指示しているのはこのためです。最初の署名を使い回して再送すると、指数バックオフで間隔が開いた時点で受信側に弾かれます。旧鍵と新鍵の署名をスペースで並べた場合は、どちらか一方が合えば通りました。
実装で踏みやすい落とし穴
公式ドキュメントと、ChatGPT と実際に接続したコミュニティの報告から、つまずきやすい点をまとめます。
| 項目 | 公式の指示・報告内容 |
|---|---|
| ペイロードサイズ | 1イベントあたり最大 256 KiB(262,144 バイト)。1リクエスト1イベント |
| 応答コード | 2xx で受領扱い。ChatGPT 側の処理は非同期 |
| 再送 | 一時的な失敗は指数バックオフで回数を区切って再送。410 と 413 は再送しない |
| 到着順 | 順不同で届きうる。書き込み系ツールは冪等にする |
| 大きなレコード | 要約だけ送り、全文は読み取りツールで取らせる |
| 権限の取り消し | 購読期間中も権限を再確認し、失効したら配信を止める |
| callback URL | HTTPS 必須。接続時に宛先アドレスを解決して検証し、プライベート・ローカル宛ては拒否。リダイレクトは追わない |
| SDK 対応 | 2026年10月初旬時点で公式 Python SDK(mcp 2.2.0)と TypeScript SDK は未対応との報告あり(OpenAI Developer Community) |
SDK 未対応への回避策として、上記の報告では次の3点が挙げられています。低レベルサーバーの add_request_handler で events/* を独自メソッドとして登録できること、2026-07-28 のトランスポートでは本文と一致する Mcp-Method ヘッダーがないとエラー -32020 で拒否されること、SDK が未知の capability キーを落とすため "events": {} を後段で差し込む必要があることです。
イベント本文はデータとして扱う
公式ドキュメントは「コメントなどユーザーが書いたテキストはデータとして扱い、イベントのペイロードにモデルへの指示を入れない」と書いています。design sketch も、クライアントはイベントデータをツール結果と同じく信頼できない入力として扱い、サーバーはペイロードを最小化して注入の余地を減らすよう求めています。
外部の第三者がコメントを書けるアプリでは、そのコメントがそのまま ChatGPT に届きます。読み取り専用の要約にとどめる、イベントを起点に動く書き込み系ツールの範囲を絞る、といった設計がサーバー側でも必要になります。ChatGPT からの書き込みがまたイベントを生む フィードバックループ を防ぐことも、公式のテスト項目に含まれています。
従来の方法との比較
| 観点 | ツール呼び出しのみ(従来) | ポーリング | MCP Events(Webhook) |
|---|---|---|---|
| 起点 | ユーザーの発話 | クライアントの定期実行 | 接続先アプリの出来事 |
| 遅延 | ユーザーが聞くまで気づかない | 間隔に依存 | 発生直後に配信 |
| サーバーの負担 | 小さい | 空振りの問い合わせが増える | 購読の保存・署名・再送が必要 |
| ChatGPT での対応 | 対応 | 非対応 | 対応(MCP 2.0 必須) |
既存の notifications/resources/updated などの通知は、クライアントが接続を保っている間にしか届きません。MCP Events は購読を永続化し、接続していない間にも外部 URL へ届けられる点が異なります。その代わりサーバー側に、購読ストア・署名鍵の管理・再送キューといった Webhook 配信基盤の責務が乗ります。
まとめ
ChatGPT の MCP Events 対応で、MCP サーバーは「呼ばれたら答える」立場から「出来事を知らせて ChatGPT を動かす」立場に広がりました。実装に必要なのは events/list・events/subscribe・events/unsubscribe の3メソッドと、Standard Webhooks で署名した配信です。署名と検証は既存の standardwebhooks ライブラリで手元でも確かめられるので、まず受信側のモックを作って callback 検証と再送を詰めておくと、ChatGPT との接続時に原因を切り分けやすくなります。
仕様自体は Working Group のドラフト段階で、ChatGPT が対応しているのはそのうち Webhook 方式だけです。公式 SDK の対応も追いついていないため、今後の仕様変更を前提に、イベント処理の層を独立させて実装しておくのが安全です。