-
-
Rejecting a table opens rule capture. The app turns that correction into a rule and applies it to future proposals.
-
The agent replans around the new rule and finds 16 seats elsewhere. The layout reaches 144 seats with zero violations.
-
Asked for 40 more seats, the agent proposes five tables through WebMCP. Nothing changes yet because proposals are previews until approved.
-
Willowmere Hall on load. 96 seats, eight built-in rules, and three locked objects the agent cannot move.
What it is
ConstraintCanvas is a venue floor planner where a human and an agent work on the same canvas at the same time.
You place the stage, lock anything that cannot move, and decide which sponsor gets the front booth. The agent handles the tedious parts, like fitting more seats without breaking egress, finding clearance violations, and explaining exactly which rule blocked a placement.
The floor planner is only the demo.
The real idea is what happens when the human says no.
Inspiration
Agent corrections are usually temporary. You tell the model not to do something, that instruction lives in context, and a few turns later you may have to repeat it.
That felt like the wrong place to store human judgment.
If I say seating must never block a door, that should not just be prompt context. It should become application state that is visible, editable, and enforced.
In ConstraintCanvas, rejecting an agent proposal can create a named rule with provenance. That rule is added to a visible rulebook and enforced against every future proposal.
The agent does not need to remember better. The application enforces the correction.
Why this use case fits WebMCP
The site owns the semantics.
The application knows what counts as blocking an exit in metres. An agent using DOM automation would have to infer geometry from pixels. With WebMCP, the floor state and rules are exposed directly as structured data.
State is shared.
The human and agent both modify the same floor plan during the same session.
Read, preview, and write are separated.
Six of the nine tools use readOnlyHint. Tools that can affect the floor only generate previews, and the user approves changes before they are applied.
That separation lives in the tool contract rather than in prompt instructions.
Refusals are structured.
A blocked proposal returns the rule ID, rule statement, offending item, and numeric shortfall.
The agent can replan using actual constraint data instead of guessing why a call failed.
What people and agents can do together
The core use case is constrained spatial planning.
The human defines the boundaries and important decisions. The agent optimises inside them. The application guarantees that those boundaries are respected.
Without that enforcement, the human either does the optimisation manually or delegates it and rechecks every result.
WebMCP enables a third model: delegate the arithmetic, keep the judgment, and turn corrections into persistent constraints.
How WebMCP was implemented
ConstraintCanvas registers nine tools with document.modelContext.registerTool, grouped into inspection, proposal, explanation, and constraint authoring.
Two headers are required before document.modelContext is exposed:
Origin-Agent-Cluster: ?1
Permissions-Policy: tools=(self)
Tools are defined as descriptors and registered through a shared bridge that wraps every execute call. This keeps logging and refusal handling at the tool boundary instead of duplicating it inside each implementation.
Each tool has its own abort controller so the surface can be withdrawn and republished cleanly. Results are returned as JSON.
The most interesting part is that the tool contract changes at runtime.
When a user ratifies a new rule, the proposal tools are re-registered with the active constraints included in their description, then the surface emits toolchange.
That means a correction changes what the agent is told it can do before the next call is created, not only what the application rejects afterwards.
Validation runs in three passes:
- Check locks as hard boundaries.
- Compare rule violations before and after the proposal.
- Convert valid proposals into a preview for human approval.
The differential check is important because an existing violation should not block a proposal that actually improves the layout.
How I built it
The app uses Next.js 16, React 19, TypeScript, SVG, and IBM Carbon design tokens. It is deployed on Vercel.
There is no state library and no canvas library.
The constraint engine is intentionally simple. Placement uses a greedy search with scoring rather than a general solver. The priority is not algorithmic complexity. It is producing layouts that remain valid and explainable.
Eight built-in rules use the same evaluation engine as user-created rules.
For egress, the app runs a flood fill outward from doors across the floor grid. Straight-line distance is not enough because an object can be physically close to an exit while still being blocked by surrounding furniture.
Circulation also enforces a minimum free-area ratio:
$$ \frac{A_{\text{floor}} - \sum_i w_i h_i}{A_{\text{floor}}} \ge 0.70 $$
This gives seat maximisation a hard upper bound.
The app also includes a small agent panel that calls getTools and executeTool against the same surface, so the full interaction can be demonstrated without a separate agent browser.
Challenges
Different WebMCP implementations exposed different APIs.
The spec describes ModelContext as an EventTarget, so I initially used addEventListener for toolchange.
That worked in Chrome but failed in ChatGPT's implementation, causing React to unmount and render a blank page.
Native calls are now feature detected against a minimum surface of registerTool, with regression tests for reduced implementations.
Bad refusals waste agent turns.
An early optimise_layout refusal only returned:
Unknown objective.
The agent had no way to discover valid inputs and spent several calls guessing.
Refusals now require a next_step, and invalid values return the accepted alternatives.
Thinking models preserve metadata across tool calls.
Gemini 3 attaches a thought signature to function call parts. I initially rebuilt the response from extracted tool calls and dropped that metadata.
The server route now forwards the original model turn unchanged.
Valid layouts can still look wrong.
Early plans passed every rule but broke the visual rhythm of existing seating.
The optimiser now detects repeated furniture spacing and includes alignment in its scoring function.
Model calls needed caching.
One planning task could use five or six requests because the agent repeatedly inspected unchanged state.
Inspection results are now cached per conversation and invalidated when a proposal changes the floor.
What I learned
A tool contract should be written as if it is the agent's only source of truth.
A refusal should explain what failed and what the agent can do next. Otherwise it becomes a dead end that wastes tokens and tool calls.
I also learned not to assume that one implementation of an emerging standard represents all of them.
Most importantly, WebMCP is not only useful because a webpage can expose tools.
The more interesting capability is that the tool surface itself can change at runtime.
That allows a human correction to become part of the application's active contract, which is much stronger than asking the model to remember it.
Built With
- ai-agents
- chatgpt
- computational-geometry
- constraint-solving
- css
- document.modelcontext
- google-gemini-api
- human-in-the-loop
- ibm-carbon-design-system
- ibm-plex
- javascript
- next.js
- node.js
- react
- svg
- typescript
- vercel
- webmcp
Log in or sign up for Devpost to join the conversation.