Places is a live floorplan. You pin what's non-negotiable. Then you and an agent rearrange the same room, the agent cannot move anyone you pinned.
Inspiration
Seating a company dinner is still a spreadsheet and an argument. The room matters. The rules matter. HR stays away from engineering. The CEO sits at the head of the center table. A few people need the aisle. A plus-one is still on the way. By the time the chart leaves the sheet, it is already wrong.
A constraint solver dumps a list. A group chat dumps a thread. Neither is the room. I wanted one canvas where the host and the model share the same seats, and where “Mabel stays at the window” is not a prompt. It is a pin. Because she is pinned, move_guest is gone.
WebMCP is a capability bus for a live page, not a cart API. Pins, aisle seats, and “do not sit together” pairs are page state. Scraping the SVG drops them. The agent has to call tools, and the tool list itself is the contract.
The hundred-person problem is the category. The demo is twenty-two, same job, at a scale the canvas can prove.
How I built it
The page is ground truth. Drag, pin, plus-one, and every agent write go through the same executeCommand() in web/lib/places/commands.ts. A tool call moves a place-card on the SVG you are looking at. There is no backend, no login, and no API key. State lives in sessionStorage for the current tab.
document.modelContext.registerTool exposes a stable read/rule set: get_room_state, list_violations, explain_guest, add_guest, list_constraints, add_constraint, remove_constraint. Selection, pins, and open violations abort the previous AbortController and register only the tools that make sense now.
| When | Tools the agent can see |
|---|---|
| A table is selected | seat_guest_here, set_capacity, leave_empty_seats |
| An unpinned guest is selected | pin_guest, move_guest, swap_guests |
| A pinned guest is selected | unpin_guest only |
| The room has violations | fix_violations (never moves pins) |
There is no always-on move_guest(guestId, tableId). You choose the contract. The agent works inside it. If document.modelContext is missing, the human controls still work. The header badge says so.
The seed is a fictional Thursday: four tables, pixel-art guests, rules you can point at. Rex and Vivian cannot share a table. Arthur needs the aisle. Two chairs are held empty. Each load picks one of four opening problems (?seed=0 through ?seed=3) so a judge can share a messy room, not a blank grid.
The solver is a deterministic hill-climb over pairwise swaps. It scores the room as
$$S = \sum_{v} w(v),\quad w(v)=\begin{cases}10 & \text{hard}\ 3 & \text{preference}\end{cases}$$
then reports why it stopped: solved, blocked_by_pins, infeasible (with a proof), or no_arrangement_found. Same room, same answer, every time, which is what the tests and the timeline need.
The stack is a Vinext/React page on ChatGPT Sites. Domain logic is isolated under web/lib/places/. CI runs unit tests, WebMCP eval fixtures, typecheck, lint, and format. The evals replay tool traces against the real registrations without calling a model or spending a key.
Challenges
The tool list had to be the lock. A prompt that says “don't move pinned guests” is a suggestion. Unregistering move_guest is a lock. Getting that to update on every pin and selection, without racing the previous AbortController, was the WebMCP work. The fixture respect-pin-when-reflow-is-blocked forbids unpin_guest, move_guest, and swap_guests, and expects blocked_by_pins.
Honesty beats a pretty rearrange. Hill-climbing gets stuck. A better arrangement may exist only if you move a pin. I had to distinguish “the pins are in the way” from “the hard rules are impossible” from “the search gave up.” The solver runs once with pins respected, once with pins relaxed, and only then returns blocked_by_pins with the guests at fault. It never overrides the host.
The room had to stay a product. An empty grid is a proof of concept. A messy dinner is a job. Building a cast, a floorplan, a timeline, undo/redo, JSON import/export, and a PNG save took as much care as the tools. If the human half is a toy, the shared room is a toy.
CI cannot pretend it is ChatGPT. Model selection is probabilistic and costs money. The eval fixtures describe representative journeys (read the room, fix it, pin blocks, plus-one, add a rule) and replay them deterministically. Live-browser trials still matter. They are not the merge gate.
What I learned
WebMCP is not “expose every action as a tool.” It is exposing the right contract at the right moment. The interesting part is what the agent cannot see.
People are good at naming constraints. Models are good at searching a room. That split only works if both hands write to the same state. Same executeCommand() whether I drag a seat or the agent calls a tool.
A pin is stronger than a system prompt. blocked_by_pins is a feature. The host stays in charge.
Pin what's non-negotiable. Rearrange the rest together.
Built With
- chatgpt-sites
- react
- tailwind-css
- typescript
- vinext
- vite
- webmcp

Log in or sign up for Devpost to join the conversation.