Let an MCP server call you: the experimental events client and its webhook delivery


You wrote a monitoring loop that makes the agent ask “any new tickets?” every ten minutes. Most of the time the answer is no. You paid for ten model calls to receive ten “no”s, and the one ticket that mattered waited up to ten minutes to be noticed.

The MCP events extension exists to fix that shape of problem: the server pushes an event the moment something happens, instead of the agent asking over and over. Hermes now implements its webhook delivery mode — PR #140121, merged on October 22, 2026. It is experimental and lives on main, but the path is complete: subscribe, answer the verification handshake, and let an event start a real agent run.

Why this used to be impossible

Hermes already had most of the parts. The gateway’s webhook listener verifies Standard Webhooks whsec_ signatures and loads dynamic routes, and those routes can deliver results anywhere. What was missing was the MCP side: events/list, events/subscribe, events/unsubscribe, and a refresh loop to keep grants alive.

Without them, registering a webhook route with an events server by hand fails. The server POSTs a signed {"type":"verification"} payload; Hermes treated it as an ordinary event, started a run, answered 202, and never echoed the challenge. The server decided the callback was unusable, returned challenge_failed, and the subscription died.

Three steps to turn it on

mcp_servers:
  ambush:
    url: "https://example.com/mcp"
    events: true          # register the event tools for this server

platforms:
  webhook:
    enabled: true
    extra:
      public_url: "https://hermes.example.com"

Three things must all be true:

  1. events: true is per server. Without it, the three event tools never appear.
  2. The webhook platform must be enabled — that is how events arrive.
  3. public_url must be HTTPS. Servers reject plain-HTTP callbacks, so put a TLS-terminating reverse proxy or a tunnel in front of port 8644.

The three tools

With the switch on, the server gains three utility tools:

  • list_event_types — the event types the server offers, plus your current subscriptions. Ask before guessing names.
  • subscribe_event — subscribe with the event’s own arguments, what to do when it arrives (instructions, up to 2000 characters), and where each run’s result goes (deliver, deliver_chat_id). Every subscription is approved by you, because a subscription is a standing trigger for unattended runs. The approval shows the instructions and the destination in full.
  • unsubscribe_event — cancel.

instructions is the part to think hardest about. It is not “notify me when X happens”; it is “when X happens, do this”. Write it vaguely and you get an agent that fires every time and does the wrong thing every time.

Underneath: a dynamic webhook route

Each subscription is stored as a dynamic webhook route — hermes webhook list shows it. It carries its own whsec_ signing secret, while the grant and the replay cursor sit beside it in mcp_event_subscriptions.json. The route itself only changes when you resubscribe with different instructions or a different destination.

A few runtime details worth knowing:

  • The verification challenge is answered without starting a run.
  • Deduplication is by eventId, not by the per-attempt webhook-id. A server resending the same event triggers one run.
  • Grants are refreshed before the lifetime the server granted runs out while the connection is up, and again whenever it reconnects.
  • If the server ends a subscription, Hermes removes the route and tells the deliver target.
  • A refresh the server refuses for good — access revoked, event type removed, arguments no longer valid — removes the route; other failures retry with back-off.

The event payload reaches the run whole, up to 32,000 characters of JSON, with a note when it is cut. Event payloads are external data, so those runs use the same constrained webhook toolset as any other webhook route.

Five ways it silently lapses

This is not subscribe-once-and-forget. The known failure paths:

  1. The connection must be up. With lazy: true or idle_timeout_seconds, the connection is not always up, so a subscription can lapse until the next connection resubscribes it.
  2. The gateway must be running. If hermes gateway run is not up, no event arrives — the most common reason a test looks like “the feature is broken”.
  3. Refresh failures back off, but a permanent refusal deletes the route.
  4. Rate limits drop events. The listener’s per-route limit (platforms.webhook.extra.rate_limit, 30 a minute by default) applies to event deliveries too, and a server may not retry a rejected delivery. Raise it for a busy event type.
  5. Turning the route off in the webhook dashboard lets the subscription lapse; turning it back on resubscribes.

What is explicitly not supported

The docs are blunt about the gaps, so do not spend time there:

  • The poll and push delivery modes are not implemented — webhook only.
  • A secondary profile under gateway.multiplex_profiles cannot subscribe. Dynamic routes load from the launch profile’s home.

The whole design also rests on the MCP events design sketch, still a working-group draft. That is why the feature is labelled experimental: the protocol shape may change.

When it is worth it

If your need is “look at a page once an hour and see if it changed”, a cron job is still simpler: no public URL, and it does not lapse quietly when the gateway stops.

MCP events pay off when the upstream already has an event stream and latency matters — new tickets in an issue tracker, a failed deployment, a new item in a feed. In those cases “being told” and “asking every ten minutes” are different experiences, and the former also removes a pile of model calls that returned nothing.

If you are still weighing push against poll, our webhook notification walkthrough compares the two. And MCP tool costs and spend limits covers another set of new keys in the same mcp_servers block that you will likely configure alongside these.

Release status

The feature is on main only: merged in the early hours of October 22, 2026, not part of the shipped v0.22.4 and not in any tag. Track main if you want to try it, and expect the protocol shape to move before it stabilizes.