Durable MCP event subscriptions
Expose named events alongside tools on an authenticated MCP endpoint. Hookway stores active subscriptions and accepted pending deliveries in PostgreSQL, then sends matching occurrences to subscribers through signed HTTPS callbacks. No model runs inside Hookway.
This implementation follows the OpenAI MCP Events webhook profile
reviewed on 2026-10-21 and the draft design sketch at revision 13e0a597.
The compatibility profile uses protocol 2026-07-28, events/list,
events/subscribe, events/unsubscribe, and capabilities.events in
server/discover. Callback errors use OpenAI's -32015 profile. Events are not
advertised to older protocol clients. Existing tool protocols remain supported.
Configure and initialize
Start with the complete example. Configure
event_delivery.postgres_url, a stable namespace, and encryption_key using an
existing env/file/AWS secret reference. The encryption secret must be base64
encoding of exactly 32 random bytes. Keep it stable across restarts and identical
on all replicas sharing the namespace. Changing it makes the namespace unusable;
key rotation and historical catch-up are outside this release.
The connection must permit creation of the hookway_events schema during the
explicit migration. It may share a database with protection or replay; its tables
and lifecycle are independent.
hookway validate --config examples/mcp-events.yaml
hookway event-store migrate --config examples/mcp-events.yaml
hookway event-store status --config examples/mcp-events.yaml
hookway --config examples/mcp-events.yaml
Set the variables named in the example before running these commands. Migration creates only the event schema/tables and takes a PostgreSQL advisory migration lock. Serving never migrates and refuses missing/incompatible schemas or a namespace encryption-key mismatch. CLI status does not print subscriptions, callbacks, secrets or caller credentials.
Optional limits default to four workers, 1,000 active subscriptions, 10,000
pending deliveries, and 256 MiB of retained serialized event bodies per namespace.
workers, max_subscriptions, max_pending, and max_bytes can be authored under
event_delivery. Subscription metadata is bounded by schema/transport limits;
max_bytes accounts for occurrence bodies, not total PostgreSQL disk usage.
Replicas must use the same configuration, namespace, and encryption key.
Expose and publish events
Shared events definitions still describe payloads, and Create event still has
no delivery side effects. Add an events list to mcp_trigger:
events:
- event: issue.opened
input_schema:
type: object
additionalProperties: false
required: [repository]
properties: {repository: {type: string}}
filters: {repository: repository}
access: {api_keys: [reader]}
Each argument must have one filter binding to a dotted path inside occurrence
data. Arguments are scalar strings, booleans, or numbers; comparisons preserve
numeric precision and do not coerce types. All supplied arguments must match.
Exposed MCP event names use 1–128 letters, digits, dots, underscores or hyphens.
Omitted optional arguments do not filter. Use explicit API-key/scope/role grants;
anonymous subscriptions are unsupported. Grants use the same semantics as tools:
all scopes, any configured role, or an explicitly named API key. Filters do not
prove access to a resource in an upstream system.
A destination selects the MCP flow, and its Publish node selects the event:
destinations:
agents:
type: mcp_events
mcp_events: {endpoint: agent_tools}
# In a normal authenticated webhook flow, after Create event:
# - {id: send, type: publish, destination: agents, target: issue.opened}
Targets must be literal event names exposed by that endpoint. Broker options,
object payloads, and alternate wire formats are rejected. Publication revalidates
the occurrence after any transformations and limits its final JSON body to
256 KiB, including cursor: null. Existing destinations retain their behavior.
In the builder, use Config → Events for definitions and durable delivery settings. Select the MCP trigger's Events tab for subscriptions and grants, then select MCP events when adding a destination. The builder exports configuration; it does not manage live subscriptions. Live subscriptions and deliveries are managed in the operator console, separately from builder configuration.
Client lifecycle
Use the same authenticated MCP endpoint as tools. MCP 2.0 requests also require
the SDK's request metadata and matching Mcp-Method header; use a compatible SDK
rather than sending only a legacy JSON-RPC envelope. Discover events and send:
{
"name": "issue.opened",
"arguments": {"repository": "acme/app"},
"delivery": {
"mode": "webhook",
"url": "https://receiver.example/mcp-events",
"secret": "whsec_<base64-of-24-to-64-random-bytes>"
},
"cursor": null,
"ttlMs": 3600000
}
These are the events/subscribe parameters. Callback URLs must use HTTPS without
embedded credentials or fragments. Verification and deliveries block non-public
IP addresses at connection time, validate TLS, and never follow redirects.
Before activation, Hookway sends a signed verification control object with a
fresh random challenge. Return a successful JSON response containing the same
challenge. Successful verification is cached for five minutes by principal,
URL, and signing-key fingerprint; no application event is sent before verification.
Subscribe returns a deterministic opaque id, refreshBefore, cursor: null,
and truncated: false. Identity includes namespace, endpoint, authenticated
principal, URL, event, and canonical JSON arguments. Reordered object keys and
equivalent numeric spellings do not create new identities. Call subscribe again
before expiry to refresh; requests for no expiry still receive a finite grant.
The default is one hour, clamped between one minute and 24 hours, and further
capped by the authenticated token or API-key expiry. Refreshing a signing secret
retains the previous key for a five-minute dual-signature overlap.
OAuth bearer tokens are never persisted. Delivery uses the verified principal's scope/role snapshot until its token deadline, checking current configured grants before every attempt. Renew with a fresh authenticated request to update that snapshot. JWT issuer-side revocation/role changes are not continuously observable without a new authentication; the token expiry bounds that authorization lease. Removing an API-key grant/key, losing the key resolver, expiry, or unsubscribe stops new attempts. Config changes take effect on restart; roll replicas with matching settings. Already-started HTTP requests may finish.
Unsubscribe takes the original name, arguments, and delivery: {mode: webhook, url: ...}. It is idempotent and can affect only the authenticated principal's
subscription. Subscription IDs cannot be used to bypass ownership.
Delivery and recovery boundaries
A successful Publish commits an occurrence and matching pending recipients in one transaction. With no matching active subscribers, publication is a successful no-op. Capacity/database failures fail publication; existing caller acknowledgement policies still apply. Hookway does not introduce a durable spool before this publication boundary.
The retained key is (namespace, endpoint, event name, event ID). Repeated
acceptance preserves the first committed body and recipient set; different data
under the same key fails. Use stable sender IDs for sender redelivery. Rebuilding
an event with a generated UUID creates a new identity. The retention window is
bounded; this is not an exactly-once guarantee.
Workers use leased PostgreSQL claims and fenced completion updates. Pending
work survives process restarts and can be recovered by another replica after
lease expiry. Each callback receives the original event ID, timestamp, data,
and null cursor, signed with Standard Webhooks headers. Retries use the same
body bytes with a fresh signing timestamp and signature. 2xx acknowledges
receipt, not completion of an agent action.
Deliveries are at least once and unordered. Network errors, 408, 429, and
5xx retry with jittered exponential backoff, starting at one second and capped
at five minutes, for at most ten delivery attempts. Other statuses, including
410 and 413, are terminal. Expired or unsubscribed work is cancelled.
Terminal delivery metadata is retained for 24 hours, then removed with
unreferenced bodies by periodic cleanup or hookway event-store gc.
This release has no historical subscription catch-up, non-null cursors, poll,
stream, gap, or terminated delivery. Recovery applies only to already accepted
pending deliveries for subscriptions that remain active. Inspect metrics or the operator console for failures. The console supports scoped inspection and eligible manual retries.
Metrics include hookway_event_delivery_total{operation,outcome},
hookway_event_pending, hookway_event_retained_bytes, and
hookway_event_store_ready. No principal IDs or callback URLs are metric labels.
Database availability affects readiness whenever event delivery is configured.
Verification
Run make event-integration-test for real PostgreSQL, signed TLS callbacks,
restart/lease recovery, ownership boundaries, retries, capacity rejection, and the
authenticated webhook → MCP event path. The deterministic test receiver verifies
signatures and echoes challenges; private-address bypass exists only in the test
client, never as a production callback option.
For a local receiver outside tests, callback_receiver.py uses the Python standard library and your own TLS certificate/private-key files. It validates timestamps and signatures, echoes challenges, and prints received occurrences. Run it on a publicly reachable HTTPS hostname with a trusted certificate when connecting an external agent; a localhost receiver alone does not satisfy production callback policy.
Hosted-agent verification is separate: connect the deployed HTTPS endpoint through a plugin in a supported ChatGPT Work/Cloud environment, discover events, subscribe, confirm challenge verification, trigger matching and nonmatching webhooks, observe an agent turn and a tool call, then unsubscribe. Automated protocol tests do not establish that hosted-agent wake-up happened. Record the client, protocol and server revision when performing that check.
Operator console
The MCP event operations guide covers subscription inspection, per-attempt delivery evidence, scoped payload access, manual retry cycles, and persistent revocation/restoration. The combined issue demo includes its own Docker test identity provider and an automated OAuth/PKCE check.