Inspiration

The WebMCP explainer says the API is "not designed for ingestion by accessibility technology."

Read that beside what WebMCP actually does and the risk is plain. A site exposes rich, structured, actionable capability — find_seats, hold_seats, complete_booking — and an agent gets all of it. A screen-reader user gets whatever the interface happened to render. Capability for agents, leftovers for disabled users. That would be a bleak outcome for a technology whose own explainer lists improving accessibility as a goal.

We think that split is an implementation choice, not a property of the standard. Aurora Hall is the other choice, built.

We picked booking accessible seating because it is, with grim regularity, the least accessible thing a venue operates — still routinely routed to a phone line during office hours. And it suits WebMCP for a specific reason: it is not a discoverability problem you can label your way out of. It is a query.

What it does

Aurora Hall is a 330-seat venue with a working booking flow. Every seat carries a complete, correct aria-label. Traversing them linearly is technically possible and practically useless.

Now add the constraints a real patron combines: two of us, one wheelchair space with a companion seat beside it, induction loop, a clear view of the caption unit, step-free, no strobe — I have photosensitive epilepsy.

Exactly two pairs satisfy all of that: N-2 + N-3 and N-4 + N-5. Two viable answers out of 330 — and the only reason anyone knows that is that something searched. Drop the induction loop and it becomes four; the extra two sit in row L, where the loop does not reach.

Correct accessibility is necessary and it is not sufficient.

Beyond search, the site does three things venue websites generally do not:

It remembers. State your access needs once and they are applied — WCAG 2.2 3.3.7 Redundant Entry taken past the letter of the criterion. No phone call, no explaining yourself to a stranger, no office hours.

It refuses. When no seat meets everything, constraints relax in a fixed order and the result says exactly what was given up. Strobe limits and step-free routes are never relaxed: giving those up does not produce a worse seat, it produces a seat the caller cannot use, or must not be sold. Ask for a strobe-free seat at our continuous-strobe show and the honest answer comes back — none is offered, and why.

It answers questions that have no answer anywhere else. Can I get a drink in the interval without queuing at a high counter? Where does my powerchair go if I transfer into a seat? Is the walk from the bus stop step-free? Is there anything here I actually cannot attend? Because access detail is attached to each thing rather than filed on an accessibility page, an agent can answer all of them — a lowered counter at 760 mm on the western end, or staff bring your order to your seat; beside the bays, not four floors away in the cloakroom; yes, but cross on the western side, there is no dropped kerb on the eastern one.

And the same contract answers a screen-reader user with no agent involved at all. The capability is not conditional on having an agent.

How we built it

Sixteen tools are defined once, in src/lib/tools/registry.ts, and consumed twice: by an agent through document.modelContext.registerTool(), and by an in-page command interface opened from the tab order or with ⌘K.

When WebMCP is available the command interface deliberately routes through getTools() and executeTool() — the same calls the agent makes — rather than shortcutting to the handlers. Parity is a property of the architecture, not a claim in a README. When WebMCP is absent it falls back to the shared registry directly: the capability never disappears, only the route changes.

Form fields are generated from each tool's inputSchema, the same JSON Schema the agent reads to decide how to call the tool. Inputs described once, rendered twice. Handlers return prose, not data structures — an agent reasons over it perfectly well, and it is the same sentence a screen reader announces.

The deliberate non-tools matter as much as the tools. There is no sign_in: establishing identity is a different order of permission from reading a seating plan. There is no update_access_profile: an agent may act on what someone recorded about their own needs, not rewrite it. And the agent never offers to "enable accessibility" — that would make accessibility conditional on having an agent, which is the two-tier web this project exists to argue against, rebuilt one layer up.

/llms.txt carries the same material for agents arriving without WebMCP, generated from the same data as the pages and the tools. Next.js 16, TypeScript, Tailwind. No backend — the hall is generated deterministically, so results are reproducible and the seed data is auditable.

Challenges we ran into

A tool that bought the wrong seats. An early complete_booking took no seat argument and booked everything on hold. Testing in ChatGPT's browser, the agent held two candidate pairs — sensible, both were viable — was asked to book one, called the only tool available, and correctly bought all four. 32.000 kr instead of 20.500. The agent did nothing wrong. Our contract was underspecified on an action that charges money and cannot be undone, so the agent faithfully executed the underspecification.

Two implementations that disagree. Chrome rejects executeTool with an object argument; ChatGPT's browser rejects it with a string. We wrote a fallback to tolerate both — and keyed it on recognising the error, which silently failed because Chrome throws a DOMException our test did not match. The in-page command interface broke on its default tool in exactly the browser a judge would use, and worked everywhere WebMCP was absent, which is why nothing caught it. It now retries unconditionally rather than trying to be clever about which error it got.

A safety rule that only half existed. Our search relaxed constraints when nothing matched, and maxStrobeExposure was in that list. Asking for a strobe-free seat at the continuous-strobe show returned six seats with the limit quietly dropped — at a performance whose own data says the venue will not sell you a seat that pretends otherwise.

A contrast regression on an accessibility project. While making the footer look more commercial, a caption went to text-slate-400 on white and nine pages failed AA. npm run a11y caught it — and it was committed anyway, because the check was piped into tail, and a shell pipeline reports the exit status of the last command. The gate had never been firing.

Accomplishments that we're proud of

Parity that is structural rather than promised. The human interface calls the same executeTool the agent does. You can verify that in the code rather than taking our word for it, and the UI says which route answered.

Safety constraints that are not preferences. A strobe limit and a step-free route are never traded away for a better view. No seat at all is the correct answer more often than the industry admits.

No accessible mode. Two views, the visual plan and a filterable list, and neither is the accessible one. The toggle says "Seat list", not "Accessible view", because the name is what decides which pattern you have built. Motion lives inside prefers-reduced-motion: no-preference, so it is not a mode you enable but motion you get unless you have already said no. forced-colors and prefers-contrast are handled the same way.

13 states clean against wcag2a through wcag22aa plus best-practice, with an ARIA grid, roving tabindex over 330 cells, and a skip link ahead of the grid.

What we learned

Exposing capability to agents raises the cost of a vague tool. It does not lower it. Every ambiguity we left in a tool description became something an agent executed confidently and correctly, to a result nobody wanted. Writing tools is API design with the safety rails of human hesitation removed.

Run the check, and check that the check can fail. A contrast regression got into an accessibility project, introduced by the people arguing for accessibility, while making a page look better. Care did not catch it. A tool did — but only once the tool was wired so it could actually fail.

Claims should survive being checked. We miscounted our own tools in our own README. If the argument is that sites should tell the truth in a machine-readable way, the prose has to be true too.

What's next for Sæti — Aurora Hall

Real venues. The hall is deterministic seed data; the interesting version connects to an actual ticketing backend, where access attributes are usually recorded badly or not at all. Modelling what a venue should record is half the contribution.

Wider assistive-technology coverage, tested with disabled patrons rather than only with developers. Automated tooling catches roughly a third of real barriers; the rest needs people.

More languages. The access vocabulary here is English over an Icelandic venue. Access needs do not translate cleanly, and a tool contract that assumes they do will fail quietly.

A pattern worth copying. Nothing here is specific to seating. Any site with a constrained search over accessibility-relevant attributes — housing, transport, clinics, hotels — has the same shape, and the same choice about whether the agent gets the good version.


Live: https://aurorahall.app Code: https://github.com/peritus-slf/webmcp-accessible-booking (MIT)

Aurora Hall is fictional. Any resemblance to a real venue's access provision, good or bad, is coincidental.

Built With

Share this project:

Updates

Submission history