Inspiration

The real problem is that humans and agents don't have a place to actually work together on data. Ask ChatGPT to analyze something and it hands you a chart or a table inline; but that artifact is disposable. It lives in the scroll of one conversation, not anywhere sticky.

Close the chat, come back tomorrow with a follow-up, and you're re-asking the same questions from scratch instead of building on what was already figured out. Existing BI tools don't fix this either, they were built for a person driving a mouse, and none of them have caught up to a workflow where an agent is a first-class collaborator rather than a text generator bolted on the side.

Also most people isn’t good at data analysis, the tools has the data analysis knowledge built in, to write the best query, chose the suitable chart to tell the correct story.

I built SheetCanvas to be that place: a canvas where a human and an agent cooperate on data analysis in real time, locally, the state persists, it's inspectable, and it's shared, instead of the agent's output vanishing back into a chat transcript the moment the conversation ends.

I was building toward WebMCP specifically, before it existed. This tool is designed for an agent to drive the canvas directly, the same way a person does — and before document.modelContext was a real, shipped API, I got there with a workaround: a hand-rolled remote MCP server over a WebSocket, so an MCP-capable client like Claude Desktop could already act on the canvas. WebMCP isn't a feature I bolted on for this challenge; it's the primitive I was always building toward, and this challenge was the first chance to stop working around its absence.

How I built it

The reason this was cheap to build rather than a rewrite: SheetCanvas already had exactly one tool registry — 26 tools, defined once with zod schemas, covering everything from createSheet and setCells to chart creation and live connector queries — feeding a single executor. Three doors already used it: a person clicking around the UI, an in-app Copilot, and the WebSocket-based remote MCP server that had been standing in for WebMCP until it existed. WebMCP became a fourth door over that same registry — not a parallel build, but the door the other three had been waiting for.

Concretely:

  • document.modelContext.registerTool wraps every existing tool, converting its zod schema to JSON Schema with the identical call the backend already used, so the two doors can't quietly drift apart.
  • A state-aware gating layer decides which of the 26 tools are registered at any moment — no point advertising queryConnection before a connection exists — keyed on durable facts (does a sheet exist? a note? a connector?) rather than conversation shape, since a tool that vanishes between the agent listing tools and calling one is an unrecoverable error, not a retry.
  • A registration lifecycle manager reconciles the tool set on every relevant state change: diffed, coalesced, debounced, with one AbortController per registered tool (unregistering in WebMCP is only aborting a controller — there's no unregisterTool).
  • All four doors — clicks, Copilot, remote MCP, and now WebMCP — now route through one shared dispatch point, tagged by origin. That's what let me add a single activity trail that watches every tool call regardless of which door it came through, and a rewind control: "undo back to here," warning inline if it would also discard edits you made yourself in the meantime.
  • Safety was the design question I spent the most time on, because there is no confirm() for an agent to hit — Chrome's own docs still reference a requestUserInteraction() primitive that was removed from the spec before it ever shipped. So instead of a dialog, safety became three structural properties: every tool is correctly annotated (readOnlyHint, untrustedContentHint on anything returning warehouse- or CSV-authored text back to the model), every mutation is undoable, and every mutation is visible in the trail as it happens — plus ChatGPT's own confirmation on top.

Built With

  • webmcp
Share this project:

Updates