Overview
When a connector’s MCP server advertises MCP Events (capabilities.events in server/discover or initialize, or the io.modelcontextprotocol/events extension), the runtime can subscribe to its events on the user’s behalf. Each event creates a Rails ticket for the chosen bot, the same ticket an inbound webhook trigger creates, and a connector.event.received entry in that bot’s bot_events ledger.
User guide: Wake a bot from a connected app’s events. Allternit as the events server: MCP Events.
Runtime REST (Clerk session)
Served next to the connector routes, at/api/v1/mcp/connectors/:id/… (also /mcp/connectors/:id/…). :id is an mcp_connectors id the caller owns.
GET /api/v1/mcp/connectors/:id/events
The server’s events/list (live, cached 60 s per user and connector) plus this user’s subscriptions on the connector.
supported: falsewithevents: []: the server does not offer events.error(otherwisenull) when the server could not be listed:{"reason", "message", "needsReauth"}.needsReauth: truemeans reconnect the connector. Subscriptions are still returned.404 {"code":"connector_not_found"}when the connector doesn’t exist for this user and has no subscriptions left.
POST /api/v1/mcp/connectors/:id/events
Answers
200 {"subscription": Subscription} when the server accepted it (status: "active"), 502 {"subscription": Subscription} when it was stored but the server or cloud refused it (status error or needs_reauth, see error). 400 {"code":"bad_request"} for a bad body, 404 {"code":"bot_not_found" | "connector_not_found"}.
Idempotent. The subscription id is mcp_protocol::events::subscription_id(user id, connector URL, name, arguments) (sub_ + 24 hex; argument key order doesn’t matter). Posting the same identity again returns the same subscription, refreshes it at the server, and updates botId/executionMode. A live subscription keeps its secret.
DELETE /api/v1/mcp/connectors/:id/events/subscriptions/:sid
events/unsubscribe at the server (best effort), removal at the cloud receiver, row deleted. 204; 404 {"code":"subscription_not_found"}.
Subscription
status: pending | active | error | needs_reauth. error is {"code", "reason", "message"} for error and needs_reauth, else null.
Lifecycle (runtime background loop, every 60 s)
- Refresh an active subscription when
refreshBeforeis within 15 minutes (or half its window, if shorter): a new secret is registered at the cloud first (the old one keeps verifying for 1 hour), thenevents/subscribewith it. - Connector deleted or disabled: removed at the cloud, row deleted.
- OAuth grant revoked (the connector had a credential and lost it) or its token can’t be renewed:
events/unsubscribeis attempted, the cloud stops accepting events, statusneeds_reauth. Reconnecting resumes it with a new secret. - Transient errors (table above) are retried; others wait for the user.
- A cloud removal that failed is retried until it succeeds.
Cloud receiver
Runtime registration (relay-signed)
Signed likePOST /api/v1/runtime/events (see MCP Events › forwarding): x-allternit-runtime-id, x-allternit-runtime-ts, x-allternit-runtime-sig: v1=<hex HMAC-SHA256(sha256_hex(device_token), "<ts>.<METHOD>.<path>.<hex sha256(body)>")>.
POST /mcp/events/callback/:key (public)
Standard Webhooks signed with the subscription’s secret (webhook-id, webhook-timestamp, webhook-signature).
Events are held in the channels relay queue (
channel_inbound_queue on an internal mcp_events route), delivered in order, retried with backoff for 24 hours, and relayed to the runtime as a device-signed POST /api/v1/mcp/event-deliveries:
200 {"status":"triggered","ticketId","eventId"}, 200 {"status":"duplicate"} (ledger idempotency key mcp-event:<sub>:<eventId>), 200 {"status":"terminated"}, or 410 for a subscription it no longer has (the cloud marks the event dead instead of retrying).
Storage
- Runtime:
mcp_event_subscriptions(migration V241). Secrets sealed with the runtime’s token key. - Cloud:
mcp_event_client_subscriptions+mcp_event_client_seen(migration 059). Secrets sealed withALLTERNIT_CREDENTIALS_KEYwhen set. Dedupe marks are kept 7 days. - Connector event data is not forwarded to the cloud event backbone.
Automation rules and runs
The event transport identity remains(owner, connector URL, event name, canonical filters). Independent rules fan out from that subscription; do not create multiple upstream subscriptions for different tasks.
POST /connectors/:id/events accepts an optional automation object. When present, it saves the subscription’s default rule (<subscription-id>:default). Omitting it preserves existing rules and keeps the older ticket-only contract compatible.
REQUIRE_APPROVAL, PLAN_ONLY, or ACCEPT_EDITS; automation rules cannot bypass permissions. The bot must belong to the caller and support enforceable session tool permissions. Unrecognized rule fields are rejected.
All paths below have the /api/v1/mcp prefix and the same authenticated owner boundary as the connector. <base> means /connectors/:id/events/subscriptions/:sid.
State progression is
batching → awaiting_approval or queued → running → completed / failed / interrupted. Other terminal states are denied and cancelled; stopping is transient. Paused rules and daily limits leave jobs queued with a reason and do not block other rules.
Delivery and execution reliability
Runtime acceptance, event deduplication and rule fan-out share an immediate SQLite transaction. Execution uses compare-and-set claims and a serial runtime dispatcher. Factory tickets use an event-derived identity and recover safely if ticket writing succeeded before ledger append. Neither webhook receipt nor ticket creation means the bot finished the task. The dispatch worker runs separately from subscription renewal. Each job snapshots rule version/config and its original event data. Approval is enforced before dispatch; all native tool actions still ask (or are denied in plan mode). Subscription/rule/bot/connector access is rechecked before a turn starts. Restarted executions become interrupted rather than being replayed automatically. Runs awaiting cancellation block new dispatch until cancellation is acknowledged or held for inspection. Cancellation requests have a 15-second deadline. Strengthening a rule to ask-first or plan-only applies to pending work while preserving its original task instructions. Event names must match the subscribed name, IDs must match the signedwebhook-id, and timestamps must be RFC3339. Arguments and payloads are checked against the advertised JSON Schemas with offline reference resolution; schemas cannot fetch local files or arbitrary remote URLs. Automation subscriptions require an advertised event definition before a rule is saved. Existing ticket-only subscriptions retain visible transport error states for unsupported or missing events. Event discovery caches are scoped to the runtime database, owner and connector.
The connector list also includes archivedSubscriptions: [{id, name}] for up to 20 stopped subscriptions with run history. Read-only run history and event payloads remain owner-scoped after the transport subscription is removed.
Cursor recovery
The last durably accepted incoming cursor is persisted and supplied on refresh. A server response withtruncated: true records that history was unavailable; data not provided by the source cannot be reconstructed. Transport retry remains a separate 24-hour cloud queue.
Each execution attempt also has a canonical agent_runs record, so the existing MCP list_runs, get_run and get_run_result tools can read its result. Terminal results commit with a durable notice outbox. Notice delivery is idempotent and forwards run references and status, without forwarding raw source payloads, task instructions or result text. Runtime restart records interrupted work and queues a notice for inspection.
Storage adds migration V249: mcp_event_rules, mcp_event_jobs, mcp_event_receipts, mcp_event_attempts mcp_event_job_actions and mcp_event_notices. Raw source receipt deduplication lasts seven days; job identities remain stable. Run limits are rolling 24-hour attempt counts. Spend thresholds use reported llm_usage_events attributed to all attempts’ sessions, not estimates or a hard budget reservation.