0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ChatGPT の MCP Events 入門、Webhook で自動処理を起動する

0
Posted at

はじめに

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 の対応も追いついていないため、今後の仕様変更を前提に、イベント処理の層を独立させて実装しておくのが安全です。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?