Packages
MCP Events from packages
Agent playbook — written for the agent connected to your account; you can read along
MCP Events let a connected host subscribe to things your packages emit — a
Discord message, a checkout, a job finishing — and wake the agent when they
happen. Kody implements the webhook-only profile of the draft MCP Events
extension (design sketch 2026-02-19; OpenAI ChatGPT profile on protocol
2026-07-28). Poll and push delivery are out of scope.
This surface is behind the mcp-events-extension feature flag. Signed-in users
can turn it on from this page. Experimenters get it when operators enable the
flag for the experiments audience.
Package subscriptions between your own packages stay on Package subscriptions and events. MCP Events is the same bus exposed to an external MCP client.
What you get
When the flag is on and the MCP client advertises events support:
/mcpadvertises aneventscapability onserver/discover.- The client can call
events/list,events/subscribe, andevents/unsubscribe. - Matching package
events.dispatchcalls POST a Standard Webhooks-signed payload to the client's callback URL.
The tool surface stays search / execute / api. Nothing is exposed by
default: each topic opts in with "mcp": true on its kody.emits entry.
Clients that consume it today
| Client | Support |
|---|---|
| ChatGPT (Work chats on web; desktop Work + Cloud; dots) | Webhook MCP Events on protocol 2026-07-28 |
| Other MCP hosts | Only if they advertise events support and implement webhook subscribe/delivery |
Cursor, Claude, and other hosts that do not advertise the extension keep today's
behavior: no events capability, and events/* methods are not registered.
Opt a topic into MCP
Declare the topic in package.json#kody.emits as usual, and set mcp: true:
{
"name": "@you/ping-notifier",
"exports": {
".": "./src/index.ts"
},
"kody": {
"description": "Emits a ping event an MCP host can subscribe to.",
"emits": {
"@you/ping.ready": {
"description": "A ping was requested.",
"mcp": true,
"payloadSchema": {
"type": "object",
"properties": {
"message": { "type": "string", "minLength": 1 }
},
"required": ["message"],
"additionalProperties": false
}
}
}
}
}Rules:
- Omit
mcpor set"mcp": falseto keep the topic package-internal (same-userkody.subscriptionsstill work). - The MCP event name is the topic string (
@you/ping.ready). - v1 subscriptions take no filter arguments (
inputSchemais an empty object). - Payloads still follow the package-event 64 KiB cap; MCP delivery allows up to 256 KiB for the full webhook body.
Emit
Same helper as package-to-package events:
import { events } from 'kody:runtime'
export default async function ping(input: { message: string }) {
await events.dispatch({
topic: '@you/ping.ready',
idempotencyKey: `ping:${input.message}:${Date.now()}`,
payload: { message: input.message },
})
return { ok: true as const }
}Dispatch stays asynchronous: validate → enqueue on
kody-package-events-dispatch → deliver to package subscribers and any
matching MCP subscriptions for the same user. There is no second dispatch path.
Subscribe and delivery
- The host calls
events/listand sees only MCP-opted topics from packages the connection may read (connection-profile grants apply). - The host calls
events/subscribewith the event name, a HTTPS callback URL, and awhsec_…signing secret. - Kody verifies the callback with a signed challenge, then stores the subscription (TTL default 1 hour, max 24 hours; refresh before expiry).
- When your package dispatches, Kody POSTs one Standard Webhooks-signed event per subscription:
POST /mcp-events/callback
Content-Type: application/json
webhook-id: evt_…
webhook-timestamp: 1739980800
webhook-signature: v1,…
X-MCP-Subscription-Id: sub_…
{
"eventId": "evt_…",
"name": "@you/ping.ready",
"timestamp": "2026-10-07T12:00:00.000Z",
"data": { "message": "hello" },
"cursor": null
}Subscriptions stop delivering when the OAuth client is revoked, the password
changes, or the TTL expires without refresh. Revoke and password-change paths
delete the rows immediately; an expired TTL only stops delivery until a later
events/subscribe from that principal prunes the stale row. Same-user scoping
is unchanged: events never cross accounts.
Try it
- Turn the flag on with the button on this page (or opt into experiments and wait for the experiments audience enable).
- Publish a package with an
mcp: truetopic (example above). - Connect ChatGPT (or another events-capable host) to your Kody MCP server.
- Ask the host to monitor
@you/ping.readyand say what to do when it fires. executethe package export that dispatches the event.
Further reading: Package subscriptions and events, ADR 0059.