Events
Events are opt-in and pinned to revision 6682596d65eec778fe0b8b1f43b4e89d2fe2c546. They require legacy-only transport configuration:
mcp({
transport: { protocolVersions: ['2025-11-25'] },
extensions: {
events: {
version: '6682596d65eec778fe0b8b1f43b4e89d2fe2c546',
cursor: { signingKey: process.env.MCP_EVENT_CURSOR_KEY! },
heartbeatMs: 15_000,
maxEvents: 100,
maxAgeMs: 300_000
}
}
})Register events with .mcpEvent(definition, handler). Each definition declares poll, push, or webhook delivery, argument and payload schemas, authorization, and optional subscription lifecycle hooks. events/list discovers authorized definitions. events/poll treats cursor: null as a fresh head and returns no backlog. Clients persist the returned signed cursor and replay from it. Providers do not persist client checkpoints.
POST events/stream returns event-only SSE. The stream emits active, event, heartbeat, gap or error, and terminated messages. A heartbeat creates no event, carries the current safe checked cursor, and lets clients persist progress during quiet periods. Apply maxEvents, maxAgeMs, and handler truncated or hasMore results to bound backlog. A gap has truncated: true and a fresh server cursor. Persist that cursor and continue the same delivery; do not reconnect or resubscribe. Applications can optionally use their own recovery tools for missing history. The example uses stable epoch-and-sequence cursors over a bounded in-memory history. It never creates an event merely because a client polls. Its epoch changes on restart, so persistent cross-restart replay requires an application event store.
Webhook subscriptions use events/subscribe and events/unsubscribe. Refresh the original tuple of principal, event name, arguments, callback URL, and variant by subscribing again. Unsubscribe with that same tuple. There is no subscription-list RPC.
Persist maxAgeMs with the webhook configuration to bound replay age; never persist the client cursor. A fresh cursor: null subscription returns a signed head cursor without replaying older events. An expired cursor or replay-age floor sets truncated. When an initial gap still has deliverable backlog, the adapter can wait for bounded initial delivery before returning a safe cursor. That cursor never advances past unacknowledged events. Clients must store returned subscribe, refresh, gap, and event cursors.
Webhook providers must be durable for persistent subscriptions. Acquire a database writer lock before reading quota state, atomically enforce principal quotas, reject owner replacement for an existing key, and persist ownership, endpoint verification, current and previous secrets, expiry, and delivery status. Call authorizeDelivery before every delivery so the application can compare the stored principal with authoritative identity and revocation state; revoked access stops and removes a subscription.
Finite grants use defaultTtlMs when ttlMs is omitted. A positive request below minTtlMs is accepted and clamped up; a request above maxTtlMs is accepted and clamped down. The subscribe or refresh result's refreshBefore is authoritative, so clients must schedule from that timestamp instead of assuming the requested duration. A ttlMs: null grant is accepted only when the provider is durable and allowNoExpiry is explicitly enabled. The example configures 60-second default, 30-second minimum, and 120-second maximum grants, and exercises both clamp directions. It uses a random, exact-match child-process credential only as an isolated fixture. Production servers must supply authoritative access-token verification.
Timer-backed intervals and deadlines—heartbeatMs, pollIntervalMs, pollLeaseMs, requestTimeoutMs, retryBaseMs, and maxRetryElapsedMs—must not exceed 2_147_483_647 milliseconds. The adapter rejects larger values instead of allowing portable timer overflow. Absolute subscription TTLs, cursor validity, and replay-age bounds use absolute expiry handling and are not reduced to this interval limit.
The adapter verifies callbacks through an authenticated HTTPS challenge, signs exact body bytes with Standard Webhooks HMAC headers, supports bounded dual signatures during secret rotation, validates addresses again at delivery time, rejects redirects, bypasses ambient proxies, limits response bytes and retries, and enforces deadlines. Receivers must verify the signature over the prefix bytes plus the original body bytes before parsing the challenge. Valid duplicate retries should receive 2xx while application processing runs once. The example's bounded deduplication cache is process-local; clustered receivers need shared durable deduplication. Production deployments keep private addresses denied. Isolated local fixtures must set both environment: 'development' and allowPrivateAddresses: true.
Hosts own subscription approval, callback allowlists, secret custody, replay recovery, and presentation of termination errors. The adapter does not provide asymmetric signing, JWKS discovery, a subscription-list method, or behavior for undefined proposal contracts.
Only one adapter instance may actively own a recovered subscription. The example demonstrates sequential process restart. Applications that share a durable provider across processes must supply external single-owner routing or leadership; the provider contract does not define distributed leases or compare-and-swap fencing.
See examples/events for Bun SQLite durability, a minimal protocol client, SSE, real subscribe/refresh/unsubscribe requests, signed TLS delivery, and recovery in a separate adapter process.