Skip to content

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
  1. What is MCP Events?
  2. We’re in the business of real-time content collaboration
  3. How fast is fast enough?
  4. Stream of collaborative-ness
  5. Exploring Agents in Studio via Presence
  6. Putting MCP Events to work: Pi runs a storefront
  7. What we learned along the way
  8. So, is streaming worth the cost?
  9. Give MCP Events a spin!

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.

Demo of real-time MCP Events with Pi and Sanity Studio

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.

Very meta, I know.
Web page content featuring the headline 'Knowing who is in the room' and a comment bubble with 'Daniel Shapiro'.

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.

A ChatGPT watcher receiving MCP events from the Aperture ecommerce store.
A ChatGPT conversation where a personal shopper monitor reports that a blouse price rose from $148 to $248, pushing an outfit deal over budget, and later reports it dropped back under budget.

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)[1]. The larger, less predictable delay came after that. Across seven trials, the agent’s next turn occurred 13.5 to 104.7 seconds after ChatGPT had 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 ack

1.74-2.57 seconds

Event delivery is reliably quick

Event ack to agent’s next document read

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[2]. We decided to take “you can just build things” to heart, and wired up an MCP client to support the full MCP Events draft spec.

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-events

As 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"}
  ]
}
Weather events emitted by the MCP server, received by Pi.
Two terminals side by side. Left: the weather station server emitting readings and storm alerts for Harbor and Hilltop. Right: Pi with pi-mcp-events receiving the events and announcing a storm warning as a radio presenter.

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 mode[3], which shaved about a second off data delivery from Sanity’s Content Lake to our MCP server (842-1,132ms over a handful of observations). With this subscription model in place, we wired up sanity.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.

An agent-badged presence avatar alongside a human author.
A Price field in Sanity Studio with two presence avatars: one labeled Agent and one for the human author.

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.

Acknowledgements

Thanks to Rune Botten for pushing us toward transaction-level listening and agent presence in Studio, Espen Hovlandsdal for the light-bulb moment that led to agent field-level awareness, to Knut Melvaer for editorial support, and to David Annez for asking “have we looked at MCP events?” in the first place.


References

  1. [1]We ran Sanity’s MCP server locally, exposed to ChatGPT through a tunnel, and timed 11 event deliveries. In seven of them we also watched for the agent’s response, measured from outside ChatGPT: the time from ChatGPT’s acknowledgement until the agent called back to our server to read the document. We can’t see inside ChatGPT, so treat these measurements as one setup and a handful of trials. It’s not a benchmark.
  2. [2]We reached out to Max Stoiber who works on a lot of the MCP stuff at OpenAI, and he pointed out that it might be too expensive to support for them. Which is reasonable given their scale and traffic.
  3. [3]This mode emits mutation events on the Content Lake as they happen, which means that there is no guarantee that the updates it describes have been indexed and are queryable, but that doesn’t matter for this use case.

Published:


Daniel Shapiro

Daniel Shapiro

Senior Software Engineer, Agent and Developer Experience (ADX)

Sanity

More from Engineering

cd ..

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.