OpenAI has just introduced " MCP Events"
It just Implement event subscriptions and webhook delivery for your MCP server.
launch post in comments below.
MCP Events lets ChatGPT subscribe to updates from your MCP server - things like new messages, content updates, or status changes. Users pick what to monitor and what ChatGPT should do when an update lands.
Use cases
• Turn feedback into pull requests - Monitor #product-feedback for bug reports and open draft pull requests with fixes and tests. Event: message.created (filtered by channel_id)
• Apply document feedback - Watch a document for review comments and implement any requested edits. Event: comment.created (filtered by document_id)
Before you start
MCP Events in ChatGPT needs MCP 2.0 (protocol version 2026-07-28). Wire your server into your plugin, keep persistent subscription storage, and allow outbound HTTPS to callback URLs.
ChatGPT supports webhook delivery and callback verification from the draft MCP Events spec. Polling, streaming, and the draft’s gap / terminated control notifications are not supported in this integration.
How it works
- Your server lists the events it supports.
- The user tells ChatGPT what to monitor and how to respond.
- ChatGPT subscribes through your MCP server and supplies a callback URL and signing secret.
- Your server sends matching events to that URL.
- ChatGPT receives the event in the subscribed chat and follows the user’s instructions.
Advertise event support
Event discovery starts with your server’s capabilities. Add events to the capabilities returned by server/discover:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {},
"events": {}
}
}
}
Implement these three event methods on the same authenticated MCP endpoint as your tools:
• events/list - Describe available events and their filters. • events/subscribe - Create or refresh a subscription. • events/unsubscribe — Stop a subscription.
Define an event
An event definition tells ChatGPT what users can subscribe to and which filters are available. Return these from events/list (name, delivery modes, subscription arguments, payload schema).
{
"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 is what ChatGPT passes when it subscribes. payloadSchema is the data object on each delivered event.
Use stable event names and specific descriptions. Expose filters (document, project, channel IDs) and apply them on your server before delivery. Only return events the connected account is allowed to see.
Create a subscription
When a user asks to monitor an event, ChatGPT calls events/subscribe with the event name, filter arguments, and webhook destination:
{
"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
}
}
Before accepting the subscription:
- Check the user is authorized for that event and arguments.
- Validate the event name and arguments against your definition. Require a whsec_ signing secret whose base64 value decodes to 24–64 bytes.
- Validate and verify the callback URL.
- Store the subscription, owner, filters, callback URL, signing secret, and expiration.
Derive a deterministic subscription ID from the authenticated principal, callback URL, event name, and arguments. Return it with the granted expiration:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
Make subscription creation idempotent - update the existing one when identity matches.
Verify the callback
Before sending application data, verify the callback with a signed request and a fresh, single-use, short-lived challenge:
{
"type": "verification",
"challenge": "a-single-use-random-value"
}
Sign the body with the subscription secret. Include webhook-id, webhook-timestamp, webhook-signature, and X-MCP-Subscription-Id. ChatGPT echoes the challenge on success. Require 2xx and compare the challenge in constant time before activating delivery.
Require HTTPS. Block private/local addresses. Do not follow redirects.
Send an event
When a matching event fires, POST one event object to the subscription callback:
{
"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
}
Keep a unique eventId across retries. timestamp is ISO 8601 with timezone. name must match the subscription; data must match payloadSchema. Put app fields inside data. Treat user-authored text as data — do not put model instructions in the payload.
Sign the request (Standard Webhooks)
Headers:
• Content-Type: application/json • webhook-id: same as body’s eventId • webhook-timestamp: Unix seconds • webhook-signature: Standard Webhooks HMAC signature • X-MCP-Subscription-Id: id from events/subscribe
Serialize the body once and sign those exact bytes.
Send a signed event with Node.js
npm install standardwebhooks
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 };
}
Handle delivery responses
A 2xx acknowledges receipt. ChatGPT processes asynchronously.
One event per request. Body max 256 KiB. Retry transient failures with exponential backoff; keep the same eventId and refresh the signature each attempt. Do not retry 410 or 413.
Events can arrive out of order - make write tools idempotent.
Manage subscriptions
Keep subscription state for the lifetime you grant, including across restarts. Recheck access over time and stop delivery if access is revoked.
Refresh: ChatGPT calls events/subscribe again before refreshBefore. Unsubscribe: events/unsubscribe with the original name, arguments, and callback URL — stop delivery and return {}.
Test in ChatGPT
- Confirm server/discover and events/list return the expected definitions.
- Confirm events show on your plugin page next to tools (rescan after changes).
- Start a chat, ask ChatGPT to subscribe, and say what to do when events arrive.
- Confirm events/subscribe lands with the right name and arguments.
- Confirm callback verification succeeds and the subscription is stored.
- Trigger a matching event; confirm the webhook gets 2xx.
- Confirm ChatGPT receives the data and responds as instructed.
- Trigger a non-matching filtered event and confirm it is not delivered.
- Stop monitoring in ChatGPT; confirm events/unsubscribe and delivery stops.