{"slug": "mcp-events", "title": "MCP Events", "summary": "ChatGPT now supports MCP Events, letting users subscribe to updates from an MCP server such as new messages, content changes, or status changes, according to OpenAI's documentation. The integration requires MCP 2.0 (protocol version 2026-07-28), persistent subscription storage, and outbound HTTPS access to callback URLs, and supports webhook delivery and callback verification from the draft MCP Events specification. Servers must advertise an `events` capability in their `server/discover` response and implement `events/list`, `events/subscribe`, and `events/unsubscribe`; polling, streaming, and the draft's `gap` and `terminated` control notifications are not supported.", "body_md": "MCP Events lets ChatGPT subscribe to updates from your MCP server, such as new messages, content updates, or status changes. Users choose what to monitor and what ChatGPT should do when an update arrives.\n\n| Use case | User request | MCP event | \n|---|---|---|\n| Turn feedback into pull requests | Monitor #product-feedback for bug reports and open draft pull requests with fixes and tests. | `message.created` , filtered by`channel_id` | \n| Apply document feedback | Watch this document for review comments and implement any requested edits. | `comment.created` , filtered by`document_id` | \n\n## Before you start\n\nMCP Events in ChatGPT requires MCP 2.0 (protocol version `2026-07-28`). Configure your server in your plugin and provide persistent subscription storage and outbound HTTPS access to callback URLs.\n\nChatGPT supports webhook delivery and callback verification from the [draft MCP Events specification](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md). Polling, streaming, and the draft’s `gap` and `terminated` control notifications are not supported by this integration.\n\n## How it works\n\n1. Your server lists the events it supports.\n2. The user tells ChatGPT what to monitor and how to respond.\n3. ChatGPT subscribes through your MCP server and supplies a callback URL and signing secret.\n4. Your server sends matching events to that URL.\n5. ChatGPT receives the event in the subscribed chat and follows the user’s instructions for how to respond.\n\n## Advertise event support\n\nEvent discovery starts with your server’s capabilities. Add `events` to the capabilities returned by its `server/discover` response:\n\n```\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"result\": {\n    \"resultType\": \"complete\",\n    \"supportedVersions\": [\"2026-07-28\"],\n    \"capabilities\": {\n      \"tools\": {},\n      \"events\": {}\n    }\n  }\n}\n```\n\nImplement these three event methods on the same authenticated MCP endpoint as your tools:\n\n| Method | Server behavior | \n|---|---|\n| `events/list` | Describe available events and their filters. | \n| `events/subscribe` | Create or refresh a subscription. | \n| `events/unsubscribe` | Stop a subscription. | \n\n## Define an event\n\nAn event definition tells ChatGPT what users can subscribe to and which filters are available. Return these definitions from `events/list`, including the event name, supported delivery modes, subscription arguments, and payload schema.\n\n```\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"result\": {\n    \"events\": [\n      {\n        \"name\": \"comment.created\",\n        \"description\": \"A new review comment was added to the specified document.\",\n        \"delivery\": [\"webhook\"],\n        \"inputSchema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"document_id\": {\n              \"type\": \"string\",\n              \"description\": \"ID of the document to monitor for new review comments.\"\n            }\n          },\n          \"required\": [\"document_id\"],\n          \"additionalProperties\": false\n        },\n        \"payloadSchema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"document_id\": { \"type\": \"string\" },\n            \"comment_id\": { \"type\": \"string\" },\n            \"text\": { \"type\": \"string\" },\n            \"url\": { \"type\": \"string\" }\n          },\n          \"required\": [\"document_id\", \"comment_id\", \"text\", \"url\"],\n          \"additionalProperties\": false\n        }\n      }\n    ]\n  }\n}\n```\n\n`inputSchema` describes the arguments passed when ChatGPT subscribes, while `payloadSchema` describes the `data` object in each delivered event.\n\nUse stable event names and specific descriptions. Expose filters such as document, project, or queue IDs, and apply them on your server before delivery. Return only events the connected account is allowed to discover.\n\nIf the catalog spans multiple pages, return `nextCursor` and accept it as `cursor` on the next `events/list` request.\n\n## Create a subscription\n\nWhen a user asks to monitor an event, ChatGPT calls `events/subscribe` with the event name, filter arguments, and webhook destination:\n\n```\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 2,\n  \"method\": \"events/subscribe\",\n  \"params\": {\n    \"name\": \"comment.created\",\n    \"arguments\": {\n      \"document_id\": \"doc_123\"\n    },\n    \"delivery\": {\n      \"mode\": \"webhook\",\n      \"url\": \"https://receiver.example.com/mcp-events/callback_123\",\n      \"secret\": \"whsec_<base64-encoded-signing-key>\"\n    },\n    \"cursor\": null\n  }\n}\n```\n\nBefore accepting the subscription:\n\n1. Check that the user is authorized for the requested event and arguments.\n2. Validate the event name and arguments against your event definition. Require a `whsec_` signing secret whose base64 value decodes to 24–64 bytes.\n3. [Validate and verify the callback URL](#verify-the-callback) .\n4. Store the subscription, its owner, filters, callback URL, signing secret, and expiration.\n\nDerive a deterministic subscription ID from the authenticated principal, callback URL, event name, and arguments. Return it with the granted expiration:\n\n```\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 2,\n  \"result\": {\n    \"id\": \"sub_123\",\n    \"refreshBefore\": \"2026-10-02T12:00:00Z\",\n    \"cursor\": null,\n    \"truncated\": false\n  }\n}\n```\n\nSet `refreshBefore` to the expiration your server grants. Return `cursor: null` for event types that do not support replay.\n\nMake subscription creation idempotent: update the existing subscription when its identity matches. Compare arguments using canonical JSON so object key order does not create duplicate subscriptions.\n\n### Verify the callback\n\nBefore sending application data, verify the callback by sending a signed request with a fresh, single-use, short-lived challenge:\n\n```\n{\n  \"type\": \"verification\",\n  \"challenge\": \"a-single-use-random-value\"\n}\n```\n\nAssign the verification request a unique `webhook-id`, such as `msg_verification_123`, and sign its body with the subscription’s secret. Include `webhook-timestamp`, `webhook-signature`, and `X-MCP-Subscription-Id`. ChatGPT echoes the challenge in a successful response:\n\n```\n{\n  \"challenge\": \"a-single-use-random-value\"\n}\n```\n\nRequire a `2xx` response and compare the returned challenge in constant time before activating delivery. Cache successful verification by authenticated principal and callback URL for a bounded period so repeated subscription requests do not trigger repeated challenges. If verification fails, return JSON-RPC error `-32015` (`CallbackEndpointError`) with a categorized `data.reason`, such as `challenge_failed` or `timeout`.\n\nRequire HTTPS for callbacks. Resolve and validate destination addresses at connection time, then connect to the validated address while preserving the original hostname for TLS verification. Block private, local, and other non-public addresses, and do not follow redirects. Apply these checks to verification requests as well as event deliveries.\n\n## Send an event\n\nWhen a new comment matches an active subscription, POST one event object to that subscription’s callback URL:\n\n```\n{\n  \"eventId\": \"evt_456\",\n  \"name\": \"comment.created\",\n  \"timestamp\": \"2026-10-01T12:05:00Z\",\n  \"data\": {\n    \"document_id\": \"doc_123\",\n    \"comment_id\": \"comment_456\",\n    \"text\": \"Can we add the rollout dates to this section?\",\n    \"url\": \"https://docs.example.com/doc_123#comment_456\"\n  },\n  \"cursor\": null\n}\n```\n\nUse a unique event ID and preserve it across retries. Set `timestamp` to the event’s occurrence time as an ISO 8601 timestamp with a timezone. The `name` must match the subscribed event, and the `data` object must match its `payloadSchema`. Keep application fields inside `data`; a top-level `type` identifies a protocol control notification.\n\nFor large records, send a summary and expose a read tool to retrieve the full record. Treat comments and other user-authored text as data; do not add instructions telling the model how to behave inside the event payload.\n\n### Sign the request\n\nChatGPT verifies deliveries using Standard Webhooks. Send these headers:\n\n| Header | Value | \n|---|---|\n| `Content-Type` | `application/json` | \n| `webhook-id` | The same value as the body’s `eventId` . | \n| `webhook-timestamp` | The signing time as Unix seconds. | \n| `webhook-signature` | The Standard Webhooks HMAC signature. | \n| `X-MCP-Subscription-Id` | The ID returned by `events/subscribe` . | \n\nSign deliveries using Standard Webhooks and the subscription’s signing secret. Because the signature covers the event ID, signing timestamp, and exact request body bytes, serialize the body once and send those same bytes.\n\n### Send a signed event with Node.js\n\nInstall the [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries/javascript):\n\n```\nnpm install standardwebhooks\n```\n\nPass a `webhookFetch` function with the same interface as `fetch` that validates callback addresses on each connection and blocks redirects.\n\n``` js\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26import { Webhook } from \"standardwebhooks\";\n\nexport async function sendEvent(subscription, event, webhookFetch) {\n  const body = JSON.stringify(event);\n  if (Buffer.byteLength(body, \"utf8\") > 256 * 1024) {\n    throw new Error(\"Event payload exceeds 256 KiB\");\n  }\n\n  const signedAt = new Date();\n  const signer = new Webhook(subscription.secret);\n  const response = await webhookFetch(subscription.url, {\n    method: \"POST\",\n    redirect: \"error\",\n    signal: AbortSignal.timeout(10_000),\n    headers: {\n      \"Content-Type\": \"application/json\",\n      \"webhook-id\": event.eventId,\n      \"webhook-timestamp\": String(Math.floor(signedAt.getTime() / 1000)),\n      \"webhook-signature\": signer.sign(event.eventId, signedAt, body),\n      \"X-MCP-Subscription-Id\": subscription.id,\n    },\n    body,\n  });\n\n  return { accepted: response.ok, status: response.status };\n}\n```\n\nSet `subscription.url` and `subscription.secret` from the subscription request’s `delivery` object.\n\n### Handle delivery responses\n\nA `2xx` response acknowledges webhook receipt. ChatGPT processes the event asynchronously.\n\nSend one event per request, with a complete request body no larger than 256 KiB (262,144 bytes). ChatGPT can group separately delivered events into one task run according to the task’s batching settings.\n\nRetry transient failures with exponential backoff and bounded attempts. Preserve the event ID and generate a fresh signing timestamp and signature for each attempt. Do not retry deliveries that return `410` or `413`.\n\nEvents can arrive out of order. Make write tools idempotent so repeated calls do not duplicate changes.\n\n## Manage subscriptions\n\nRetain subscription state for the lifetime you grant, including across server restarts. Recheck the user’s access during the subscription’s lifetime and stop delivery if access is revoked.\n\n### Refresh a subscription\n\nChatGPT refreshes expiring subscriptions by calling `events/subscribe` before `refreshBefore`, using the same subscription identity and the last saved cursor. Update the existing subscription and return its new expiration in `refreshBefore`.\n\nWhen `ttlMs` is omitted, use your server’s default subscription lifetime. When provided, it specifies the requested lifetime in milliseconds. Grant no more than that duration, except when enforcing a minimum lifetime to prevent excessive refresh requests.\n\n`ttlMs: null` requests a subscription without expiration. Return `refreshBefore: null` only when granting that request. Otherwise, return a finite expiration and stop delivery when it passes.\n\nWhen a refresh supplies a replacement signing secret, replace the stored secret. During a short rotation window, sign with both the old and new keys using Standard Webhooks’ space-separated signatures.\n\nFor replayable events, use the request’s `cursor` to resume after an expired subscription or server restart. In subscription responses and event payloads, return a cursor that does not skip events still awaiting delivery. Return `truncated: true` when the requested history is no longer available. For event types without replay, return `cursor: null`; events missed during an interruption cannot be recovered through the protocol.\n\n### Stop a subscription\n\nHandle `events/unsubscribe` using the original event name, arguments, and callback URL:\n\n```\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 3,\n  \"method\": \"events/unsubscribe\",\n  \"params\": {\n    \"name\": \"comment.created\",\n    \"arguments\": {\n      \"document_id\": \"doc_123\"\n    },\n    \"delivery\": {\n      \"mode\": \"webhook\",\n      \"url\": \"https://receiver.example.com/mcp-events/callback_123\"\n    }\n  }\n}\n```\n\nStop sending events for the matching subscription and return an empty result:\n\n```\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 3,\n  \"result\": {}\n}\n```\n\nMake unsubscribe idempotent and authorize it against the connected account.\n\n## Test in ChatGPT\n\nWith the event methods and webhook delivery in place, connect your MCP server to ChatGPT through a plugin and test the full subscription lifecycle:\n\n1. \nConfirm your server receives `server/discover` and`events/list` , and returns the expected event definitions.\n2. \nVerify that your events appear on your plugin page alongside your tools. Rescan your MCP server whenever you change its tools or events. ***Plugin events.****Discovered events appear alongside tools on the plugin page.*\n3. \nStart a new chat, ask ChatGPT to subscribe to one of your events, and specify what it should do when an event arrives.\n4. \nConfirm your server receives `events/subscribe` with the expected event name and arguments.\n5. \nCheck that callback verification succeeds and the subscription is stored.\n6. \nTrigger a matching event in your app and confirm that the webhook delivery receives a `2xx` response.\n7. \nVerify that ChatGPT receives the expected event data and responds as instructed.\n8. \nFor filtered subscriptions, trigger an event that does not match the filters and confirm it is not delivered to the subscription.\n9. \nStop monitoring in ChatGPT. Confirm that your server processes `events/unsubscribe` and stops delivery.\n\nAlso test repeated subscription requests, expiration and refresh across a server restart, account disconnection, revoked access to a subscribed resource, invalid signatures, duplicate deliveries, and bursts with batching enabled and disabled. If the requested action changes data in the source app, verify that the resulting events do not create a feedback loop.\n\nProtocol reference: [MCP Events design sketch](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md).", "url": "https://wpnews.pro/news/mcp-events", "canonical_source": "https://developers.openai.com/plugins/build/mcp-events", "published_at": "2026-09-29 18:20:15+00:00", "updated_at": "2026-09-29 18:48:11.794410+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "ai-products", "developer-tools"], "entities": ["ChatGPT", "OpenAI", "Model Context Protocol", "MCP Events", "MCP 2.0"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/mcp-events", "markdown": "https://wpnews.pro/news/mcp-events.md", "text": "https://wpnews.pro/news/mcp-events.txt", "jsonld": "https://wpnews.pro/news/mcp-events.jsonld"}}