Skip to content

sep(SEP-3415): Events Extension - #3415

Open
pja-ant wants to merge 10 commits into
modelcontextprotocol:mainfrom
pja-ant:pja/sep-events-extension
Open

pja-ant wants to merge 10 commits into
modelcontextprotocol:mainfrom
pja-ant:pja/sep-events-extension

Conversation

@pja-ant

@pja-ant pja-ant commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Defines Events, an extension (io.modelcontextprotocol/events) that lets an MCP client subscribe to things that happen in an upstream system, such as a Slack message, a repository push or a PagerDuty incident, and have an agent react without the user being present.

  • A server lists event types with events/list: a name, an inputSchema for subscription arguments, a payloadSchema, and the delivery modes it supports.
  • Three delivery modes, none mandatory: poll (events/poll), push (a long-lived events/stream), and webhook (events/subscribe, signed per Standard Webhooks, with a refreshed TTL).
  • Opaque cursors let a client resume after a disconnect. Replay is optional per event type, and truncated: true signals skipped events.

This is the design sketch from the Triggers & Events Working Group, discussed in its weekly meetings, written in SEP format. To fit the 2026-07-28 protocol, the capability moves under extensions, list_changed rides on a subscriptions/listen filter field, and results carry resultType.

For reviewers:

  • The SEP requests five general-purpose error codes. The numbers (-32023 to -32027) are provisional.
  • The reference implementation in an official SDK is outstanding.
  • Open Questions lists eight items, including whether to fold these errors into InvalidParams.

Verified with prettier --check, and every in-page link resolves to a heading. The file keeps its 0000- name, so render-seps skips it.

AI assistance: an AI coding assistant restructured the design sketch into SEP format and aligned it with the current protocol. The design, design decisions and review are by @pja-ant.

Adds an Extensions Track SEP for Events, an extension that lets a client
subscribe to things that happen in an upstream system and have an agent
react to them without the user being present.

The SEP is the Triggers & Events Working Group's design sketch written in
SEP format and aligned with the 2026-07-28 protocol. A server lists event
types with events/list, and delivers occurrences by poll, push or webhook,
with opaque cursors for resuming after a disconnect.
@pja-ant pja-ant changed the title SEP-0000: Events Extension SEP-3415: Events Extension Oct 5, 2026
The verification challenge is answered by the webhook endpoint in its HTTP
response, so the client has nothing to do with it. Only gap and terminated
envelopes must be forwarded to the client.

Also say how a receiver picks the secret for a challenge on a first subscribe.
The subscription id in the header has not been given to the receiver yet, so
it tries its registered secrets that have no id. The 503/425 retry guidance
does not apply to the challenge.
panyam added a commit to panyam/mcpconformance that referenced this pull request Oct 13, 2026
MCP Events now has a SEP: modelcontextprotocol/modelcontextprotocol#3415,
opened 2026-10-05, the design sketch written up in SEP format. SEP numbers
are PR numbers, so the yaml and every check ID move from the 9999
placeholder to 3415. The ID set is unchanged apart from the prefix (160
IDs), and the scenarios, negative controls and runner behaviour are
untouched.

The rows still quote the sketch. The SEP restates it rather than quoting
it, so only 11 of 165 row texts appear in the SEP verbatim, and spec_url
keeps naming the text the rows actually quote until they are re-extracted.
The header now says so, and records that sketch PR 7 closed unmerged and
SEP-3415's Capability Negotiation section carries the rule its six rows
quote.
@lmaccherone

Copy link
Copy Markdown

Strong +1. @pja-ant, I'm glad you kept going with this after the Transports WG ruled webhooks out of scope, along with my WebSockets SEP.

I'd like to take on Open Question 2 with a working prototype: resource watching through events, over a store that keeps every version of a resource. It will be an MIT-licensed package.

One question before I build. A resource subscription is state, not a stream of events, so the server can coalesce: if three updates land while an endpoint is down, sending only the newest is correct for anyone watching the resource. Should an event type be able to declare that it coalesces, and should coalescing set truncated: true? I lean no, since nothing a state subscriber needs was skipped.

The cursor also tells the server which version the subscriber already holds, so a delivery can be a single JSON Merge Patch (RFC 7396) from that version to the newest, not the full value. That's the design for my own resources layer today.

@yaotsakpo

Copy link
Copy Markdown

A standard place in _meta for the cause of an event

An EventOccurrence tells a client what happened (name, data) and when, but not who caused it or how well the server knows that. The server usually does know. GitHub sends author_association with pull request events, so a server can tell a repository member from a first-time contributor. An email server has the DMARC result, so it can tell an authenticated From from one anyone could have typed. Today a server can only put that in its own payloadSchema, and a client has to know that particular server to use it. The Governance section asks clients to support "policy evaluation between event receipt and action execution" (L931), and who caused the event is the fact such a policy most needs and cannot work out by itself.

I'd suggest one optional, reserved _meta key on EventOccurrence:

// a pull request opened by someone new to the repository
"_meta": {
  "io.modelcontextprotocol/events/cause": {
    "actor": "octocat",
    "actorKind": "github_user",
    "established": "authenticated",
    "relation": "FIRST_TIME_CONTRIBUTOR"
  }
}

// an email whose From passed DMARC
"io.modelcontextprotocol/events/cause": {
  "actor": "billing@vendor.com",
  "actorKind": "email",
  "established": "authenticated",
  "method": ["dmarc"]
}

The second example is the one that matters most. A vendor's mailbox is taken over and sends new bank details from the vendor's real address. DMARC passes, established is "authenticated", and the message is still the one that moves money to the wrong account. That is why the field describes the cause and grants nothing.

What the text would say:

  • established is one of three values. "authenticated" only when the server, or an upstream it relies on, verified the actor. It means the actor is who the payload says, and nothing about whether that actor should be causing this event. "asserted" when the upstream reports an actor it did not verify, such as a From header that did not pass DMARC. "unknown" when the server cannot say.
  • A missing key means unknown. A client MUST NOT treat it as authenticated.
  • actorKind, method and relation are server-defined strings that the server documents, so nothing beyond the three established values needs a shared vocabulary.
  • It describes the cause. It is not an authorization decision and grants nothing. What to do with it stays with the client and the user, as today.
  • actor comes from the upstream, so it is untrusted text like the rest of the payload.

A server that has none of these facts sends nothing and loses nothing. It also sits well with payload minimality: a few short fields, not content. If it is useful I can turn this into a change against the SEP text, including the entry in Reservations.

gkurt added a commit to edgedelta/edgedelta-mcp-server that referenced this pull request Nov 2, 2026
)

## Why

Clients such as Canvas start work from events on a connector's MCP
server through the MCP Events extension,
`io.modelcontextprotocol/events`
([SEP-3415](modelcontextprotocol/modelcontextprotocol#3415),
not merged yet). They subscribe as the team, so no model or session has
to be there. With this PR, a client can start work when an Edge Delta
monitor alerts, with no agent polling `get_event_search`.

This PR adds poll delivery only. The hosted server is stateless, and
webhooks need a durable subscription store in the backend. That plan
lives with the backend in the edgedelta repo.

## What

- `initialize` advertises the extension, through an `AfterInitialize`
hook.
- mcp-go sends methods it doesn't know to "method not found" and has no
hook for custom ones, so the server answers `events/list` and
`events/poll` before mcp-go sees them:
- **HTTP:** `MCPHTTPServer.Handler()` wraps the Streamable HTTP server.
`HTTPServer()` is unchanged, but it doesn't serve events.
- **stdio:** stdin goes through a filter, and every response is written
as one line.
- Other `events/*` methods answer `-32601`.
- Two event types, both delivered by polling:
- `monitor.alert`: a monitor changes state. Arguments: `monitorId`,
`monitor`, `type`, `states`, `service`, plus extra CQL in `query`.
- `pattern.anomaly`: the same for pattern anomaly monitors, with the log
patterns behind the anomaly in the payload.
- Each event is the event search's own. Its `eventId` is the event's
`event.id`. Its payload has a `title` and a `url` to the event in the
app (the link monitor notifications use), plus the monitor's state,
priority, service, message and values.
- The cursor is a time plus the IDs delivered since that time:
- The time stays two minutes behind now, because the search indexes
events a little late. The IDs stop a later search from delivering
anything twice.
- `maxAgeMs` moves the cursor forward, and so does a seven-day limit.
`truncated` is true only when that skipped an event.
- `maxEvents` defaults to 50 and is capped at 100, with `hasMore` set
when more are waiting. `nextPollMs` is 30000.
- Errors use the SEP's provisional codes:
  - `-32023` NotFound, with `data.kind: "event"`.
- `-32602` for bad arguments, a cursor the server didn't issue, or CQL
the search refuses.
  - `-32024` when the token can't search events.
- `-32020`, with HTTP 400, when an `Mcp-Name` header names a different
event. The header isn't required yet.
- New `WithAppURL` option. By default the app URL follows the API URL:
staging gets `app-staging`.
- The README has a new Events section.

## Testing

- Table-driven tests in `pkg/events` cover:
- polling sequences: a `null` start, each event delivered once, events
indexed late, backlogs drained in batches with ties at the same
millisecond, and `maxAgeMs` with and without a gap;
  - every error;
  - the CQL built from arguments, and the payload mapping;
  - the JSON-RPC layer.
- I checked that the tests fail when the lag is set to zero.
- Tests in `server` cover `initialize`, `events/list`, the `Mcp-Name`
check and core methods still reaching mcp-go over HTTP, and the stdio
filter.
- Run against prod over stdio: polls returned real monitor alerts and
pattern anomalies (the anomalies with their patterns), and bad CQL in
`query` came back as `-32602`.
- Run with Canvas's own client (`src/server/connectors/events.ts` on the
MCP SDK) against this branch over HTTPS. It listed both types and polled
real events. Bad arguments came back as InvalidParams, an unknown type
as NotFound and subscribe as MethodNotFound, and it sent `Mcp-Name` on
every `events/*` request.
- `go test ./...`, `go vet` and `go mod tidy` pass. golangci-lint
reports nothing new; the 5 remaining issues are already on `main`.

Once this is tagged (v0.2.42), the edgedelta repo bumps to it and
switches admin to `Handler()`.
@tineira

tineira commented Nov 3, 2026

Copy link
Copy Markdown

A user-present case for Open Question 1 (how events reach the model)

I maintain Agent Pointed Context (APC, paper). A person drags a mark onto something in a web app (a bar in a chart, a customer row), and their own MCP agent gets what they pointed at. Nobody shares a screen.

APC is the opposite of the case that motivates this SEP: here the user is present, watching, and expects the agent to react within seconds. That makes it a useful data point for Open Question 1, and for what the SEP leaves to the client under "Delivering events to the model".

What we do today, and where it breaks. A pin is rare and already minimal, in the spirit of "Payload minimality": a kind, an id, and the tool that reads it (next, next_args), never the data itself. We deliver it in two ways, and the model drives both:

  • an attention key on the agent's next tool result, which only helps if the agent happens to call another tool;
  • a long-poll tool, wait_for_focus (25 s by default, 50 s at most), which the agent is told to call after it answers.

What we see in practice:

  1. The first pin always comes through chat: the person says "look at what I'm pointing at" and the agent calls get_current_view.
  2. Once the agent answers it goes idle, so there is no tool result for a pin to ride on.
  3. wait_for_focus is where it fails. Some agents never call it, and the ones that do run two or three rounds and then stop. From then on a new pin reaches nobody until the person types again.

So a long-poll that the model holds is not a reliable way to deliver events. This comes from daily use across agents and is not yet a measurement.

Where Events fits. events/stream moves the wait from the model to the client, which is exactly the fix. An APC server would offer one event type (say apc.focus_changed, whose payload is the pin) and drop wait_for_focus for clients that subscribe. Push mode is enough, since the person and the client are both online.

What is still missing (Open Question 1). The transport alone does not close the gap. If the client receives the pin but the model stays idle, nothing changes for the person. For events with the user present, the useful behaviour is to put the event into the active conversation and start a turn within a second or two, not to queue it or start a background run. If the SEP adds non-normative guidance for Open Question 1, I'd suggest it names this case next to the background one. A per-event-type hint from the server might help the client choose (something like "audience": "interactive" | "background"), though I'm not sure it belongs in this revision. The Claude Code channels comment above shows a host can already inject pushed events into a live session, and SEP-2495 looks like the place for the re-entry step itself.

Happy to port APC's open-source reference implementation to events/stream as a test server and client once an SDK implementation lands, and to report the latency from pin to event to model turn back to the WG.

@poita

poita commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

A standard place in _meta for the cause of an event

...

Thanks for the response @yaotsakpo . I think this is a good thing to solve, but perhaps doesn't belong specifically in Events: provenance is something that should be solved in MCP as a whole (e.g. a tool results that returns an email) - so probably there's a broader solution here for defining provenance metadata that isn't events-specific.

Probably worth a discussions with the auth group and could be its own SEP that events can adopt?

Comment on lines +210 to +213
#### Event type removal and breaking changes

When a server stops offering an event type, or changes its `inputSchema` or `payloadSchema` incompatibly in place (contrary to _Schema evolution_ above), existing subscriptions to that name no longer hold a valid contract. The server SHOULD end them with each mode's termination signal (see [Subscription Termination](#subscription-termination)): `notifications/events/terminated` on push streams, and a `terminated` envelope to webhook subscriptions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if it is removed between events/list and the subscription command? What would the client receive? I assume just an error that the event doesn't exist.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, same as calling a non-existent tool in that casae.

Comment on lines +186 to +188
- Existing fields SHOULD NOT be removed, renamed or retyped.
- Enums SHOULD NOT be narrowed.
- `inputSchema` SHOULD NOT be tightened so that previously accepted `arguments` become invalid. A webhook refresh re-sends them, and would fail with `InvalidParams`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is SHOULD good enough here or should this be a MUST?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think MUST is too strong, and unenforceable. We can encourage people to be backwards compatible but can't force it.

- Enums SHOULD NOT be narrowed.
- `inputSchema` SHOULD NOT be tightened so that previously accepted `arguments` become invalid. A webhook refresh re-sends them, and would fail with `InvalidParams`.

A breaking change SHOULD instead be published under a new event name and served alongside the old one for a migration period, after which the old name is removed (see [Event type removal and breaking changes](#event-type-removal-and-breaking-changes)).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, i feel we might be better of with a MUST

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that it's unenforceable and e.g. if a server has known usage in a controlled environment then it seems mostly harmless to have the option of backwards incompatible schema changes. We could put "MUST" here, but in practice people will change schemas and clients will need to handle this. Having a MUST for something that isn't guaranteed I think would just lead to fragile clients.

In general, I do think we need a better approach to schema versioning (and discussed this in the WG), but the sentiment was that we should probably solve this for all MCP primitives rather than trying to solve for Events on its own.

Comment on lines +267 to +268
"maxAgeMs": 300000, // optional; do not replay events older than this many milliseconds (see Cursor Lifecycle)
"maxEvents": 50, // optional; cap events returned

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these have max and min values and default values? I want to make sure there are some guarantees server authors can rely on and don't potentially have to stream 1M events and queue events forever. A max bound seems reasonable to me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Servers set their own maximums. We aren't mandating a max at the protocol level. The default is unbounded, but expect that server will have its own bound based on how much it queues.

It probably does make sense it introduce a default for this (and maxAgeMs) so that default behaviour is good. It seems other similar APIs (Kafka etc.) do have defaults. Will discuss with WG and update.

@poita poita added the accepted SEP accepted by core maintainers, but still requires final wording and reference implementation. label Nov 11, 2026
@poita

poita commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

The Core Maintainers voted on this SEP and it is now Accepted.

We'll begin the work to finalise and get this merged.

@poita
poita requested review from a team as code owners November 11, 2026 19:56
@poita poita changed the title SEP-3415: Events Extension sep(SEP-3415): Events Extension Nov 11, 2026
@mintlify

mintlify Bot commented Nov 11, 2026

Copy link
Copy Markdown

Preview approval required

This pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit f77a5fe.

Approve preview

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accepted SEP accepted by core maintainers, but still requires final wording and reference implementation. extension SEP triggers-event-wg

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

10 participants