How to Use MCP Events in ChatGPT: Build a Webhook-Powered Server That Pushes Updates
Introduction
Most agents wake up for one of two reasons: a schedule fires, or a human types a message. MCP Events adds a third trigger. Your MCP server pushes a signed webhook to ChatGPT the instant something changes in your app, and ChatGPT carries out the user’s standing instruction without being asked. This tutorial walks you through building a minimal webhook-emitting MCP server that ChatGPT can subscribe to, from capability advertisement through callback verification to signed delivery.
Quick answer
To use MCP events in ChatGPT, expose an MCP 2.0 (protocol 2026-07-28) server that advertises an events capability, implements events/list, events/subscribe, and events/unsubscribe, verifies the HTTPS callback ChatGPT provides, and pushes Standard-Webhooks-signed event webhooks (up to 256 KiB).
Evidence
OpenAI introduced this capability at DevDay on September 29, 2026. Since that announcement, community testing reports that ChatGPT supports the proposed MCP Events specification at protocol version 2026-07-28 (MCP 2.0) on all plans, which makes this a rare automation primitive that does not sit behind a paid tier gate.
The requirements are strict and explicit. OpenAI’s documentation states: “MCP Events in ChatGPT requires MCP 2.0 (protocol version 2026-07-28),” and adds that the server must “provide persistent subscription storage and outbound HTTPS access to callback URLs.” The delivery rules are equally tight: one event per request, with a body no larger than 256 KiB, signed with Standard Webhooks; callbacks must be HTTPS, redirects are not followed, and private, local, and other non-public addresses are blocked for verification requests as well as deliveries. OpenAI is unusually candid about the spec’s status: “ChatGPT supports webhook delivery and callback verification from the draft MCP Events specification. Polling, streaming, and the draft’s gap and terminated control notifications are not supported by this integration.”
Reception from product builders has been warm. On October 3, 2026, Nathan Baschez, who works on product design at Notion, wrote that he had not seen near enough excitement about the documentation, adding: “Event-driven triggers are a huge deal.” Independent end-to-end verification exists too: on October 2, 2026, a community developer published a Python package (mcp-webhook-events) implementing the three events methods, tested it against ChatGPT with a small company-watching demo, and reported that after refreshing tools the event appeared next to the tools on the plugin page, the callback verified, the test event got a 200, and a few seconds later ChatGPT posted in the chat about it without being asked.
To read the authoritative details as you build, keep OpenAI’s MCP Events plugin guide open alongside the Standard Webhooks specification that defines the signing scheme every delivery must follow.
Prerequisites
- A publicly reachable HTTPS endpoint for your MCP server. ChatGPT’s callback URLs are HTTPS-only, and your server needs outbound HTTPS to reach them.
- An MCP server built on protocol version
2026-07-28(MCP 2.0). The stateless transport from this protocol version is what makes subscription-based delivery possible; if you need background on the protocol shift, read our MCP stateless spec migration guide. - Python 3.10+ (the reference implementation below) or any stack where you can handle JSON-RPC methods and sign webhooks.
- Persistent storage for subscriptions (SQLite or Redis is enough for a first version).
- A ChatGPT account with plugins/MCP connected apps enabled. If you are new to the Model Context Protocol itself, start with our MCP server tutorial before continuing.
Step-by-step guide
Step 1: Advertise the events capability in discovery
Before ChatGPT can watch anything, it must discover that watching is possible. Your server adds an events capability to its server/discover response. In the official Python SDK, the low-level server accepts custom methods through add_request_handler, so the three events/* methods can be added without forking the SDK. The community end-to-end test from October 2, 2026 found that after refreshing tools, the discovered event showed up alongside the plugin’s tools on its page, which is exactly the user-facing surface you are targeting.
Advertise only what you implement. ChatGPT will offer users whatever events/list returns, so every event you list must be wired end to end.
Step 2: Implement events/list to describe what can be subscribed to
This method returns the events your server emits, each with a name, a description, supported delivery modes, an inputSchema for subscription filters, and a payloadSchema for the delivered data. OpenAI’s documentation uses a comment.created example: a required document_id filter, and a payload containing the document ID, comment ID, text, and URL.
Design rules that save you pain later:
- Use stable, descriptive event names.
comment.createdbeatsnotify_1. - Split schemas deliberately.
inputSchemadescribes the filtering parameters the user sets at subscription time;payloadSchemadefines the shape of data delivered on each trigger. - Apply filters on your server before delivery, and return only events the authenticated account is authorized to discover. Authorization happens here, not at delivery time.
Step 3: Implement events/subscribe with full validation
When a user tells ChatGPT to monitor something (“alert me when a new comment lands on this doc”), ChatGPT calls events/subscribe with the event name, filter arguments, a callback URL, and a signing secret. Before accepting the subscription, run these checks in order:
- Confirm the authenticated user is authorized for the requested event and arguments.
- Validate the event name and arguments against your event definition.
- Require a
whsec_signing secret whose base64 value decodes to 24-64 bytes. Reject anything else. - Require an HTTPS callback URL. Reject private, local, and other non-public addresses. This is the server-side half of the SSRF protection that OpenAI enforces on its own side for verification requests and deliveries.
- Verify the callback endpoint with a signed, single-use challenge before sending any application data. The endpoint must echo the challenge back correctly.
- Store the subscription: owner, filters, callback URL, signing secret, and expiration.
Return a subscription record with a deterministic ID derived from the authenticated principal, callback URL, event name, and canonicalized arguments (canonical JSON comparison prevents duplicate subscriptions when argument key order differs). Include refreshBefore, the expiration your server grants; ChatGPT will call events/subscribe again before that time to refresh, using the same identity and the last saved cursor. Return cursor: null for event types that do not support replay.
One transport detail from community testing: on the 2026-07-28 transport, a request must carry an Mcp-Method header matching the JSON-RPC method in the body, or it is rejected with error -32020. If your own test client gets -32020, check the header first.
Step 4: Sign every delivery with Standard Webhooks
Delivery is webhook-only. Each trigger sends one POST request carrying one event, with a body no larger than 256 KiB. Every delivery carries four headers: webhook-id (unique per delivery, used as the idempotency key), webhook-timestamp (Unix seconds), webhook-signature (HMAC-SHA256, v1,<base64> format), and X-MCP-Subscription-Id.
The signature is HMAC-SHA256 over the string {webhook-id}.{webhook-timestamp}.{raw-body}, keyed with the base64-decoded bytes of the whsec_ secret. A minimal Python sender looks like this:
import base64, hashlib, hmac, time, uuid, httpx
def sign_event(secret: str, webhook_id: str, timestamp: int, body: bytes) -> str:
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{webhook_id}.{timestamp}.".encode() + body
digest = hmac.new(key, signed, hashlib.sha256).digest()
return "v1," + base64.b64encode(digest).decode()
def deliver(subscription, event_body: bytes):
if len(event_body) > 256 * 1024:
raise ValueError("event payload exceeds the 256 KiB limit; send a summary and let the agent fetch the full data with a read tool")
webhook_id = "msg_" + uuid.uuid4().hex
timestamp = int(time.time())
headers = {
"Content-Type": "application/json",
"webhook-id": webhook_id,
"webhook-timestamp": str(timestamp),
"webhook-signature": sign_event(subscription["secret"], webhook_id, timestamp, event_body),
"X-MCP-Subscription-Id": subscription["id"],
}
resp = httpx.post(subscription["url"], content=event_body, headers=headers, follow_redirects=False, timeout=10)
return resp.status_code
Follow the retry rules from the documentation walkthroughs: use exponential backoff, and do not retry on 410 (Gone) or 413 (Payload Too Large). Treat the webhook-id as the receiver’s idempotency key so retries never double-process an event.
Step 5: Run the end-to-end test the way the community verified it
You do not need a production app to prove the loop works. The verified October 2, 2026 test used a tiny “watch a company” server: the tester asked ChatGPT in a Work chat to watch a company, the subscription came in and the callback verified, a test event was sent, ChatGPT’s receiver returned 200, and ChatGPT posted in the chat about the event without being asked. Replicate that shape with your own domain event (a watched document, a status field, a queue length), then confirm these four things in order: the event appears next to your tools after a tool refresh, the subscription request arrives with a valid whsec_ secret and HTTPS URL, the callback challenge verifies, and a test event produces agent action in the chat within seconds.
Step 6: Harden the subscription lifecycle for production
Two operational details separate a demo from a production server. First, persistence: OpenAI requires persistent subscription storage, and subscriptions must survive server restarts. Store the full tuple (principal, callback URL, event name, arguments, secret, expiration) and reload it on boot. Second, secret rotation: if a refresh arrives with a new secret, replace the stored secret and sign with both keys during a short transition period, since Standard Webhooks supports multiple space-delimited signatures. The draft also describes a terminated revocation envelope, but ChatGPT does not support it; the only revocation signal you get is a failed refresh returning -32012 Forbidden, so treat refresh failures as subscription death and stop delivering.
If you are connecting this server to an existing automation stack, our tutorial on building an MCP server in Python covers the SDK patterns, and the guide to connecting an MCP server to Claude Desktop shows the client-side setup pattern that translates directly to ChatGPT’s plugin flow.
Troubleshooting
1. The event does not appear on the plugin page
The discovery response is cached. After adding the events capability and the events/list handler, use the plugin page’s refresh action (“Refresh tools”) so ChatGPT re-discovers the server. In the October 2, 2026 community test, the event appeared next to the tools only after a refresh. If it still does not show, log your server/discover response and confirm the events key is actually present in the capabilities object.
2. events/subscribe test calls fail with error -32020
On the 2026-07-28 stateless transport, every request must carry an Mcp-Method header whose value matches the JSON-RPC method in the body. The SDK’s low-level server rejects mismatches with -32020 before your handler runs, so this error always means a transport problem, not a logic problem. Add the header to your test client: Mcp-Method: events/subscribe alongside the JSON-RPC body.
3. Callback verification fails or times out
The verification request is a signed, single-use challenge to the callback URL ChatGPT supplied. Check four things: the URL is HTTPS, it resolves to a public address (private, local, and non-public addresses are blocked), your endpoint does not redirect (redirects are not followed), and it echoes the challenge back in exactly the expected form. Any proxy in front of your endpoint that rewrites paths or strips query strings will break verification silently.
4. Deliveries return 413 or silently stop
A 413 means your event body exceeded 256 KiB; do not retry it. The documented pattern is to send a summary in the event and let the agent fetch the full data with a read tool. If deliveries stop without errors, check whether your refreshBefore expired: ChatGPT refreshes by calling events/subscribe before expiry, and if your server rejected the refresh (or crashed without persistent storage), the subscription is dead. Rehydrate subscriptions from persistent storage on every restart.
5. The agent acts on stale or duplicate events
Use the webhook-id as an idempotency key on the receiving side and keep a short window of seen IDs. Remember that Standard Webhooks signing covers the timestamp, so reject deliveries with timestamps far outside a few minutes of now to defeat replays. If an event loop appears (your handler emits the event it subscribes to), add a source filter to your subscription arguments so the agent only watches external changes, not its own writes.
FAQ
See the FAQ section in the page front matter summary: free-plan availability, events vs polling, SDK support status, supported delivery modes, security traps, and subscription expiry behavior.
Conclusion
MCP Events turns ChatGPT from a tool you ask into a watcher you instruct. Build an MCP 2.0 server, advertise the events capability, implement the three methods with strict validation, verify callbacks with a signed challenge, and deliver one signed webhook per event under the 256 KiB ceiling. Start with a single high-value event in a Work chat, prove the loop with a test trigger, then harden storage and secret rotation. For the broader picture of event-driven agent design, continue with our n8n AI agent tutorial to wire these triggers into full automation workflows, or read the Claude Code MCP setup guide to compare how another agent host consumes MCP servers.
Frequently asked questions
Does ChatGPT support MCP events on free plans?
Yes. Community testing since the September 29, 2026 DevDay announcement reports that ChatGPT supports the proposed MCP Events specification at protocol version 2026-07-28 (MCP 2.0) on all plans, as long as your server meets the protocol requirements.
How is an MCP event different from a scheduled check or polling?
Polling wakes the agent on a schedule even when nothing changed. MCP events flip the direction: your server pushes a signed webhook the moment something happens (a new message, a comment, a status change), and ChatGPT runs the user's standing instruction right then. No wasted checks, no latency between change and action.
Do the official MCP SDKs support events yet?
Not yet. As of early October 2026, neither the official Python SDK (mcp 2.2.0) nor the TypeScript SDK had native events support, so builders add the three events/* methods as custom request handlers. Community packages like mcp-webhook-events on PyPI already fill the gap for Python servers.
Which delivery modes does ChatGPT support for MCP events?
Webhook delivery only. OpenAI's documentation states explicitly that polling, streaming, and the draft specification's gap and terminated control notifications are not supported by the ChatGPT integration.
What are the main security traps when building an events server?
Three stand out. First, SSRF through callback URLs: OpenAI blocks private, local, and other non-public addresses for verification requests and deliveries, and your server should do the same. Second, prompt injection through event payload text: treat payload content as untrusted data, never as instructions. Third, feedback loops: make sure an event handler cannot emit the same event again and subscribe to its own noise.
What happens when a subscription expires?
Your server sets refreshBefore when it accepts a subscription. ChatGPT calls events/subscribe again before that time to refresh it, using the same identity and the last saved cursor. If the user no longer needs monitoring, ChatGPT calls events/unsubscribe.