all posts

MCP Events: how agents subscribe to what happens in your product

MCP Events lets a server tell an agent that something happened, so work can start with nobody asking. This guide covers how subscriptions work, the 3 delivery modes, webhook security, and how to add events to your MCP server.

Guide 11 min read
Contacts imported from trade-show.csv14:02
contacts.importedsigned webhook
update_contactsstarted by an event
No one in the conversation

The work starts. Nobody asked.

What MCP Events is

MCP Events lets an agent act on something the moment it happens, with nobody asking. A contact list is imported, a reviewer leaves a comment, an incident opens, and the agent hears about it from the server instead of finding out later.

MCP Events is a proposed extension to the Model Context Protocol, identified as io.modelcontextprotocol/events. It lets an MCP client subscribe to things that happen in the system behind a server, and receive each occurrence by polling, over a held-open stream, or as a signed webhook. The proposal is SEP-3415, written by Peter Alexander for the Triggers and Events Working Group, which Clare Liguori and Peter Alexander lead.

This guide covers why MCP needed events, how a subscription works from discovery to delivery, the 3 delivery modes and when each fits, how webhooks are secured, what is promised about ordering and loss, who supports it today, and how to add events to your own MCP server.

ItemStatus on 11 November 2026
ProposalSEP-3415, Extensions Track, status Draft
Opened5 October 2026; marked ready for review on 28 October
Protocol revisionDefined for 2026-07-28 and later
Reference implementationOutstanding in an official SDK; conformance tests are open for review
Open questions5 listed in the proposal, including how events should reach the model

Treat everything below as a draft that can still change. The shape has held steady since the working group’s design sketch in February 2026, but the error codes are provisional and nothing is merged.

Why MCP needed events

MCP is request-driven. A person or a model asks, and a server answers. That covers most of what agents do, and none of what happens while nobody is asking: a new email, a push to a repository, a contact list imported into a CRM.

MCP already has notifications, and the proposal explains why they are not enough:

  • They carry no parameters. notifications/resources/updated says a URI changed. It cannot say “a P1 incident was created for service X”, because there are no subscription arguments, no payload schema and no payload.
  • They need a live connection. A client that is not connected receives nothing, and a client that cannot hold a stream open has no way in.
  • They cannot resume. There is no cursor, so whatever happened during a disconnect is gone, and the client cannot tell it missed anything.

The deployments that need events also pull in different directions. A serverless MCP server cannot hold a connection per subscriber. A client behind a firewall has no public URL for a callback. A client that pays per request should not poll. 1 delivery mechanism would strand at least 1 of them, which is why the proposal defines 3.

How MCP Events works

Take the CRM used across this site. Its MCP server already offers find_contacts, update_contacts and log_activity. With events it can also announce contacts.imported, so a sales team’s agent can update the contacts from every new trade-show list the moment it lands, with nobody typing the request.

  • Discover. The server advertises the extension in the capabilities it returns from server/discover. A client must not send events/* requests to a server that did not advertise it.
  • List. events/list returns each event type: a name, a description, the delivery modes it supports, an inputSchema for subscription arguments and a payloadSchema for what each event carries.
  • Subscribe. The client subscribes with a name and arguments, in a mode the event type offers. Arguments can filter the stream, such as 1 folder or 1 team, or shape the payload.
  • Receive. Each occurrence arrives as {eventId, name, timestamp, data, cursor}. The cursor is the subscription’s position, so the client can resume from it.
  • Act. The client decides what the agent does with it: add it to a conversation, start a new run, or queue it. The proposal leaves this to the application.

An event type, as events/list returns it

{
  "resultType": "complete",
  "events": [{
    "name": "contacts.imported",
    "description": "Fires when a contact list is imported into the CRM",
    "delivery": ["webhook", "poll"],
    "inputSchema": {
      "type": "object",
      "properties": { "teamId": { "type": "string" } }
    },
    "payloadSchema": {
      "type": "object",
      "properties": {
        "importId": { "type": "string" },
        "teamId": { "type": "string" },
        "importedBy": { "type": "string" }
      }
    }
  }]
}

The payload names the import and stops. The agent fetches the contacts through a tool when it needs them. The proposal asks for exactly this, under the name payload minimality: enough to identify and triage the event, never the full content.

A server that adds or changes event types at runtime can declare listChanged: true and send notifications/events/list_changed, the same pattern tools and resources use. Schemas should only grow. A breaking change ships under a new event name, alongside the old one for a while.

The 3 delivery modes

Each event type advertises any non-empty mix of poll, push and webhook. None is mandatory. The client SDK chooses a mode and runs the loop; the model only sees the events that arrive. Pick a mode to see the messages it takes:

  1. events/pollname, arguments, cursor: null
  2. resultno events, a fresh cursor, nextPollMs: 30000
  3. events/pollthe stored cursor, after 30 seconds
  4. result1 contacts.imported event, a new cursor, hasMore: false
  5. events/streamname, arguments, cursor; the request stays open
  6. notifications/events/eventcontacts.imported, sent on the open stream
  7. notifications/events/eventthe next import, on the same stream
  8. cancelclosing the request ends the subscription
  9. events/subscribecallback URL, a whsec_ secret, ttlMs
  10. POST challengea single-use nonce the endpoint must echo
  11. resultsubscription ID, refreshBefore, a cursor
  12. POST eventcontacts.imported, signed with the secret
  13. events/subscribethe same call again, before refreshBefore
Server holds
The subscription, its URL, secret and expiry
Client needs
A public HTTPS endpoint
Fits
Remote servers where a held connection is impractical
Arrows to the right are client requests; arrows to the left come from the server. Highlighted rows carry an event.
ModeMethodSubscription ends whenFits
Pollevents/pollThe client stops polling. The server holds no state the protocol requiresClients behind a firewall; stateless and serverless servers
Pushevents/streamThe client closes or cancels the requestLocal servers over stdio and clients that already hold connections
Webhookevents/subscribeThe TTL runs out without a refresh, or events/unsubscribeRemote servers and hosted agents with a public endpoint

Poll respects the server’s pace: nextPollMs says when to come back, and hasMore: true says to come back now and drain the backlog. Push carries the subscription ID in each notification’s _meta so a client with several streams can tell them apart.

Webhook delivery in detail

Webhook is the mode hosted agents will lean on, and the one with the most moving parts, because the server makes outbound requests to a URL a client supplied.

Subscribing to contact imports by webhook

{
  "jsonrpc": "2.0",
  "method": "events/subscribe",
  "id": 2,
  "params": {
    "name": "contacts.imported",
    "arguments": { "teamId": "sales" },
    "delivery": {
      "mode": "webhook",
      "url": "https://agent.example.com/hooks/crm",
      "secret": "whsec_<base64 of 24 to 64 random bytes>"
    },
    "cursor": null,
    "ttlMs": 3600000
  }
}
  • The client owns the secret. It is required, it uses the Standard Webhooks format (whsec_ plus 24 to 64 random bytes in base64), and the server never generates one. Each subscription gets its own.
  • The endpoint proves it wants the traffic. Before delivery starts, the server sends a signed request with a single-use challenge, and the endpoint must echo it. This stops anyone from pointing a server’s events at a victim’s URL.
  • The subscription expires unless refreshed. The client suggests a TTL; the server grants one and returns refreshBefore. Calling events/subscribe again with the same name, arguments and URL refreshes it. ttlMs: null asks for no expiry, which a server may grant.
  • Every delivery is signed. Each POST carries webhook-id, webhook-timestamp and webhook-signature headers. The receiver checks the HMAC, rejects timestamps older than 5 minutes, and drops repeated IDs.
  • The server guards its own network. Callback URLs must be HTTPS. The server validates the IP address at delivery time, connects to that address, and follows no redirects, so a subscription cannot steer it at internal hosts.

The subscription ID is derived from the principal, the URL, the event name and the arguments, so subscribing twice returns the same subscription. It is a routing handle, not a credential: unsubscribing or rotating a secret still needs the authenticated principal.

Cursors, ordering and what is guaranteed

The proposal promises less than a message queue, on purpose, and says so plainly. What a client gets:

  • Resume from a cursor. The cursor is opaque and server-defined. Replay is optional per event type; a type without it returns cursor: null, and anything that happened during a disconnect is lost.
  • A flag for gaps. truncated: true says the server skipped events, for example because the cursor was older than its history.
  • Order within 1 subscription. Poll and push deliver in the order the server produces. Webhook order is best effort, because each event is retried on its own.
  • At-least-once, with work. Delivery is at-least-once when the cursor is backed by a durable source and the client replays from it. Exactly-once is the client’s job, by de-duplicating on eventId.

For the contacts agent this means 2 rules. The update must be safe to run twice on the same importId, because the same import can arrive twice. And the agent should read the contacts’ current state through a tool before acting, because the event says what happened, not what is true now.

Who supports MCP Events today

ClientSupportSource
ChatGPTWebhook mode only, in Work chats on the web, in the desktop app with Cloud selected, and in dots. 1 event per POST, body up to 256 KiBOpenAI’s developer docs
ClaudeNot supported. Claude’s connector docs list draft capabilities among the MCP features it does not support yetAnthropic’s connector docs
Official MCP SDKsNo reference implementation yet; the proposal lists it as outstandingSEP-3415
Vercel AI SDK, TanStack AI, MastraExperimental supportHookdeck’s guide

ChatGPT’s implementation is the one to build against now. It needs events/list, events/subscribe and events/unsubscribe on the same authenticated endpoint as your tools. A person describes what to watch and what to do, for example watching a feedback channel for bug reports and opening draft pull requests, and ChatGPT subscribes for them. It may batch separately delivered events into 1 task run.

Security: an event is data, not an instruction

An event carries text from the outside world straight into an agent’s context, and it can start work with nobody watching. The proposal’s security section is built around those 2 facts.

  • Payloads are untrusted. An email subject that says “ignore all previous instructions” is still just a subject. Clients should treat payloads the way they treat tool results, and servers should keep payloads small to shrink what can be injected.
  • Authorisation happens 3 times. At subscribe time, the server checks the principal may watch that event with those arguments. At delivery time, it re-checks and ends the subscription if access was revoked. At action time, the agent’s response goes through normal MCP authorisation: receiving an event grants nothing.
  • Policy sits between receipt and action. The governance section asks clients to support policy evaluation between an event arriving and the agent acting on it. It leaves the policy language to the application.
  • Every mode has a kill switch. Stop polling, cancel the stream, or unsubscribe the webhook. A subscription granted no expiry has to be unsubscribed explicitly.

OpenAI adds 1 more from running it: watch for feedback loops. If the agent’s action changes data in the source app, and that change fires another event, the agent can run forever on its own output. Test for it before launch.

How to add events to your MCP server

The draft is not merged, so build where it is cheap to change: behind a flag, with the event logic separate from the delivery code.

Choose the events

  • Start from tasks people already ask for. If agents call update_contacts right after every import, contacts.imported is the event. Name events as noun.past-tense-verb, the way the proposal’s examples do.
  • Make arguments filters. A list, a team, a severity. A subscription to every import in the account is rarely what anyone wanted.
  • Send IDs, not content. Put the ID and the triage fields in the payload, and keep a read tool for the rest.
  • Reuse upstream IDs. When the source has a stable ID, use it as eventId, so the same import seen by 2 paths de-duplicates.

Make the tools safe to trigger

  • Idempotent writes. Updating the same 9 contacts twice should log 9 activities, not 18.
  • No echoes. Contacts the agent writes back must not fire contacts.imported again. Mark agent-made changes, or filter them out of the event.
  • Honest results. An update that changed 3 of 9 contacts should say so. With no person in the run, the tool result is the only thing that can.

Test the lifecycle

OpenAI’s test list is a good checklist for any client: advertise events, confirm events/list, subscribe, pass the challenge, trigger a matching event and a non-matching one, unsubscribe. Then test the edges: repeated subscribes, expiry and refresh across a restart, a disconnected account, revoked access, a bad signature, a duplicate delivery and a burst.

What changes when a task starts with no one watching

Today the contacts task ends at turn 9, when the person asks “Where are the rest? Can you try that again?” That question is how a team finds out that 6 of the 9 emails didn’t match. An event-started run has no turn 9. The agent updates 3 contacts, logs 3 activities, and nobody is there to ask.

  • The run needs a starter. Record whether a person, a schedule or an event started it, and which event. Without it, an event storm looks like a surge in demand.
  • Person-side signals disappear. Rephrasing, correcting and giving up need a person. What is left is the agent’s side: a retried tool, a changed argument, a partial update, a step cap reached.
  • Cost has no one to notice it. A feedback loop is spend with no conversation attached. A limit per event type, on runs per hour and cost per run, belongs next to the policy check.

That is where Abracadabra fits. It reads each run as a task, with what started it, where it stopped and what it cost, so an event-started update that changed 3 of 9 contacts shows as a stopped task, not a quiet success.

What is still open

  • How events reach the model. As injected context, a special message type, or a callback like a tool call. The proposal calls it an SDK question and leaves it open.
  • Whether old notifications become events. Resource subscriptions and the list_changed family could be re-expressed as events, gaining cursors and poll.
  • 1 subscription, several event names. Some sources yield 1 ordered feed of mixed types. Supporting that would change the wire format.
  • Task updates as events. A reserved mcp.task.updated event would let a client that started a long task and disconnected hear when it finished.
  • The error codes. 5 new codes are requested, and may be folded into InvalidParams instead.

Delivery infrastructure, the signing, retries and replay, is real work. If your server already sends ordinary webhooks, keep them, and add events/list when a client you care about asks for it.

Read the runs nobody asked for

Events add a new kind of run to every agent: 1 that starts on its own and ends without a reply to anyone. Those runs need the same reading as a conversation, with the starter in place of the person.

Abracadabra groups the calls of each run into a task and marks where it stopped, whether a person or an event started it. It starts free; see pricing.

MCP Events FAQ

What are MCP Events?

MCP Events is a proposed extension to the Model Context Protocol, SEP-3415, that lets an MCP client subscribe to things happening in the system behind a server and receive them by polling, a held-open stream or a signed webhook, so an agent can react without a person asking.

Is MCP Events part of the MCP specification?

Not yet. It is a draft on the Extensions Track, opened on 5 October 2026 and marked ready for review on 28 October. Its error codes are provisional and an official SDK reference implementation is still outstanding.

Which delivery mode should my MCP server support?

Webhook if you want ChatGPT today, since it supports only that mode. Poll if your server is stateless or your clients sit behind firewalls. Push if your server runs locally over stdio. A server can offer any mix per event type.

Does Claude support MCP Events?

Not as of November 2026. Claude’s connector documentation lists draft capabilities among the MCP features it does not support yet. ChatGPT supports the webhook mode in Work chats, the desktop app with Cloud selected, and dots.

How are MCP Events webhooks secured?

Each subscription has its own client-supplied Standard Webhooks secret. The server verifies the endpoint with a single-use challenge, signs every delivery, sends only to HTTPS URLs it has validated, and follows no redirects. Receivers reject timestamps older than 5 minutes.

all posts

Let events start the work. Read how it went.

See what started each task, where the rest stopped, and the calls that explain why.