Inspiration
I wanted a small, concrete domain to actually test what WebMCP makes possible, instead of a demo that just claims an agent did something. Planning a night out — a film, then dinner after — is a real multi-constraint problem: the showtime fixes the cinema, the cinema fixes the route, the route decides how much time is really left before the table, and every one of those choices moves the total cost. It's exactly the kind of thing an agent is good at grinding through and a person is good at approving. Most "agentic" demos I'd seen were either a chat box bolted onto a UI it couldn't see, or an agent with unchecked authority to just click "book." I wanted the version where the agent gets real capabilities and the person still holds the only button that matters.
What it does
PlanOnIt plans a real evening out in Dhaka — a film, the trip across the city, and dinner after — with an AI agent doing the tedious cross-referencing through 13 WebMCP tools registered directly on the page. The agent can search restaurants and showtimes, build a plan, check feasibility, calculate cost, and repair a plan a human edit just broke. It can never approve or reserve anything — that stays a real click in the UI, every time. Once approved, reservation is a genuine sandboxed transaction: a compare-and-swap against shared inventory, idempotent on retry, and it fails closed on conflict instead of quietly double-booking.
How we built it
React 19 + TypeScript + Vite on the frontend, Zod for runtime validation, deployed as a static build to Vercel. The core decision was to build one domain layer (src/domain.ts) that both the human UI and the WebMCP tools call into — the same solver, the same nine feasibility checks, the same versioned mutations — so there's never a "friendly explanation for the person" that can quietly drift from "the raw check the agent gets." Every mutation carries an expectedVersion; a stale write fails loudly with STALE_PLAN_VERSION instead of silently overwriting someone else's edit, whether that edit came from a human or another agent call. Same-origin tabs stay in sync through a Web Locks-backed compare-and-swap. Inventory sits inside a rolling two-week window computed off the real clock, so the demo never goes stale no matter when a judge opens it.
Challenges we ran into
The one I'm least proud of, and gladdest I caught before submitting: late in the build I found that a human could never manually pick a transport option — every click failed with "Transport option does not belong to the selected venue route." The agent's tools worked fine, and so did the auto-repair path, which made it easy to miss. It turned out the manual UI was resolving the cinema-to-restaurant route with the two location IDs swapped, and since each direction generates its own distinct set of transport-option IDs, the buttons on screen were always offering IDs from the wrong route object. It only ever broke the human path — a good reminder that "the agent's tools work" and "the manual UI works" are two separate claims, and testing one doesn't verify the other. I added a regression test that reproduces the exact failure so it can't come back silently.
Building a reservation that's an actual transaction, not just a returned success string, was its own challenge: idempotency keys are bound to the reservation's content fingerprint, not just a request ID, so a retry after a dropped connection can't accidentally create a second booking, and a replayed or forged confirmation can't be pointed at a different reservation.
And a last-minute one that had nothing to do with code: my original LICENSE restricted commercial use without permission, which meant it wasn't actually an open-source license, even though I'd always intended it to be. Caught and fixed to MIT before submission — "open source" has to be literally detectable, not just intended.
Accomplishments that we're proud of
- 198 automated tests plus 21 real-browser Playwright tests, covering adversarial input, concurrency, the full reservation lifecycle, and idempotency — not just the happy path.
- The evaluation-symmetry property: the UI's "why isn't this plan valid yet" and the agent's
validate_plantool result are always the same function call against the same state. There's no second copy of the business logic that could quietly disagree with the first. - Approval isn't gated by a permission check the agent could theoretically satisfy — it's architecturally absent from the tool surface. No WebMCP tool touches
approvalat all. - Shipping with no login requirement, on purpose. It would have been easy to bolt on accounts to look more "complete," but this build doesn't actually need cross-device sync to do its job, so I didn't fake it.
What we learned
That "the agent works" has to be tested completely separately from "the human path works" — they share code, but a single reversed argument can break only one of them, silently. That documentation drifts fast: numbers like "12 tools" or "eight checks" written once during development go stale the moment the code moves on, and are worth re-deriving from the running code rather than trusting memory. And that the strongest trust story for an agentic feature isn't a more polite prompt — it's an architectural boundary the agent literally cannot cross, like approval never being expressed as a tool at all.
What's next for PlanOnIt
- A real server-authoritative reservation boundary. The extension point already exists (
RemoteReservationAuthorityinsrc/authority.ts), but no concrete server ships in this build — a public browser bundle can't hold a shared bearer secret without a real per-user session, and I'd rather ship nothing than ship authentication theater. - Real inventory behind the restaurants, cinemas, and routes, in place of the Dhaka sandbox data.
- A repair flow that explains the alternatives it considered and rejected, not just the plan it landed on.
Built With
- css
- eslint
- html
- javascript
- jsdom
- lucide-react
- npm
- playwright
- react
- typescript
- vercel
- vite
- vitest
- webmcp
- zod
Log in or sign up for Devpost to join the conversation.