Inspiration

In September 2024 I moved to a new country, into an empty apartment I'd only seen in photos. A year later, almost to the week, I moved again. Different place, same problem. Both times it went like this: you stand in the middle of a bare room with a phone full of furniture tabs and no real sense of scale, you order things that don't quite fit, and you spend evenings pushing an imaginary couch around in your head.

It isn't just me. Every fall a wave of people do exactly this: new students, new arrivals, first apartments, furnishing a room they can only picture in fragments. What you want in that moment is simple: what do I need, how would it look, what will it cost, and once it's all in, is there any floor left to stand on?

That's a planning problem, and planning is exactly what an agent should be good at. But a chatbot can only describe your room back to you. It can't move anything in it. WebMCP changes that: a page can hand its own capabilities to an agent as tools. So I built the page worth handing over.

What it does

Roomwright is a 3D room planner that runs entirely in the browser. You draw a room, set the doors and windows, and place furniture from a catalogue of parametric pieces. Every item has real dimensions and required clearances. Orbit it, drop to a floor plan, resize or recolor anything, and it keeps a running shopping list with a total.

Every one of those actions is exposed to an agent as a WebMCP tool. There are 19 of them. So instead of clicking, you can say: "Studio apartment, 4.6 by 7 metres, door on the south wall, two windows on the east. Bed against the back wall, a sofa splitting off the living area, a small dining set by the window." The agent reads the tools the page offers and builds the whole thing: resizing the room, cutting the openings, furnishing three zones, running the planner's own clearance checks, catching its own overlaps, and fixing them. Then you take over: "turn the sofa toward the TV, lose that chair." Same room, same undo history, you and the agent trading turns on one canvas.

How WebMCP powers it

The key decision: the UI and the agent mutate the exact same state. There's a single store, and a button click and a tool call go through the same action. So undo/redo, autosave, shareable-URL persistence, and live layout validation all just work for the agent. There is no agent-specific code path for any of them.

Tools register against document.modelContext using the spec's AbortSignal lifecycle, in four groups:

  • Read: get_room, list_catalog, get_shopping_list
  • Build: add_item (resolves a product by id, name, or fragment, so the agent never has to guess), create_custom_item, duplicate_item (with an east-west / north-south mirror so "a chair on each side" is actually symmetric), remove_item
  • Arrange and check: move_item, rotate_item, resize_item, set_variant, set_room_dimensions, set_opening, remove_opening, suggest_spot (grids the room and returns candidate positions), check_layout (overlaps, blocked doors/windows, clearance failures, walkways under 0.75 m, narrowest gap)
  • Show: select_item (highlights a piece, opens its inspector), set_view, reset_layout

suggest_spot and check_layout matter most. They give the agent a feedback loop, not just setters. It places, checks, notices a conflict, and re-plans, visibly, in the activity log.

How I built it

  • React 19 + TypeScript + Vite
  • React Three Fiber + drei + @react-three/postprocessing for synthetic image-based lighting (no HDRI file), real shadow-mapped shadows, screen-space AO, and a restrained post pass
  • Parametric furniture: every piece is authored geometry keyed on its kind, not a glTF download, so the bundle stays small and every dimension is real
  • Zustand for the single shared store, Tailwind CSS v4 for the UI
  • Device-aware quality tiers (High / Medium / Low, auto-detected from the GPU string and core count, with a live framerate monitor that steps down) so it runs on a laptop, not just a workstation
  • Deployed on GitHub Pages

Challenges

  • The spec is still moving. Early references used navigator.modelContext; the current spec uses document.modelContext and has no unregisterTool. You unregister by aborting an AbortSignal. I probe both and use the signal lifecycle, with a short retry in case the host injects the API after first paint.
  • Shared post-processing broke drei's ContactShadows. EffectComposer sets gl.autoClear = false, and ContactShadows relies on autoClear to wipe its render target, so moving a piece smeared shadow trails across the floor. Fixed by dropping ContactShadows for a directional shadow map plus per-frame screen-space AO.
  • The agent kept guessing product ids. It saw sofa-halden and tried coffee-milo (the real id is table-milo). Fuzzy resolution plus returning the full catalogue on a miss fixed it in one step.
  • Symmetry. duplicate_item offset copies by a fixed diagonal, so "a lounge chair on each side" never mirrored. Added a real mirror across the room's centre line, and floor lamps now auto-aim at the room centre so a mirrored pair points the right way.
  • Testing WebMCP is hard today. Only ChatGPT's desktop in-app browser exposes document.modelContext (plus Chrome 149 behind a flag), and it's gated by account type.

What I learned

The leverage in WebMCP isn't the tool calls. It's the shared state. The moment the agent and the human mutate the same model, everything the app already does (undo, validation, persistence) becomes something the agent does too, for free. Design the state model well and the agent integration is almost nothing.

And tool descriptions are the API. Fuzzy product resolution, mirror options, and full-catalogue errors did more for reliability than any prompting. The agent behaves well when the tools let it.

What's next

Real glTF furniture and user uploads, photo-assisted room capture, L-shaped and multi-room layouts, a path-traced render mode. And a retailer-facing version: drop these tools onto product pages, and every shopping agent can lay out a real room instead of guessing.

Built With

Share this project:

Updates

Submission history