Inspiration
I kept seeing the same demo of "agentic BI": a headless MCP server that
spits out a chart, or a vision model squinting at a screenshot to figure
out where to click. Neither one understands the chart it's looking at, and
neither lets a person and an agent actually work on the same dashboard
together. WebMCP's document.modelContext.registerTool offers a different
answer — the page exposes its own tools, the tab itself becomes the tool
surface, nothing in between. I built Vivid to see if that holds up on
something real: a six-panel B2B SaaS revenue dashboard, not a toy chart.
What it does
Vivid is a live analytics workspace with two dashboards — a six-panel revenue report (Northbeam) and a product-usage report (Activity OS) — that an agent can read and edit right in the same tab a person is looking at. No MCP server behind it, no model reading screenshots.
The agent can pull live report context, set filters (segment, region, plan tier, channel, contract type, account), and reshape specific charts. ARR bridge and NRR/churn retention take a validated patch to their underlying state — never a raw spec. ARR mix, top accounts, and net-new logos only let the agent swap between a couple of approved presentations, like donut vs. bar.
A semantic layer — Cube Cloud over 7 real Postgres tables — grounds open-ended questions ("MRR by region") in real metric definitions instead of guessed column names.
And it's genuinely shared: every edit, person or agent, goes through the same validation, persists to a room, syncs live to everyone on that room's link, can be undone, and lands in one activity log with both actors in it.
How I built it
Vite, React, TypeScript. Two report surfaces, each with its own filter state and its own WebMCP tools, registered only while that tab is open.
The two agent-editable charts (ARR bridge, retention) are Vega-Lite specs
rendered via vega-embed, but the spec is derived from a small "knob"
object, never written directly. Everything else is plain SVG/CSS. A
lighter contract (set_report_chart_contract) later opened up three more
panels — ARR mix, top accounts, net-new logos — to an allow-listed
presentation swap, still no direct spec access.
The tool layer is document.modelContext.registerTool calls made from the
page: read-only (get_report_context, list_report_options,
find_field_values) and mutating (update_chart_spec,
set_report_filters), each validating input before touching state. Every
call returns { ok: true, data } or { ok: false, reason, error }.
Persistence is one JSON snapshot per room, mutated only through a Supabase Edge Function and a Postgres RPC that checks a capability digest, compare-and-swaps the version, and writes state plus an activity-log row in one transaction. RLS blocks direct table writes — no shortcut around validation.
The semantic layer is Cube Cloud over those same 7 tables, reached only
through an Edge Function proxy — the API token never touches the browser.
Two more tools, get_business_definitions and query_business_metric,
answer questions unrelated to either report's charts.
Challenges I ran into
document.modelContext is a draft API — I didn't have a browser that ships
it. So I injected a minimal registerTool polyfill through devtools and
added a re-entry point (window.__vividRegisterTools()) to still exercise
the real execute functions end to end: discovery, validation,
re-render, persistence, activity log. Not a live agent, but as close as I
could get without one.
Making "never dual-axis" actually true took longer than expected. Telling an agent not to do something is easy; making the schema incapable of expressing the bad state is harder. I kept finding invariants that were still just conventions until I pushed them into the validator.
Adversarial input was its own problem once I accepted an agent writing state needs the same suspicion as a hostile user — oversized values, SQL-looking field names, injection attempts, spoofed capabilities, UUID reuse. A dedicated test suite covers all of it; the rule was every one has to fail cleanly, never a crash or a silent partial write.
Keeping Revenue and Product Usage independent without duplicating everything was a constant judgment call — separate filters and tool sets, but one shared wire protocol and undo pattern underneath both.
Accomplishments that I'm proud of
- A full WebMCP tool contract — discovery, reads, validated writes, real error reasons — running end to end with nothing mocked, verified without a shipping WebMCP browser in hand.
- Invariants that hold because the validator won't let them break, not because a tool description asked nicely.
- An adversarial security pass that throws real hostile input at every mutation path and degrades cleanly every time — no crashes, no silent writes.
- Multiplayer that isn't faked for the demo: agent and person edits share one path, one sync, one log.
- A semantic layer with zero shared imports back into the chart code — it'd survive being lifted into a different app.
- Something live at vividdata.pages.dev a judge can actually click through.
What I learned
Chart-as-data instead of chart-as-code is what makes this trustworthy. I could've let the agent send a raw Vega-Lite spec — I didn't. Every editable chart runs off a small typed object, and the spec is derived from it. That's the decision that made invariants real instead of aspirational.
MCP and WebMCP aren't rivals — they're two stages of the same job. MCP gets you a fast first draft: a headless server hands an agent the data, it produces something in one shot. But getting a draft right after that is visual and iterative — "make the bars wider," "try it as a donut," "now filter to EMEA" — and a headless server has nothing on screen to see or touch for that. WebMCP picks up right there.
A semantic layer changed what "grounded" meant in practice — real metric definitions instead of guessed column names made the gap between a correct answer and a confident wrong one a lot more visible.
Multiplayer forced honesty about state. Once edits sync live into a shared log, you can't hand-wave version tracking — it's a real compare-and-swap, or two edits eventually stomp on each other.
What's next for Vivid
Right now this proves the idea on one dashboard. I want chart-as-data to stop being specific to Northbeam and become a pattern other people's dashboards use.
That means pulling the governed-presentation contract and the knob-based chart state out of this codebase into something closer to a spec — small and documented enough that any team could implement it over their own charts without re-deriving these invariants from scratch.
I also want Vivid to stop being a demo of one dashboard and become a place people actually work. Connect Data already proves the generic version — point at any Postgres table, get an agent-editable chart through a validated contract — I want that to be the default onboarding, not a hidden tab.
The semantic layer should scale past one Cube deployment over 7 tables — the same shared-key discipline tying a chart's knobs to its metric definition could stretch across multiple warehouses.
I like the idea of more than one agent in a room at once — a drafting agent proposing something, a review agent checking it against governed invariants, a person steering both — on the same activity log and undo stack that already treats a person and an agent as peers.
Before any of that touches real data: real tenancy. The bearer-link model is deliberately bare-bones; authenticated rooms and real capability revocation are what turn this from a convincing prototype into something a company could point at its own numbers.
And longer term — every BI vendor is racing to ship its own locked-in MCP server. My bet runs the other way: WebMCP means any agent, not just the vendor's own, can reach into a live chart. That only matters if more than one surface adopts the pattern, which is why I want to pull it out into something reusable instead of leaving it bolted to this one app.
Built With
- cube-cloud
- deno
- mcp
- model-context-protocol
- oxlint
- postgresql
- react
- semantic-layer
- supabase
- supabase-edge-functions
- supabase-realtime
- typescript
- vega
- vega-embed
- vega-lite
- vite
- webmcp
Log in or sign up for Devpost to join the conversation.