Parity —
Parity is a specialist-care booking app where every capability is reachable three ways — the visual interface, a keyboard/voice command palette, or your AI agent — and all three drive the same WebMCP tool registry. The human palette is not a parallel implementation: it calls document.modelContext.getTools() and executeTool(), the identical APIs the agent uses. One definition, two callers.
Why booking specialist care fits WebMCP
Booking a specialist is a constraint-satisfaction problem: insurance × language × wheelchair access × ASL interpreter lead time × paratransit pickup window × caregiver availability × appointment length. No booking site lets you express those at once, so today it is forty minutes of phone calls.
The people with the most constraints are disproportionately the people the
interface already blocks: calendar grids with no keyboard path, custom dropdowns
with no roles. Chrome's own WebMCP docs use date_pick as the canonical control
an agent cannot understand. Calendar grids are simultaneously among the web's
most notorious accessibility failures. That overlap is the entire product. A
constraint solver is exactly what an agent is good at, and the tools that let it
solve are the same tools that give a keyboard user a way in.
How it improves the experience
Parity has one tool registry. The visual UI, a ⌘K command palette, voice, and
your agent are all callers of it.
The palette is not a menu of app features. It calls
document.modelContext.getTools(), the same discovery API the agent uses, and
executes through executeTool(). Every form in it is generated from the tool's
own JSON Schema, and each field's label is its schema description. That is
the inverse of spec issue #286: rather than deriving a parameter description
from an accessible name, the accessible name is the parameter description, so
they cannot drift apart. Voice works the same way. The grammar is assembled from
each tool's declared aliases and the enum values in its own schema, so a new
tool becomes speakable the moment it is defined.
Second contribution: agentic browsing is currently an accessibility
regression. When an agent fills a form, the DOM mutates silently. A sighted
user watches it happen; a blind user is told nothing. Every execution here
announces through aria-live with the actor named: "Agent selected Dr. Amara
Okafor." One fix, both surfaces.
What people and agents can now do together
Ask for "the earliest wheelchair-accessible neurologist who takes my plan, with an interpreter" and the agent solves it, while you watch each step land and hear it announced. Then take over mid-flow with the keyboard, from the same registry, and undo what the agent just did.
Because every surface is a caller rather than a copy, a search the agent runs moves the page's own form, and a paratransit window set by voice greys the slots it excludes while saying which window excluded them.
The agent cannot commit a booking. confirm_booking returns
pending_authorization and an approval card appears that only a human can act
on. When a tool disappears, the agent is not left guessing: spec issue #262
calls that semantic context blindness, and get_booking_state.unavailable[]
returns {tool, reason_code, unlock_by} for every non-live tool, so "the hold
expired, call hold_slot" survives the unregistration that destroyed it.
How WebMCP is implemented
No server. A static SPA; the browser is the MCP client, the page is the server made of closures.
- 19 tools defined, never more than 8 registered. Each carries
available(state); the registry diffsallTools.filter(t => t.available(state))on every store change and registers/unregisters viaAbortController. Illegal actions are prevented by absence, which is Parity's answer to #255, built from existing primitives. - One
defineToolspec → six consumers: registration (z.toJSONSchema), palette form, voice grammar, runtime validation, announcement, audit entry. - One typed envelope (#282): refusals fulfil with
{ok:false, kind, reason, next}; only bugs throw. - One grant gate, three callers. SHA-256 over canonical arguments, 120 s expiry, consumed once, re-validated at commit.
Verified rather than asserted: 385 unit tests, 36 assertions driven against real Chrome over the DevTools Protocol, 14 accessibility checks (axe-core at every stage, a rendered refusal included), and 7 adversarial evals against the live deployment.
And the honest part. Spec issue #288 records a host clicking a page's own
Approve button. We reproduced it: CDP-injected input approved our card and
the page logged it as a trusted event. So we say page-side approval is
necessary, not sufficient, record delta_ms and modality, and flag
sub-second approvals. We add no CAPTCHA or timing challenge: every mechanism
that separates synthesized input from physical input is a barrier to switch
access, voice control and screen reader users, and still would not establish
authorization.
We also say what we have not done. Contrast is measured rather than estimated and enforced by a script, which caught a colour whose own comment claimed 4.6:1 while it measured 3.09:1. No screen reader has been run against this build, and no disabled person has used it. Both are in the README, and both were said to the working group.
What went upstream
The spec issues are not decoration. Building against them produced findings, and the findings went back:
- Adopted. Our enforcement by absence is now mitigation (d) in the body of #298, credited, with the comparison table and the caveat that absence destroys context unless something registered names what is missing.
- Cited. Our reproduction is referenced in the body of #300. A tool that unregisters itself mid-execute rejected its caller after the action succeeded, so an agent was told a booking failed while the patient had one, and its natural next move is to retry.
- Contributed. A Web Platform Test for that case is open on
web-platform-tests/wpt(#62642). It fails on Chrome 152 and passes on Chromium 153. - Measured. On #299, a caller's abort cancels the report and not the
work: a write loop ran to completion while the caller received
AbortError. On #96, one browser gives a declarative form handlerSubmitEvent.agentInvokedand gives an imperative tool nothing but anAbortSignal, so a page cannot tell who called. That is why the actor in our announcements is documented as inferred, not known. - Corrected. Twice we called Chrome 152 behaviour a gap in the specification. A reviewer showed the draft already required the correct behaviour both times. We verified his reading on Chromium 153, fixed our own README, and said so publicly.
Building your website for agents is how you finally make it usable by the humans your interface locked out. It is the same work, not two projects.


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