Repository navigation
Conversation
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.
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.
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.
|
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 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. |
|
A standard place in An I'd suggest one optional, reserved // 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, What the text would say:
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. |
) ## 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()`.
|
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 (
What we see in practice:
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. 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 Happy to port APC's open-source reference implementation to |
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? |
| #### 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. | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yeah, same as calling a non-existent tool in that casae.
| - 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`. |
There was a problem hiding this comment.
Is SHOULD good enough here or should this be a MUST?
There was a problem hiding this comment.
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)). |
There was a problem hiding this comment.
Again, i feel we might be better of with a MUST
There was a problem hiding this comment.
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.
| "maxAgeMs": 300000, // optional; do not replay events older than this many milliseconds (see Cursor Lifecycle) | ||
| "maxEvents": 50, // optional; cap events returned |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMsZi9wRHH7j882xfkqw3y
|
The Core Maintainers voted on this SEP and it is now Accepted. We'll begin the work to finalise and get this merged. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMsZi9wRHH7j882xfkqw3y
Preview approval requiredThis pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit |
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.events/list: a name, aninputSchemafor subscription arguments, apayloadSchema, and the delivery modes it supports.events/poll), push (a long-livedevents/stream), and webhook (events/subscribe, signed per Standard Webhooks, with a refreshed TTL).truncated: truesignals 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-28protocol, the capability moves underextensions,list_changedrides on asubscriptions/listenfilter field, and results carryresultType.For reviewers:
-32023to-32027) are provisional.InvalidParams.Verified with
prettier --check, and every in-page link resolves to a heading. The file keeps its0000-name, sorender-sepsskips 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.