Inspiration
A doorway 100 mm too narrow does not look narrow.
That is the whole problem. The mistakes that make a home unusable are geometric, not aesthetic: a bathroom door at 800 mm instead of 900, a kitchen with no 1500 mm circle to turn a wheelchair in, a wardrobe parked where it quietly cuts a bedroom in half. They are invisible to the eye and obvious to a measurement — and the people who most need to catch them are the least likely to have an architect on retainer. Someone adapting a home after an injury. A carer assessing a flat before a parent moves in. A small landlord doing an accessibility retrofit.
LLMs are famously bad at spatial reasoning from a picture. But they are excellent at driving a tool that measures. So the interesting question was not "can an agent design a floor plan" — it was "what happens if we give the agent a real geometry engine and let it iterate against the numbers?"
What it does
Groundplan is a floor plan editor. You drag walls, drop furniture, cut doors and windows — all with a mouse, like any drawing tool.
The difference is that the whole engine underneath it is registered as WebMCP site tools, so an agent sitting in the browser can use it too.
Open the app and you get a two-bedroom flat that looks perfectly reasonable. Ask an agent to check it:
error Doorway into Bathroom is too tight 760 mm → needs 815 mm
error Bathroom is unreachable — the route narrows to
800 mm at the door between Bathroom and Hall 800 mm → needs 900 mm
warning No turning circle in Kitchen 1204 mm → needs 1500 mm
warning 68% of the home is step-free reachable
The bathroom door is 800 mm. That leaves 760 mm of clear opening — 55 mm short of the ADA figure — which means a wheelchair user in this flat cannot get to the toilet. Nothing on the drawing looks wrong. The number is wrong.
Look at what the second finding says. Not "the bathroom is unreachable", which is a symptom, but which of the seven doors is pinching the route and by how much. That comes from a widest-path search over the whole flat, and it is the difference between an agent that can act and one that can only report.
The agent calls one tool. The page does not apply it. Instead the change is drawn onto the plan in violet — the widened door, arrows where furniture would move — and a card appears in the panel: "Widen door in Bathroom: 800 → 900 mm — Approve / Discard." You click Approve, and the tool's result comes back:
{
"issues": {
"resolved": ["Doorway into Bathroom is too tight", "Bathroom is unreachable"],
"introduced": []
},
"plan_score": 78
}
Two findings closed by one 100 mm change, and the agent was told exactly that in the same round trip — so it knows whether to stop or keep going. Score 49 to 78; step-free reach 68% to 78%.
The other starter plan is a bungalow that passes every rule and scores 100. A checker that can only ever say no is not much of a checker.
Why WebMCP is the right fit
Three reasons this could not be an MCP server behind an API.
The measurements only exist in the page. Clearance, reachability and turning circles come from a live rasterisation of the drawing. There is no document to upload, no id scheme to learn — the agent measures the thing the user is looking at, in the state they left it, including the change they made ten seconds ago.
The confirmation is the real UI. When an agent proposes an edit, the page draws it on the plan — violet for what would appear or move, red for what would go, arrows showing where things travel — and the tool's promise does not resolve until a person clicks Approve or Discard. A chat bubble asking "shall I widen the door?" is a paraphrase of a change. This is the change.
The tool set is a function of application state. WebMCP lets a page register and unregister tools at runtime, so what the agent is able to ask for narrows and widens as the user works:
| State | Registered |
|---|---|
| Normal editing | 30 tools |
| Something selected | 31 — an edit_selection tool appears, scoped to that entity, so "make this wider" resolves |
| Findings outstanding | fix_violation appears, with the live rule ids as its enum |
| Review mode | 16 — every mutating tool is unregistered. The agent can look but not touch |
| Proposal pending | 18 — writes withdrawn, check_proposal added, so edits cannot pile up behind a decision you have not made |
That last one is the part I am proudest of. The user's control is expressed in the protocol, not just in the UI.
It also has one deliberate exception that took a while to find. The tool whose call is currently blocked on the approval has to stay registered, because unregistering a tool aborts its running invocation — withdrawing it kills the very call waiting for the user's answer.
The bit that changes people's minds
Numbers do not land. So there are two tools for making them land.
The first is compare_standards, for the question people actually ask — would this work for my mother? One call measures the same home against several bodies at once:
This home works up to 700 mm. At 900 mm, Bathroom becomes unreachable
— 800 mm at the door between Bathroom and Hall.
a walking frame 700 mm 88% reachable nothing cut off
a standard wheelchair 900 mm 68% reachable Bathroom cut off
a large powered chair 1000 mm 30% reachable five rooms cut off
The second is show_route.
Ask "show me why I can't get to the bathroom" and a violet disc — a wheelchair, drawn at its true 900 mm diameter — sets off from the front door, sweeps down the hall, reaches the bathroom door, and stops dead. It turns red, and a label appears on the spot: 800 mm — needs 900 mm.
Then ask it to try again with a walking frame. The same disc, 700 mm this time, goes straight through and turns green.
Same door. Same drawing. The difference is 100 mm — and it is the difference between a home someone can live in and one they cannot. It is not an illustration, either: it is the same widest-path field the rule engine uses, played back at scale with time added.
That is the argument for putting an agent inside the page rather than behind an API. The agent did the measuring; the page did the showing; the person watched it happen on their own drawing.
And it can build one from nothing
Ask "design me a one-bedroom flat for a wheelchair user" and the agent makes a single apply_batch call: five rooms placed against each other, a front door, an archway, three internal doors, three windows, every room furnished. Twenty-odd operations, drawn on screen in violet before any of it exists, and approved once.
Five rooms, about 74 m², every rule passing and 100 out of 100 — held there by a test that replays the whole conversation through the real operations and fails the build the day it stops scoring 100.
Getting that to work honestly was most of a day. The first attempt produced a flat that failed its own inspection: the automatic furniture placer parked a wardrobe across a doorway and a fridge facing a worktop, because it only knew how to avoid other furniture. It now rules out everything the checker would object to — doorways, the arcs door leaves sweep, the clear floor other fittings need — and among what survives, picks the position with the most space around it. There is a test that plays the whole conversation and fails the build if the result stops scoring 100.
Pointing it at a real home — and admitting when it missed
Most people meet a floor plan as a picture: an estate agent's listing, a photo of a drawing on a wall. So you can drop one onto the page, say how wide it really is, and get_tracing_image hands the picture to the model with the real-world coordinates it sits at. The agent reads the room names and printed dimensions off it, builds the rooms in one apply_batch, and they land on top of the drawing the person can see.
The important tool is the next one. check_against_source takes the areas printed on the original and compares them with what was actually drawn, room by room:
Bedroom 1 drawn 14.494 m² source 14.49 matches
Living room drawn 19.285 m² source 19.32 matches
Dining drawn 11.365 m² source 11.61 matches
Bedroom 3 drawn 11.365 m² source 13.01 wrong — drawn too small
That last line is the point. Reading geometry off a marketing drawing is unreliable — I traced a real three-bedroom plan and found the drawing contradicting itself, labelling a room 14 ft wide inside a column that measures 13.2. No model can resolve that, and a tool that quietly averaged the difference would be worse than useless, because everything downstream is a measurement someone might rely on.
So the app does not pretend. It tells you which rooms it got right, which it did not, and by how much. A tracing that measures correctly is the difference between advice about a real home and advice about a sketch of one.
What humans and agents can now do together
Both parties edit the same drawing, through the same commit(), into the same history. The Activity panel shows who did what — and every agent edit has a revert button beside it.
The agent can point: highlight makes rooms and furniture pulse on the canvas. It can show its working: set_view turns on the clearance heatmap so you see the same field its tools measured, rather than reading a description of it. And it can ask what you mean: get_selection tells it what you have clicked, so "this room" and "that door" are unambiguous.
apply_batch makes a real piece of work one decision rather than eleven: a whole sequence runs against a single draft under a single approval, atomically. If step four is rejected, steps one to three never happened, and the agent is told which step failed and why.
When it is done, export_plan hands over a link carrying the entire drawing in its URL — nothing uploaded, no account, no server to lose it.
Practically, that means you can say things like:
Check this flat for wheelchair access and fix whatever fails. Which door is stopping me getting to the bathroom, and how much wider does it need to be? I'm selecting the second bedroom — can a double bed and a wardrobe fit with a turning circle? Load the empty shell, design a one-bedroom flat for a wheelchair user, then send me a link.
How I built it
No framework, no backend, no telemetry. TypeScript in strict mode, Vite, a 2D canvas, and a static bundle you could host on a floppy disk.
The geometry (src/core/grid.ts) is the honest part:
- Rasterise the plan at 50 mm — rooms become floor, wall bands straddle every shared edge, doors and archways are carved back out, windows stay solid, furniture footprints are burned in.
- Run an exact Euclidean distance transform (Felzenszwalb & Huttenlocher) over the obstacle set. That gives the clear radius at every square of floor, and from that single field come turning circles and doorway clear widths.
- Flood fill from the front door, restricted to cells whose clearance is at least the mobility radius. A doorway narrower than the body simply does not connect — which is exactly the failure the app exists to surface.
- Dilate the result by the same radius to get the floor that body actually sweeps.
- Run a widest-path search — Dijkstra with
minin place of+and a max-heap — giving, for every square of floor, the widest body that could ever reach it and the route it took. Walking that route back finds the pinch point; matching the pinch against the openings names the door responsible. Among equally wide routes the search prefers the shorter one, so the path a body takes looks like a path a person would take, which matters once you animate it.
The rule engine (src/core/rules.ts) runs seventeen checks against ADA/ANSI A117.1 and ISO 21542 clearances plus typical habitable-room minimums. Every finding carries measured, required, unit, the entity ids, and a fix sentence phrased as a tool call. That shape is what turns check_plan from a status light into the other half of the agent's loop.
The tools (src/mcp/tools.ts) follow three rules learned the hard way:
- Failures are prose with a hint, never exceptions.
add_openingdoes not just refuse a 1600 mm door — it says "a 1600 mm opening will not fit the 1200 mm north wall of the Hall; the widest that fits with jambs is 1000 mm." Models correct that. They do not correct a stack trace. - Every mutation reports its effect on the findings (
issues.resolved,issues.introduced), so the agent learns whether the edit helped without a second round trip. - Everything resolves by name.
"bathroom","Bathroom"andr3all work, because an agent should not have to learn an id scheme before it can widen a door.
Both the single-shot tools and apply_batch dispatch through one map of operations, so a batched edit_room behaves identically to a standalone one, down to the wording when it refuses.
The host layer (src/mcp/runtime.ts) prefers native document.modelContext, falls back to the @mcp-b/global polyfill loaded on demand, and diffs the desired tool set against the registered one on every state change — registering with an AbortSignal so withdrawing a tool is a single abort().
Challenges
Getting the clearance measurement to mean the right thing. My first "doorway clear width" measured the largest clear circle inside the reveal — which quietly reported a perfectly good door as 460 mm because a bed sat 200 mm on the other side of it. True, but it is a different problem with a different fix. It now walks straight across the reveal and takes the longest unobstructed run, and "furniture parked in front of a door" is a separate rule.
Reachability that a person recognises. The flood fill tracks where the centre of a 900 mm body can go, which made a normal flat report 13% reachable — technically correct and completely useless. Dilating the result by the body radius gives the floor it actually sweeps, and the number became something you can argue with.
Tuning the rules so they are not noise. A coffee table 450 mm from a sofa is not "blocking the sofa". A dining table clipping 70 mm of a sink's clear-floor zone is not blocking the sink. Both fired in the first version. Low items are now flagged as non-blocking, and an obstruction has to take a fifth of the zone before it counts.
The consent gate quietly eating its own tool calls. Withdrawing the write tools while a proposal is pending is the feature — but unregistering a tool aborts its running invocation, and the tool that raised the proposal is the one blocked on the answer. Every approved change came back as "Tool unregistered". The fix is a keep-alive list threaded through the registration diff. The lesson is that dynamic registration and long-running calls interact in ways worth testing on purpose.
Accomplishments
The consent gate. An agent tool call that blocks on a human clicking a button in the page is, I think, the thing WebMCP unlocks that nothing else does — and building it as a promise the tool awaits, with the write tools unregistered for the duration, made it feel like part of the protocol rather than a UI afterthought.
Also: the whole thing works with no agent at all. There is a Site tools panel that lists everything currently registered, with schemas, and runs any of them by hand — the identical function an agent calls. It made development possible and it means anyone can see the surface without a special browser.
And it is keyboard-operable end to end — cycle through every room, opening and item, nudge things 50 mm at a time, with a live region announcing what is selected and what is failing. A tool about accessible design that needed a mouse would be a poor advertisement for the idea.
What I learned
Tool descriptions are the product. I spent as long on error messages as on the distance transform, and the error messages did more for the agent's success rate.
And: give a model a measurement and it stops hallucinating. Every question that would have been a confident guess — is this bathroom accessible? — becomes a tool call with a number on the end of it.
What's next
Genuinely non-rectangular rooms. L-shapes already work — you join rectangles with an archway 1400 mm or wider and the rule engine pools their area, daylight and turning circle as one space — but a polygon editor would be honest rather than clever. Multiple storeys with stair geometry. Import from DXF and IFC, where the geometry is exact: tracing a photograph is already in, and it taught me that a marketing floor plan often contradicts its own printed dimensions, so a picture can never be better than a starting point. Export to DXF. Turning circles for the specific chair someone actually owns rather than the standard one. And a shared session, so an occupational therapist and a client can walk a plan together, with the agent in the room and the wheelchair on the drawing.
Built with
208 tests cover it. Beyond the unit tests, one suite drives every single tool the way an agent would — valid arguments, nonsense arguments, and the consent gate, including that a destructive tool still asks even when the approval switch is off. Another throws sixty randomly generated plans and a set of deliberately broken ones at the engine and checks that it never crashes, never emits a NaN, never draws a wall across a doorway, gives the same answer twice, and never hands an agent a finding pointing at something that no longer exists. The drawings in the README are generated by the app's own SVG exporter, so they cannot drift from the code.
Built With
- 2d
- bottleneck
- canvas
- distance
- felzenszwalb?huttenlocher
- html
- paths
- polyfill
- shortest
- transform
- typescript
- vite
- vitest
- webmcp
Log in or sign up for Devpost to join the conversation.