MCP Events を使うと、ChatGPT は MCP サーバーから新着メッセージ、コンテンツの更新、ステータスの変更などの通知を購読できます。ユーザーは監視する対象と、更新通知を受け取ったときに ChatGPT が行う処理を指定します。
| ユースケース | ユーザーの依頼 | MCP イベント |
|---|---|---|
| フィードバックをもとに Pull Request を作成 | #product-feedback のバグ報告を監視し、修正とテストを含むドラフトの Pull Request を作成してください。 | message.created を channel_id で絞り込み |
| レビューコメントをドキュメントに反映 | このドキュメントのレビューコメントを監視し、依頼された編集を反映してください。 | comment.created を document_id で絞り込み |
利用前の準備
MCP Events は、Web 版 ChatGPT の Work モードのチャット、デスクトップアプリでクラウドを選択した Work モードのチャット、および dot で利用できます。プラグインとイベントを契機に実行されるタスクには、ワークスペースの管理設定が適用されます。
ChatGPT は、initialize を使う Streamable HTTP と、MCP 2.0 HTTP(プロトコルバージョン 2026-07-28)の両方で MCP Events をサポートしています。プラグインでサーバーを設定し、サブスクリプションを永続化するストレージを用意してください。また、サーバーからコールバック URL に HTTPS で接続できるようにしてください。
ChatGPT は、MCP Events の仕様案に定められた Webhook 配信とコールバック検証に対応しています。この連携では、ポーリング、ストリーミング、および仕様案にある gap と terminated の制御通知はサポートされていません。
仕組み
- サーバーが対応するイベントの一覧を提示します。
- ユーザーが監視対象と対応方法を ChatGPT に伝えます。
- ChatGPT が MCP サーバーにサブスクリプションを登録し、コールバック URL と署名用シークレットを渡します。
- サーバーが条件に一致するイベントをその URL に送信します。
- ChatGPT がサブスクリプションを登録したチャットでイベントを受信し、ユーザーの指示に従って対応します。
イベント対応の宣言
ChatGPT は、サーバーが公開する機能情報をもとに、利用可能なイベントを検出します。初期化を行う MCP バージョンでは、events を initialize レスポンスで返す機能情報に追加します。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {},
"events": {}
},
"serverInfo": {
"name": "example-server",
"version": "1.0.0"
}
}
}
MCP 2.0(プロトコルバージョン 2026-07-28)では、events を server/discover レスポンスで返す機能情報に追加します。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {},
"events": {}
}
}
}
ツールと同じ認証付き MCP エンドポイントに、次の 3 つのイベントメソッドを実装します。
| メソッド | サーバーの動作 |
|---|---|
events/list |
利用可能なイベントと、それぞれに使えるフィルターを返します。 |
events/subscribe |
サブスクリプションを作成または更新します。 |
events/unsubscribe |
サブスクリプションを停止します。 |
イベントの定義
イベント定義は、ユーザーが購読できるイベントと利用可能なフィルターを ChatGPT に伝えます。events/list では、イベント名、対応する配信モード、サブスクリプションの引数、ペイロードのスキーマを含むイベント定義を返します。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"events": [
{
"name": "comment.created",
"description": "A new review comment was added to the specified document.",
"delivery": ["webhook"],
"inputSchema": {
"type": "object",
"properties": {
"document_id": {
"type": "string",
"description": "ID of the document to monitor for new review comments."
}
},
"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
}
}
]
}
}
inputSchema は ChatGPT がサブスクリプションの登録時に渡す引数を定義し、payloadSchema は配信する各イベントの data オブジェクトを定義します。
イベント名は安定したものにし、説明にはイベントの内容を具体的に記載してください。ドキュメント、プロジェクト、キューの ID などで絞り込めるフィルターを用意し、配信前にサーバー側で適用します。接続中のアカウントに検出が許可されているイベントだけを返してください。
カタログが複数ページにわたる場合は nextCursor を返し、その値を cursor として次の events/list リクエストで受け取ります。
サブスクリプションの作成
ユーザーがイベントの監視を依頼すると、ChatGPT は events/subscribe を呼び出し、イベント名、フィルターの引数、Webhook の送信先を渡します。
{
"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 バイトになることを確認します。 - コールバック URL の妥当性を確認し、コールバックを検証します。
- サブスクリプションと、その所有者、フィルター、コールバック URL、署名用シークレット、有効期限を保存します。
認証済みの主体、コールバック URL、イベント名、引数から、同じ入力に対して常に同じサブスクリプション ID を生成します。この ID を、サーバーが設定した有効期限とともに返します。
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
refreshBefore にはサーバーが設定した有効期限を指定します。再配信に対応していないイベント種別では cursor: null を返します。
サブスクリプションの作成は冪等にします。識別情報が一致する場合は、既存のサブスクリプションを更新してください。オブジェクトのキーの順序が違うだけでサブスクリプションが重複しないよう、引数は正規化した JSON で比較します。
コールバックの検証
アプリケーションデータを送信する前に、有効期間の短い使い捨てのチャレンジを新たに生成し、それを含む署名付きリクエストを送信してコールバックを検証します。
{
"type": "verification",
"challenge": "a-single-use-random-value"
}
検証リクエストには一意の webhook-id(例:msg_verification_123)を割り当て、サブスクリプションのシークレットで本文に署名します。webhook-timestamp、webhook-signature、X-MCP-Subscription-Id を含めてください。成功時には、ChatGPT がレスポンスでチャレンジをそのまま返します。
{
"challenge": "a-single-use-random-value"
}
2xx レスポンスを受け取り、返されたチャレンジと送信した値を定数時間で比較して一致を確認してから、配信を開始します。サブスクリプションの登録が繰り返されても毎回チャレンジを送信せずに済むよう、検証に成功した結果を認証済みの主体とコールバック URL の組み合わせごとに、期間を限定してキャッシュします。検証に失敗した場合は、JSON-RPC エラー -32015(CallbackEndpointError)を返し、data.reason に challenge_failed や timeout などの理由コードを設定します。
コールバックには HTTPS を必須とします。接続時に宛先のアドレスを解決して検証し、TLS 検証には元のホスト名を使用したまま、検証済みのアドレスに接続します。プライベートアドレス、ローカルアドレス、その他の非公開アドレスへの接続をブロックし、リダイレクトには従わないでください。これらのチェックは、検証リクエストとイベント配信の両方に適用します。
イベントの送信
新しいコメントが有効なサブスクリプションの条件に一致したら、そのサブスクリプションのコールバック URL にイベントオブジェクトを 1 件、POST で送信します。
{
"eventId": "evt_456",
"name": "comment.created",
"timestamp": "2026-10-01T12:05:00Z",
"data": {
"document_id": "doc_123",
"comment_id": "comment_456",
"text": "Can we add the rollout dates to this section?",
"url": "https://docs.example.com/doc_123#comment_456"
},
"cursor": null
}
イベント ID は一意にし、再試行時にも同じ ID を使用します。timestamp には、イベントの発生時刻をタイムゾーン付きの ISO 8601 形式で設定します。name は購読対象のイベント名と一致し、data オブジェクトはそのイベントの payloadSchema に準拠している必要があります。アプリケーション固有のフィールドは data 内に配置してください。トップレベルの type はプロトコルの制御通知を識別するためのフィールドです。
レコードが大きい場合は要約を送信し、レコード全体を取得できる読み取り用ツールを公開します。コメントなど、ユーザーが書いたテキストはデータとして扱ってください。モデルの動作を指示する文をイベントのペイロードに追加しないでください。
リクエストへの署名
ChatGPT は Standard Webhooks を使って配信を検証します。次のヘッダーを送信してください。
| ヘッダー | 値 |
|---|---|
Content-Type |
application/json |
webhook-id |
本文の eventId と同じ値 |
webhook-timestamp |
署名時刻を秒単位で表した Unix タイムスタンプ |
webhook-signature |
Standard Webhooks の HMAC 署名 |
X-MCP-Subscription-Id |
events/subscribe が返した ID |
配信時には、Standard Webhooks とサブスクリプションの署名用シークレットを使って署名します。署名の対象には、イベント ID、署名時刻、リクエスト本文の正確なバイト列が含まれます。本文は一度だけシリアライズし、署名に使ったバイト列をそのまま送信してください。
Node.js を使った署名付きイベントの送信
Standard Webhooks ライブラリをインストールします。
npm install standardwebhooks
webhookFetch には、fetch と同じインターフェースを持ち、接続のたびにコールバックのアドレスを検証し、リダイレクトをブロックする関数を渡してください。
import { Webhook } from "standardwebhooks";
export async function sendEvent(subscription, event, webhookFetch) {
const body = JSON.stringify(event);
if (Buffer.byteLength(body, "utf8") > 256 * 1024) {
throw new Error("Event payload exceeds 256 KiB");
}
const signedAt = new Date();
const signer = new Webhook(subscription.secret);
const response = await webhookFetch(subscription.url, {
method: "POST",
redirect: "error",
signal: AbortSignal.timeout(10_000),
headers: {
"Content-Type": "application/json",
"webhook-id": event.eventId,
"webhook-timestamp": String(Math.floor(signedAt.getTime() / 1000)),
"webhook-signature": signer.sign(event.eventId, signedAt, body),
"X-MCP-Subscription-Id": subscription.id,
},
body,
});
return { accepted: response.ok, status: response.status };
}subscription.url と subscription.secret には、サブスクリプションの登録リクエストに含まれる delivery オブジェクトの値を設定します。
配信レスポンスの処理
2xx レスポンスは Webhook を受信したことを示します。ChatGPT はイベントを非同期で処理します。
リクエスト 1 件につきイベントを 1 件送信し、リクエスト本文全体を 256 KiB(262,144 バイト)以下に収めてください。ChatGPT はタスクのバッチ設定に応じて、個別に配信された複数のイベントをまとめて 1 回のタスク実行で処理できます。
一時的な失敗には指数バックオフで再試行し、試行回数には上限を設けます。イベント ID は維持し、試行のたびに署名時刻と署名を新たに生成してください。410 または 413 が返った配信は再試行しないでください。
イベントは発生順とは異なる順序で届くことがあります。書き込み用ツールは冪等にし、繰り返し呼び出しても変更が重複して適用されないようにしてください。
サブスクリプションの管理
サーバーが設定した有効期間中は、サーバーを再起動してもサブスクリプションの状態を保持してください。有効期間中もユーザーのアクセス権を再確認し、権限が取り消された場合は配信を停止します。
サブスクリプションの更新
ChatGPT は、有効期限が近づいたサブスクリプションを更新するために、events/subscribe を refreshBefore より前に呼び出します。その際、同じサブスクリプションの識別情報と、最後に保存したカーソルを使用します。既存のサブスクリプションを更新し、新しい有効期限を refreshBefore で返してください。
ttlMs が省略された場合は、サーバーで定めたサブスクリプションのデフォルトの有効期間を使用します。指定された場合、その値は要求された有効期間をミリ秒単位で表します。更新リクエストが過剰に発生するのを防ぐために最短有効期間を適用する場合を除き、指定された期間を超える有効期間は設定しないでください。
ttlMs: null は有効期限のないサブスクリプションを要求します。その要求を受け入れる場合に限り、refreshBefore: null を返します。それ以外の場合は具体的な有効期限を返し、期限を過ぎたら配信を停止してください。
更新リクエストで新しい署名用シークレットが渡された場合は、保存済みのシークレットを置き換えます。鍵の切り替え時には短い移行期間を設け、旧鍵と新鍵の両方で署名し、Standard Webhooks の形式に従って署名をスペース区切りで送信します。
再配信に対応するイベントでは、サブスクリプションの期限切れやサーバーの再起動後に、リクエストの cursor を使って配信を再開します。サブスクリプションのレスポンスとイベントのペイロードには、配信待ちのイベントを飛ばさずに再開できるカーソルを含めてください。要求された履歴がすでに利用できない場合は truncated: true を返します。再配信に対応していないイベント種別では cursor: null を返します。中断中に取り逃したイベントをこのプロトコルで取得することはできません。
サブスクリプションの停止
events/unsubscribe は、登録時のイベント名、引数、コールバック URL を使って処理します。
{
"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"
}
}
}
一致するサブスクリプションへのイベント配信を停止し、空の結果を返します。
{
"jsonrpc": "2.0",
"id": 3,
"result": {}
}
登録解除の処理は冪等にし、接続中のアカウントにそのサブスクリプションを解除する権限があることを確認してください。
ChatGPT でのテスト
イベントメソッドと Webhook 配信を実装したら、プラグインを通じて MCP サーバーを ChatGPT に接続し、サブスクリプションのライフサイクル全体をテストします。
-
サーバーが
eventsへの対応をinitializeまたはserver/discoverで宣言し、その後events/listを受け取って期待どおりのイベント定義を返すことを確認します。 -
プラグインのページにツールと並んでイベントが表示されることを確認します。ツールやイベントを変更するたびに MCP サーバーを再スキャンしてください。
プラグインのイベント。 検出されたイベントは、プラグインのページにツールと並んで表示されます。
-
Web 版 ChatGPT で Work モードのチャットを開始するか、デスクトップアプリで Work とクラウドを選択します。ChatGPT にいずれかのイベントの購読を依頼し、イベントが届いたときの対応を指定してください。dot でもテストできます。
-
サーバーが
events/subscribeを受け取り、イベント名と引数が期待どおりであることを確認します。 -
コールバックの検証に成功し、サブスクリプションが保存されることを確認します。
-
アプリで条件に一致するイベントを発生させ、Webhook 配信に対して
2xxレスポンスが返ることを確認します。 -
ChatGPT が期待どおりのイベントデータを受け取り、指示どおりに対応することを確認します。
-
フィルターを設定したサブスクリプションでは、フィルターに一致しないイベントを発生させ、そのサブスクリプションに配信されないことを確認します。
-
ChatGPT で監視を停止します。サーバーが
events/unsubscribeを処理し、配信を停止することを確認します。
サブスクリプションの登録リクエストを繰り返した場合や、サーバーの再起動をまたいで期限切れと更新が発生する場合もテストしてください。アカウントの接続解除、購読対象のリソースへのアクセス権の取り消し、無効な署名、重複配信もテストします。また、バッチ処理が有効な場合と無効な場合のそれぞれで、イベントが短時間に集中したときの動作を確認してください。依頼された処理が元のアプリのデータを変更する場合は、その変更で発生するイベントによってフィードバックループが生じないことを確認します。
プロトコルの参考資料:MCP Events の設計案。