Inspiration

Automated accessibility scanners axe-core, Lighthouse, WAVE are what most teams rely on to catch WCAG issues, but independent research puts their real-world coverage at roughly 30-40% of the actual violations. The other 60-70% only exist as behavior a broken tab order, a validation error not reaching a screen reader, a keyboard trap that only emerges after cycling through a component a few times. To static crawlers reading HTML, none of this exists only what the markup says, not what the page does when operated.

That's where WebMCP stops being desirable and becomes structurally necessary. An agent using ordinary browser automation has to guess at page state from screenshots and DOM scraping slow, brittle, and blind to anything that's only ever existed transiently, like whether an aria-live region fired. WebMCP lets the page itself expose ground-truth tools for the agent to ask directly "did this actually announce to a screen reader?", rather than inferring an answer from a static snapshot. It's not merely a faster way to do the same thing it's a category of inspection that wasn't previously practically possible.

What it does

AccessCanary presents a mock government-benefits application form, with three real, deliberately-built accessibility defects implemented the way these bugs occur in production code:

A broken focus order a tabIndex override silently reorders the entire page's tab sequence (WCAG 2.4.3)

An ARIA-live region that's silent a validation error appears visually but was never wired to announce to screen readers (WCAG 4.1.3)

A keyboard trap a custom date picker's focus-cycling logic never releases focus back to the page (WCAG 2.1.2)

As an AI agent calls six WebMCP tools, it narrates what the form is doing in real-time, alongside the form. A glowing focus-trace dot follows keyboard focus live, making tab order visible to anyone watching, not just the person tabbing around the form.

The most novel part is that the agent can only propose a finding its state-changing tool, report_violation, stages a finding for review but never logs anything directly. Only a human clicking Confirm moves it into the permanent violation log; Dismiss discards it with no trace. That's what "people and agents working together" really means the agent picks up on things humans couldn't, but the human keeps the decision power to determine what's a real bug.

How we built it

React 19 + TypeScript + Vite, deployed as a static site with no backend, and all of our WebMCP tooling is client-side as well.

Our five read-only tools are registered as @mcp-b/react-webmcp's useWebMCP hook wraps document.modelContext.registerTool for the current WebMCP API:

typescript

useWebMCP({

name: 'get_focus_order',

description: 'Returns the real browser tab order starting from a given element...',

inputSchema: getFocusOrderInput,

outputSchema: getFocusOrderOutput,

annotations: { readOnlyHint: true, idempotentHint: true, destructiveHint: false },

execute: async (input) => ({ steps: getFocusOrder(input.startSelector, input.limit) }),

});

Five tools are read-only (get_focus_order, simulate_tab_sequence, get_aria_live_state, get_element_role, get_violation_log), each with the correct readOnlyHint/idempotentHint/destructiveHint security annotations from WebMCP's published guidance, and each tool respects the recommended character budgets (500-char descriptions, 150-char parameter descriptions, ~1.5K-char output cap). The one state-changing tool, report_violation, only can stage a finding it can't confirm it.

The actual detection logic exists in a framework-free lib/ layer, separate from the WebMCP wiring, making it independently unit-testable. getFocusOrder correctly implements real browser tab-order rules (positive tabIndex sorted first, per the HTML spec, which is what makes the focus-order bug detectable) and a MutationObserver-based tracker records whether aria-live regions genuinely fire, not just whether the attribute exists.

Challenges we ran into

Two real bugs, both worth being upfront about. First: our initial tool schemas were Zod object maps, following a pattern from a tutorial, but @mcp-b/react-webmcp doesn't declare any Zod dependency at all the Zod support is structurally typed against Zod v3 internals, while the current Zod release is v4. Real, reproducible TypeScript errors, not a guess. We switched to plain JSON Schema the format the package's official examples show first to avoid version-coupling risk.

Second: our unit tests were just failing in strange ways every getFocusOrder test returned an empty array. jsdom (our test environment) doesn't have a real layout engine, so element.offsetParent which our visibility check relied on is always null there. The fix detects whether a real layout engine is present, and only requires that signal when one is, ensuring real-browser behavior is preserved but also making it testable.

Accomplishments that we're proud of

Getting the human-confirmation mechanism to actually work as a real constraint, not just a UI suggestion. The agent's report_violation tool is architecturally incapable of writing to the confirmed violations log that decision is made in the React reducer, not by convention. We're also proud of the fact that every one of the three bugs is a real defect (verified by manually tabbing through and screen-reader spot-checking) and shipped with a real, passing unit test suite covering the actual detection logic, not just a working demo.

What we learned

That "thoughtful use of WebMCP" mostly comes down to restraint a tight set of six tools with correct security semantics beats a sprawling one. The most interesting design decision in this whole project wasn't a tool it was to make the one state-changing tool incapable of actually changing anything on its own. We also learned, concretely, not to trust a tutorial's dependency assumptions over what's actually declared in a package's own package.json.

What's next for AccessCanary

Extending detection to more WCAG behavioral patterns beyond the three shown here (focus restoration after modal close, dynamic content insertion announcements), and testing against real production forms rather than a single mock page, to see how the six-tool approach generalizes beyond the demo.

Built With

Share this project:

Updates

Submission history