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 diffs allTools.filter(t => t.available(state)) on every store change and registers/unregisters via AbortController. Illegal actions are prevented by absence, which is Parity's answer to #255, built from existing primitives.
  • One defineTool spec → 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 handler SubmitEvent.agentInvoked and gives an imperative tool nothing but an AbortSignal, 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.

Built With

Share this project:

Updates

posted an update —

Updated and pushed. Two things were wrong with it, one of which mattered more than staleness.

The correction

The copy said:

"#165's requestUserInteraction(), which we feature-detect on every call and found absent in Chrome 152."

That reads as a spec feature Chrome hadn't shipped. I checked: requestUserInteraction appears zero times in index.bs. It's the shape proposed in #165, and it's absent from Chrome 152 and Chromium 153.

That's the exact draft-vs-implementation conflation we were corrected for upstream — sitting in the submission text the whole time. Now stated as a proposal.

What was missing

The whole "What went upstream" section, which didn't exist when this was written. That's the biggest change to what this project is:

Adopted — enforcement by absence is mitigation (d) in #298's body, credited Cited — our reproduction is in #300's body Contributed — WPT #62642, fails on 152, passes on 153 Measured — #299 and #96 Corrected — the two times we were wrong and said so publicly

"Built against the spec issues" and "the findings went back and changed them" are different claims, and only one of them was on the page.

Also

Every number re-measured, not recalled — 385 tests, 36 Chrome assertions, 14 a11y checks, 16 providers, 8 max live. I put the commands to re-measure them in the paste comment so the next edit has no excuse.

Added the accessibility honesty (contrast measured not estimated, and the no-screen-reader/no-disabled-user line), and the features built since: surfaces reflecting each other's calls, and constraints that grey the slots they exclude while saying which window excluded them.

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