By Daniel Shapiro
Published
To work alongside humans effectively, agents need to know what changed and who changed it in real-time. We built that for them with Pi and MCP Events.
Jump to section
An agent is restocking an online store. It has product descriptions to update and orders to place. Then a human opens one of those products in the CMS and starts typing. Without a way to know someone is there, the agent would write right over them. This one notices, waits until they’re done, and picks up where it left off.
That’s MCP Events, a draft extension to the Model Context Protocol, streaming content changes and presence from Sanity to Pi, an open-source agent harness. No MCP client supported streaming out of the box, so we built it. The rest of this post covers how we got there, what broke, and what we think the spec still needs.
Until recently, agents interacted with Model Context Protocol (MCP) servers (aka “connectors”) by calling a tool to do something, waiting for the answer, and moving on based on whatever feedback it got. When data shifts out from under agents’ feet—a price changes, a build errors out, a teammate starts to edit the same CMS field—they only find out on the next tool call. Or not at all.
The MCP Events specification (currently in draft) plugs this gap by giving agents access to information on-demand.
What is MCP Events?
When loop engineering took over where prompt engineering left off, agents were still alternating question-answer via user-assistant turns. With MCP Events, agents don’t have to ask “what’s new”, the client/harness watches for changes and lets the agent proactively know when something happens.
The Events Extension proposal (SEP-3415, still a draft) describes how to make this happen via new events/* capabilities built on MCP 2.0 (protocol revision 2026-07-28).
A server can tell agents what events it will emit. If the client supports it, they can subscribe to these events and get continuous updates through three tried-and-true delivery modes:
- Polling, where clients refetch data periodically
- Webhooks, where clients register a callback destination
- Streaming, where clients open a long-lived connection and receive events as they happen
Depending on your use case, each option has its benefits. If your agents primarily make decisions based on lagging indicators that don’t require immediate action, it’s not the end of the world if they discover them eventually. On the other hand, anyone who’s authored content with other people in real-time knows that things get gnarly fast if the editing experience doesn’t give continuous, up-to-date feedback.
We’re in the business of real-time content collaboration
At Sanity, we have spent almost 10 years building a platform for real-time content collaboration across UI, APIs, MCP, agents, and services. We have customers who publish content that has huge impact on the news cycle, consumers, and the market. So naturally, MCP Events caught our eye and we’re pretty stoked about it!
If your product already has a live change feed, a change history, outbound webhooks, or event-triggered functions, and you’ve got an MCP Server (or are building one!), MCP Events can help connect agents to those sources.
For Sanity, we could draw it up like this:
- Polling maps to History API, where an agent can ask for everything that changed since the last change it saw and catch up after being offline.
- Webhooks maps to a document-triggered Function or webhook, which can post to the agent's callback URL when something changes
- Streaming maps to Listening API, where agents subscribe to a document change stream.
All of these are grounded in data changes, but collaboration is more than that. It’s also about who’s currently working on the thing™.
Knowing who is in the room
Take Sanity Studio. It streams content changes in real time, while presence avatars let authors know when they’re working on structured content at the same time as their colleagues (and their agents). Those visual cues give authors a heads up to steer clear of fields or sections that someone else is working on.
In early August, Sanity’s App SDK team added support for presence in both directions, which got my Agent and Developer Experience teammates and me thinking:
What if agents knew other authors were actively working on something and… waited for them?
How fast is fast enough?
OpenAI’s MCP Events plugin provided an awesome opportunity to try connecting MCP Events to a Sanity data source. Currently, it supports webhooks deliveries to work chats on ChatGPT web, in the ChatGPT desktop app with Cloud selected, and in Dots.
We set up a ChatGPT tunnel connecting a local Sanity MCP server instance to an MCP Events plugin trigger listening to sanity.document.changed events. Then we created a personal shopper ChatGPT monitor, or “watcher”, and had it keep tabs on product inventory in a Sanity dataset, updating recommendations to match our budget as prices and availability changed.
It felt magical to see agent turns kicked off without a prompt in between! Things worked just as advertised, though with a bit more latency than our content collaboration use case could support.
Our MCP server handed events to ChatGPT quickly: all 11 measured deliveries were received by ChatGPT within ~2.5 seconds of event trigger to ack (median 2.16 seconds)ack’ed our incoming event, and in one trial an update still hadn’t arrived after 5 minutes, and looks like it never did.
Stage | Observed range | What it told us |
|---|---|---|
Content change to ChatGPT event | 1.74-2.57 seconds | Event delivery is reliably quick |
Event | 13.5-100.7 seconds (seven trials) | Event ingestion takes a bit longer |
Visible response to content change | Seen in six of seven trials. One remained absent after 312 seconds | Successful event delivery didn’t always produce a response |
Past the point our server sent an event to ChatGPT, we lost a bit of visibility into what happened next, and delivery timetables varied significantly. For information that can tolerate some staleness, that’s a great fit. For an agent jumping into a document that someone else is editing, it’s the difference between waiting (perhaps too politely, as we later found) and potentially overwriting their work.
Stream of collaborative-ness
MCP Events: streaming edition seemed like the next best option for us. Conveniently, the server-side change required to support MCP Events streaming was something we’d already planned to tackle. Bumping Sanity’s MCP SDK version from 1 to 2 required surprisingly few changes to our MCP server. Kudos to the spec authors!
But as bleeding edge as the ChatGPT client is on MCP Events, it hadn’t implemented streaming support, and maybe it never will
Implementing MCP Events in Pi
There are tons of awesome open-source agent harnesses out there: I was personally looking forward to getting to know Pi after hearing great things from Sanity’s engineering community. Pi turned out to be a perfect fit for piloting the full MCP Events spec!
If you want to try this yourself, please check out @sanity-labs/pi-mcp-events, a Pi extension that supports MCP Events polling, streaming, and webhook delivery. Install it from npm:
pi install npm:@sanity-labs/pi-mcp-eventsAs very few MCP servers have implemented MCP Events to date, we included an MCP Events-enabled reference server in the Pi extension’s source repo that publishes weather updates. Clone the repo, start it with npm run dev:weather, and point Pi at it with a config like the one below. If your session is already under way, run /reload to see events start rolling in:
{
"servers": {
"weather": {"transport": "http", "url": "http://127.0.0.1:3015/mcp"}
},
"watches": [
{"id": "readings", "server": "weather", "name": "weather.reading", "mode": "push"},
{"id": "alerts", "server": "weather", "name": "weather.alert", "mode": "poll"}
]
}Agents love feedback, and our reference server allowed them to quickly hill-climb their way to a stable MCP Events implementation, which we ported into our local Sanity MCP server 2.0 prototype.
For upstream data, we subscribed new Pi client connections to Sanity’s Listening API in transaction modesanity.document.changed events for streamed delivery in the same way we’d enabled them for our ChatGPT tunnel’s webhooks-based delivery.
For this initial spike, we landed on best-effort events delivery: missed source changes wouldn’t be replayed. We stored subscription state in Redis, and used in-memory queues for live event delivery.
Pi needed to handle the usual deduplication and ordering snags this sort of system sees. For that, we added a filesystem-backed inbox to preserve events that Pi had received but not yet processed and made available to the agent runtime. Pi supports ephemeral conversations with --no-session, so we made sure our inbox remained in memory and disappeared at the end of one.
We gave Pi a /mcp-events slash command to help configure, start, stop, and resume a unique combination of watcher name, event type, and delivery mode (see “watches” in the mcp-events.json example above). This made it a bit easier for us to see and debug what was going on.
With this setup, Pi harness was ready to receive document update notifications and respond to content changes in real-time. Sweet!
Now it was time to tackle the other side of the equation: Sanity Studio, the “CMS“ part of Sanity. It would be cool if we could see Pi working alongside us, and give it the same awareness of what we were up to.
Exploring Agents in Studio via Presence
Sanity Studio advertises authors whereabouts to each other via WebSockets that power Presence avatars (the little chips with your initials or photo in it). When someone joins a document, a “roll call” asks everyone else to announce their presence. This gives newcomers a live view of who’s around and what they’re doing. We set our sights on a Studio prototype to prove this out!
We decided to keep agents “invisible” when reading, only having them trigger roll calls and announce their presence when they were ready to start editing. This kept presence-related traffic to the service that manages those socket connections from seeing a surge of chatty agent behavior.
We then connected MCP session identities to Studio avatars. Agents popped into frame immediately! It was pretty exciting to see their avatars listed alongside other authors.
Making agents less timid
Initially, we introduced support for agent presence at document scope through joining and leaving MCP tools. While that helped keep roll calls down, it didn’t give agents enough visibility into document edits: they needed to know which piece of content authors were working on to wait effectively. At document scope, agents were too timid, waiting any time anyone (and any agent) was active on any document field.
We pivoted to field-level presence by adding a way of declaring “this is the field my agent wants to update.” This gave us the right balance of agent awareness and productivity. Our events-enabled Sanity MCP server prototype now advertised three new tools:
document_presence_join: announce agent presence and intended edits.document_presence_update: change intended edit scope without leaving the document.document_presence_leave: remove agent presence.
We placed a small [Agent] badge directly below each agent’s human presence avatar. This gave agents a sort of “delegate” appearance, and made it clear an agent was acting on behalf of someone.
We’re big fans of tying agents back to human ownership and accountability, especially in content operations. Agents aren’t independent authors: as we’ve decided internally at Sanity, we own every word an agent writes on our behalf. They might as well use our avatars too.
Putting MCP Events to work: Pi runs a storefront
We were ready to bring everything we’d built together—Sanity Studio with agent presence announced on edits, bidirectional awareness of who’s editing what via MCP Events — and put it to use in a practical content ops setting. We gave Pi a job managing inventory for “Aperture”, a fictional e-commerce store.
Pi was responsible for making good restocking decisions.
Watch restock demand. When we hit a product's "we need more" threshold, order it.
Aperture shoppers could request sold-out sizes through a pre-order button. Each request increased a demand count tracked in an inventoryPlanning document in Sanity. When shopper intent hit a predetermined request threshold that means “buy more of these, people are clamoring for them!”, Pi restocked the order by creating an inventory tracking document.
Video demo
To prove out the “pause while humans are authoring what you want to edit” scenario, a human Sanity Studio collaborator (me) dropped into various documents to change product descriptions while Pi had in-flight edits queued up. It identified me and paused until I’d stopped for a bit, then picked up where it left off.
What we learned along the way
Events eat up context fast
This one seems self-explanatory, but by giving agents access to on-demand information delivery, they become super token hungry. Manage your events subscriptions carefully: prefer event payloads with minimal data, and encourage agents to refetch updates in batches rather than handling messages at a 1:1 event-to-response cadence.
If you’re building MCP Events client capability into your existing harness, prefer to wake your agent for actionable signals only.
As we found out, not every message needs a turn.
To collaborate effectively, agents need lots of signal
Initially, we focused on minimizing presence activity (leaving, joining, roll calls) to keep agents from generating excess upstream service traffic. It ended up making more sense to scope presence announcements to the kinds of activity that mattered for agent-human co-visibility (writes, not reads) and let agents operate with field-level awareness.
Subscription lifecycle management is tricky
As we hardened our Pi extension’s webhooks-based delivery subscription capability, we ran into subtle timing issues that required a bit of additional coordination server-side to ensure each worker had a viable upstream data source listener attached before clients got the “ready to receive!” ack.
The MCP Events draft spec might benefit from a bit of additional guidance here: we’d recommend something like “servers SHOULD wait for source subscriptions to become active before returning success from events/subscribe.”
A subscription count wasn’t enough
Our first version of the Pi extension showed how many watches were running and offered a way to stop them. That left us guessing when sources went quiet: was the watch still active? Was it retrying, had it stopped entirely?
We built that visibility into our extension via /mcp-events status, and we think other MCP Events adopters would benefit from a shared vision of watcher metadata, and its client representation. We’re excited to push our interface towards further transparency by surfacing delivery failures and replay gaps, too.
Events need somewhere to wait
Our Pi implementation lets agents receive events in the background without interrupting their current turn. We decided to write them to a local inbox before advancing a resume cursor, so pending events remained accessible across restarts (--no-session is the exception here: those events evaporate when a conversation ends).
The current MCP Events draft leaves this up to clients, which is totally reasonable from a flexibility perspective, but might lead to variable experiences for MCP Events consumers. There’s several ways to handle wait timing and unprocessed events. For events sources that don’t support replayability, stronger guidance could be useful here.
So, is streaming worth the cost?
We’re aligned with the MCP Events draft authors on this one: webhooks are the right choice for non real-time dependent features. If an event supports all three delivery modes, and the client’s advertised a callback URL, pick webhooks every time.
But it’s not a one-size-fits-all solution: there’s a barrier to entry. You need a machine that safely exposes a callback destination, and that’s tricky to manage especially for non-technical folks and the IT teams that support them. Agents are often spun up in sandboxes with limited network egress/ingress, further complicating things.
When you zoom out and think about how agents respond to MCP Events, there’s really two cost factors at play:
- Tokens spent consuming events, reasoning over next steps, and firing off additional tool calls
- Infrastructure required to support reliable events delivery
Streamed events means steady-state server load, which carries operational risk.
Streaming also means handling interruptions: dropped connections, subscription restoration. If you can support webhook delivery with reasonably tight SLAs, they’re the right choice.
Client cost as a function of tokens spent responding to events is comparable for both delivery modes, and it’s worth providing rough numbers here. The demos we spun up cost about ~200-250 tokens per agent “wake up” call and model turn, and anywhere from 2000-5000 tokens in the most expensive scenarios involving agent-human edit collisions. Those observations pushed us to optimize away signal that shouldn’t trigger a model turn: things like liveness checks and subscription updates that didn’t require model visibility.
My sense, if you’re launching MCP Events, you’re promising your customers a pretty amazing opportunity to give always-on agents capabilities that usually require a dedicated platform team. At Sanity, we like doing hard things to make our customers lives’ as easy as possible. Polling is perfectly fine for non-latency-sensitive features and data with slow rate-of-change. Webhooks fit the bill for almost everything else.
But for our agent-human collaboration scenario and our customers’ wide range of agent harnesses, streaming was a natural fit and fun deep dive.
Give MCP Events a spin!
We’re making @sanity-labs/pi-mcp-events available today on npm and GitHub. We hope this reference implementation will help support the MCP Events proposal as it moves toward canonical SDK capability, and give folks a playground to explore events-driven agent sessions.
We built with Sanity product experiences in mind. If you’re building event-driven agents, we’d love to compare notes! We’re following the evolution of MCP Events SEP-3415 closely. Try out what we’re advocating here, question our choices, tell us where you’d take things next.
More from Engineering
Open roles
Subscribe and get the next issue straight in your inbox
By submitting this form, you confirm that you have read and understood Sanity's Privacy Policy.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.