Inspiration
Most WebMCP demos will be one site that registers a few tools. That is useful. It is also the obvious move.
I wanted the harder surface: several real apps that stay themselves, each with its own origin and document, and one workspace where a person and an agent share those live apps. I built it so that Root does not sneak around the iframe and hit the child APIs itself.
Cross-app work is what people already struggle with. Copy an id. Retype an email. Open the next tab. Hope the page still matches the plan. Agents are already strong at planning. What they cannot do, unless the sites hand them tools, is act on those live pages as part of one governed run.
That is what Root is for: an orchestrator between Catalog, Customers, and Cases. Each app keeps its own tools on its own document. Root discovers and invokes them there, and the UI on that page is the evidence.
What it does
Root is a Mac-style desktop in the browser. Catalog, Customers, and Cases open as real provider windows. Lab can join after a human grant. Each provider is its own deployed site. Root loads it in an iframe with allow="tools" and only talks to tools that document exposes to Root.
A person and an agent can run a pass across those windows:
- Search customers, products, or cases on the live lists
- Pause for a human row pick when there is a real choice
- Bind a later step to that selected snapshot (Root carries the object; it does not flatten Ada into a retyped email string)
- Open a record, or fill a create form and wait for a human persist on stage
- Keep other provider windows live instead of destroying them when work moves
- Notify when a wait needs you: after you grant it in inspect Settings, a pending-on-human pause plus a hidden Root tab shows a Notifications API toast (not Push, and not while Root is visible)
Root’s own page registers the gateway the agent calls: list_providers, discover_capabilities, prepare_workflow, execute_workflow, invoke_granted_tool, plus inspect, cancel, and window chrome. Prepare freezes an allowlisted graph before anything runs. Execute opens or reuses each window, rediscovers, revalidates, and invokes on the live document. Take control stops the agent on that window. Custom tools never enter pass evidence.
Try it: root.danieloyerinde.com with Customers, Catalog, Cases, and Lab.
How I built it
Four independently deployed Next apps (Root, Accounts, Shop, Support) plus Lab, one Express/tRPC API, and Postgres with tables owned by domain. No cross-domain joins. Support keeps its own copies of customer and order text. Root never mutates child domains through tRPC.
On WebMCP:
- Each child registers tools on
document.modelContextand exposes them only to Root’s origin - Root’s
Permissions-Policydelegatestoolsinto those iframes - Discovery uses
getTools({ fromOrigins }); invoke usesexecuteToolin the child’s context - Root is the author-provided agent that composes nested provider tools; the agent talks to Root’s gateway, and Root invokes tools inside each iframe
The workflow layer is prepare then execute. Search, select, open, and create are allowlisted. A later argument may be a bind to an earlier selected snapshot. Create writes only after a human persist on stage. Agent lease is per window so one document serializes while another stays free. Rich fill, preview, and pick presentation run on stage on the live controls, not as a fake overlay of the whole desktop.
Challenges I ran into
The first hard truth was uniqueness. Multi-tab agents can already visit three sites. “Root opens three iframes” is not enough. The product only wins if the person and the agent share the same live documents under a freeze the agent cannot quietly rewrite.
A core challenge was ChatGPT’s in-app browser. It only exposed top-level tools on Root. It did not discover tools registered inside provider iframes. That is exactly where Catalog, Customers, and Cases live. That gap is what pushed me onto a WebMCP-capable Chrome setup (testing flag / extension path) so nested getTools and executeTool could actually run against those documents.
One stage window that remounted and killed the previous app made collaboration impossible. Search results and selections vanished when the next step switched providers. I moved to persistent independent windows so Customers can stay while Catalog runs.
Binding looked simple until it was not. Typing Ada’s email into Cases again is not a handoff. Root has to deliver the selected snapshot intact and refuse graphs that invent ids from search results instead of an explicit selection.
Human pauses were another edge. A pending pick, a create persist, Take control, and a silent timeout are different outcomes. Take control must fail closed as stopped by the user with no retry. A timeout with no gesture is not the same thing.
Accomplishments that I'm proud of
I shipped a real desktop, not a slide. Multiple provider windows stay live. Prepare refuses illegal tools and arguments. Execute rediscovers and fails closed on drift.
The together loop is concrete: the agent fills a search, the person clicks Lin or Silent Mouse on the live list, Root binds that snapshot into a later Cases step without retyping her email, and Take control can stop the run on that window.
Domain data lives in Postgres with SQL search. The iframe is not a picture over Root’s own API. Custom providers can still join behind a human grant without entering the typed pass.
What I learned
WebMCP tools belong to the open document. If Root calls the child’s API itself, the iframe is only decoration.
An agent can run searches it already knows without the person. That is not much of a shared loop. Selection, bind, and create-after-persist are the parts where the person actually has to act on the live page.
One allowlist scales cleaner than a prepare function per app. The step cap is only a safety limit. It is not tied to how many providers exist. Legal tools can repeat. Illegal ones still fail.
Remounting the only window on every step wiped the previous app’s UI. Keeping separate windows open, with a lease per window, matched how people already keep a few apps around.
What's next for Root
As WebMCP leaves experiment and settles into real browser conventions, I expect clearer nesting rules, better host and agent tooling, and shared patterns for selection, grants, and multi-document work. Root will get simpler as those conventions land.
Conclusion
As of today, this principle gets shaky towards my final stages, after a final extensive check. A few things met me head-on. The agent can bypass Root. Even with a custom-tool setup where Root would in fact reject invoke when the tool is not granted, the agent could still go around me, because the underlying iframe exposes tools to the parent origin. That path is not quite spelled out in the docs the way you’d hope. Those tools would not show up when you list Root’s gateway tools, but Chrome still exposes them by name (direct invocation), so there is a direct bypass between Chrome and the providers.
As WebMCP moves off experiment, the goal is that we get to conventions where this kind of multitasking is better supported, and where the tooling around it is better too.
There is also usually a .map error that surfaces via the inspector. After extensive debugging, several bridges on my side were tested and still did not own the failure. The tools I had were limited, so I could not A/B the remaining layers cleanly (fresh tab vs stale session, inspector path vs gateway path, flag and extension in vs out) to confirm where it breaks. I treat that as outside Root: the experimental API or the extension.
Built With
- chatgpt
- codex
- cursor
- nextjs
- railway
- render
- typescript
- vercel
Log in or sign up for Devpost to join the conversation.