mcp-webhook-events
MCP Events for Python MCP servers. Let ChatGPT watch something in your app and act when it changes.
On 29 September 2026 OpenAI added MCP Events to ChatGPT. A user can ask ChatGPT to watch something in your app, ChatGPT subscribes through your MCP server, and your server sends a signed webhook when it changes. ChatGPT then does whatever the user asked.
The official Python MCP SDK (mcp 2.2.0) does not support this yet. Neither does the TypeScript SDK (1.32.0). This package adds it to a server built on the official Python SDK, without forking or patching the SDK.
pip install mcp-webhook-events
Proven with the real ChatGPT
Tested on 2 October 2026 against a live server (UK Business Check, which watches UK companies):
- After "Refresh tools", ChatGPT listed the server's
company.status_changedevent next to its tools. - In a Work chat, "watch company 00445790" made ChatGPT call
events/subscribe. The callback was verified with a signed single-use challenge, and ChatGPT echoed it correctly. - A test event was signed and posted. ChatGPT's receiver returned
200, and seconds later ChatGPT posted in the chat by itself: "TEST — TESCO PLC (00445790): the service owner sent a test event; nothing about the company actually changed."
Quick start
from mcp.server import MCPServer
from mcp_webhook_events import EventDefinition, McpEvents
mcp = MCPServer("Ticket watcher")
events = McpEvents(mcp, store="events.sqlite3", definitions=[
EventDefinition(
name="ticket.updated",
description="A support ticket you are watching was updated.",
input_schema={"type": "object", "properties": {"ticket_id": {"type": "string"}},
"required": ["ticket_id"], "additionalProperties": False},
payload_schema={"type": "object",
"properties": {"ticket_id": {"type": "string"}, "summary": {"type": "string"}},
"required": ["ticket_id", "summary"], "additionalProperties": False},
),
])
events.install() # adds events/list, events/subscribe, events/unsubscribe and the `events` capability
app = mcp.streamable_http_app(stateless_http=True, json_response=True)
When something changes, from anywhere that can open the same store (a cron job, a webhook handler, a worker):
events.publish("ticket.updated", {"ticket_id": "T-1", "summary": "Customer replied"})
events.deliver_pending() # sends now. Call it again on a timer to retry failures
By default an event goes to every live subscription whose arguments match fields of the same name in the event data. Pass matches= to an EventDefinition for anything cleverer.
ChatGPT dots: get_recent_events
As of 3 October 2026, a ChatGPT dot that subscribes to your event gets woken up when you deliver it, but the event's data never reaches the dot (reported here). Work chats on the web do see it. Until OpenAI fixes that, turn on the workaround:
events.install(recent_events_tool=True)
This adds one read-only tool, get_recent_events. A dot that wakes up without the data calls it and gets the latest events from its subscriptions, newest first, with the full data and whether each was delivered. It takes event_name, event_id, arguments (the same values it subscribed with, matched with your event's matches rule) and limit (1 to 50, default 5). History is kept for 7 days.
Each caller only sees events from their own subscriptions. That needs principal= on McpEvents: a function that returns a stable user ID from the request context. If your server has no logins and you leave it out, every caller looks the same, so the tool refuses to list everything and asks for an event_id or the arguments instead. A dot always knows what it subscribed to, so it still gets its data, but nobody can browse other people's watches.
A full example is in examples/watch_server.py.
What it handles for you
Everything on OpenAI's MCP Events page that a server has to do:
| Requirement | How |
|---|---|
Advertise events in server/discover |
Middleware on the result (see below) |
events/list, events/subscribe, events/unsubscribe |
Registered on the SDK's own handler table |
whsec_ secrets of 24 to 64 bytes |
Checked before anything else |
Arguments validated against your inputSchema |
jsonschema |
| Callback verification with a fresh, signed, single-use challenge, constant-time compare | Yes, cached per caller and URL for 24 hours |
JSON-RPC -32015 with data.reason when verification fails |
Yes |
| Deterministic, idempotent subscription IDs, key order ignored | SHA-256 of caller, URL, event name and canonical arguments |
| Subscriptions survive restarts | SQLite (WAL) |
Expiry, refreshBefore, ttlMs |
Default 7 days, minimum 1 hour, never "forever" |
| Secret rotation | Signs with old and new keys during a 24 hour window |
Standard Webhooks signing, webhook-id = eventId, X-MCP-Subscription-Id |
Uses the official standardwebhooks package |
| Retries with backoff, same event ID, fresh signature | 30s, 2m, 10m, 30m, 1h, 2h, then gives up |
No retry on 410 or 413, and 410 drops the subscription |
Yes |
| One event per request, 256 KiB maximum | Enforced |
| HTTPS only, public addresses only, checked at connect time, no redirects | Yes, with TLS still checked against the original hostname |
| Forget what isn't needed | Expired subscriptions, their URLs and secrets, and old delivery history are purged |
Not included, because ChatGPT does not support them either: polling, streaming, replay cursors and the draft's gap and terminated notifications.
Three things that had to be worked out
These are the parts that are not obvious from the documentation, in case you are building this yourself.
- Custom methods are allowed. The SDK's low-level server has
add_request_handler(method, params_type, handler), and it acceptsevents/*methods over the 2026-07-28 Streamable HTTP transport. No fork is needed. - Clients must send
Mcp-Method. On the modern transport, a request without anMcp-Methodheader matching the body's method gets rejected with-32020. ChatGPT sends it, but your own test client needs to as well. - The SDK drops unknown capabilities.
ServerCapabilitiesignores unknown keys, and spec results are cleaned against the schema, soevents: {}never reaches the wire if you put it in the result. This package adds it in middleware, after the result is serialised.
Testing
pip install -e ".[test]"
python -m unittest discover -s tests -v
python tests/mutation_check.py # breaks the code 36 ways, and every one must be caught
The tests run a local receiver that plays ChatGPT. It checks every signature with the official Standard Webhooks verifier rather than this package's own code. mutation_check.py proves the tests catch real faults: unverified callbacks, wrong signatures, ignored filters, retrying a 410, leaking to private addresses and so on.
Status
Version 0.2.0 (adds get_recent_events for ChatGPT dots). MCP Events is still a draft extension, so details may change, and the official SDKs will probably add support at some point. Until they do, this is a small, tested way to use it today.
Issues and pull requests are welcome.
Licence
MIT. Made by Shaun Owen.
Metadata
Release files for mcp-webhook-events 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mcp_webhook_events-0.2.0.tar.gz | 22.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_webhook_events-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 36.7 kB
Release files / mcp_webhook_events-0.2.0.tar.gz
| Download URL | mcp_webhook_events-0.2.0.tar.gz |
|---|---|
| Size | 22.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7eff677e2a98a3309edd8ae00cc1bc8c93e398de10c99c7bf588b8d0e24219e5
|
|
BLAKE2b-256 checksum How to use checksums |
9541629819b9e4c1ca48e6852a33972b49ec19fd02e792d9e3b8056753f362f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / mcp_webhook_events-0.2.0-py3-none-any.whl
| Download URL | mcp_webhook_events-0.2.0-py3-none-any.whl |
|---|---|
| Size | 14.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
15587d0000bc240b58e209b92c6117fefeb3aa574ee5e9d6d075d906b7af38f5
|
|
BLAKE2b-256 checksum How to use checksums |
4106617faa89b2db358785d4866246cb682f84374fd411e141044cb1d1dd9d69
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log