Demo — running an accessibility audit with the MCP server
What this is. A step-by-step account of running an accessibility audit on a real application using the neumo MCP server — not building an app, but auditing one. It documents the exact prompts, which MCP tools are called, the raw results, the one real issue it surfaced, the fix, and the re-verification.
The subject is the React demo (
examples/react-unclaimed-property), but the workflow applies to any rendered markup — including the .NET / Vue web-component apps (capture their flattened DOM the same way).
The setup
| Subject | The UnclaimedProperty.com waitlist page (React demo, examples/react-unclaimed-property) |
| MCP tools | audit-a11y (WCAG 2.2 structural scan via axe-core) + check-contrast (WCAG 1.4.3 contrast ratios) |
| Supporting | Playwright (capture the rendered DOM — audit what ships, not the source) |
| Standard | WCAG 2.2 AA |
Why two tools? audit-a11y covers structure (labels, roles, headings,
landmarks, alt text, ARIA…). It is static — by its own admission it can't
resolve colors that live in a stylesheet. check-contrast covers exactly that
gap: the color-contrast success criterion (1.4.3). Together they give
full-spectrum automated coverage; the two together are the audit.
Step 1 — Audit what actually ships (not the source)
Prompt: "Run an accessibility audit on the React app with the MCP server. Audit the rendered DOM, not the JSX — I want to check what the user actually gets."
Auditing the source JSX would miss the ARIA the components add at runtime
(aria-haspopup, aria-expanded, generated ids, the real <button>/<input>
markup the compound components emit). So the app runs (npm run dev) and
Playwright captures the rendered DOM:
// Playwright — serialize the mounted app
await page.evaluate(() => document.getElementById("root").innerHTML);
That HTML string is what gets audited.
Step 2 — Structural audit (audit-a11y)
Prompt: "Run audit-a11y on this rendered markup."*
Tool — mcp__neumo-ds__audit-a11y (axe-core WCAG 2.2 rules + static
contrast/target-size). Passing the captured DOM string as markup:
Result:
{ "violations": [], "totalViolations": 0,
"summary": "No WCAG violations found by static analysis (see limitations)." }
Zero structural violations — every input has an associated <label>, the
heading order is correct (h1 → h2), the page uses <main> / <footer>
landmarks, the card is aria-labelledby its title, and all decorative icons are
aria-hidden. (The state select renders as a labelable <button>, so its visible
"State You Live In" label associates correctly — axe confirms it's named.)
But note what the tool explicitly says it cannot judge — this is the important part, not a footnote:
- Contrast is checked from inline color/background styles only; class/stylesheet colors aren't resolved.
- Runtime ARIA states (expanded/checked/…) need a live DOM.
- Target-size (2.5.8) is approximated from inline dimensions; real geometry needs layout.
This app's colors are all in app.css (classes), so contrast is completely
uncovered by Step 2. That's the cue for Step 3.
Step 3 — Contrast audit (check-contrast)
Prompt: "audit-a11y can't see stylesheet colors — pull the real token hex
values for every text/background pair and run check-contrast on each against
WCAG AA."
Tool — mcp__neumo-ds__check-contrast (foreground + background hex → ratio +
AA/AAA pass/fail). Ran it on every text/surface pair in the design (token values
resolved from @kofile/gds-foundations):
| Text | Background | Ratio | AA (normal) |
|---|---|---|---|
Eyebrow #efe745 |
hero #160a30 |
14.45 | ✅ AAA |
Lede / stat labels #b7bcc3 |
hero #160a30 |
9.81 | ✅ AAA |
Accent words #baa8f7 |
hero #160a30 |
8.96 | ✅ AAA |
Card title/text #121212 |
white | ~17 | ✅ AAA |
Subtitle #6b7280 |
white | 4.83 | ✅ AA |
Trust row #343944 |
#f7f7f7 |
10.8 | ✅ AAA |
Placeholder #818791 (neutral-500) |
white | 3.62 | ❌ fail |
Fineprint #818791 (neutral-500, 13px) |
white | 3.62 | ❌ fail |
The finding: two elements use --color-neutral-500 (#818791) as text on
white — the input placeholders (16px) and the fineprint (13px). Both are
normal-size text, so AA needs 4.5:1; #818791 reaches only 3.62:1. Both
fail WCAG 2.2 AA (1.4.3).
This is exactly the kind of issue automated structural audits miss and a design system's own contrast tool catches — a real, shippable defect, surfaced in one call.
Step 4 — Fix, then re-verify
Prompt: "Fix the failing pair with the nearest token that passes AA, then
re-run check-contrast to confirm."
The next darker neutral, --color-neutral-600 (#6b7280), is the design's own
muted-text color (it's already what the subtitle and the .NET demo's fineprint
use). Re-checking it:
check-contrast(#6b7280, #ffffff) → 4.83 → AA: pass. ✅
So both rules moved from neutral-500 → neutral-600 in app.css:
.card input::placeholder { color: var(--color-neutral-600); opacity: 1; }
.card__fineprint { color: var(--color-neutral-600); }
Placeholders and fineprint stay visibly muted, now clear AA, and match the
design's existing muted-text token. Re-running audit-a11y still reports 0
structural violations — the page is now clean on both axes.
What automated static analysis can't catch (verify these live)
The two MCP tools are the automated backbone, but an audit isn't complete without
a live pass. Per audit-a11y's own limitations, still verify by hand:
- Keyboard + screen reader: tab order, focus-visible rings, that the select's menu announces its open/closed state (runtime ARIA the static scan can't see).
- Required-field semantics: the email's required marker is a visual
•(aria-hidden) — consideraria-required/ instructions so the requirement is conveyed non-visually (WCAG 3.3.2). Not an automated failure; a human-review item. - Target size with real layout (2.5.8), and contrast in dark mode / hover/focus states.
Good tools for the live pass: the browser's axe DevTools extension against the running app, and actual VoiceOver/NVDA navigation.
The prompt — a reusable a11y-audit loop
Run an accessibility audit on <app> with the neumo MCP server (target WCAG 2.2 AA):
1. Run the app and capture the RENDERED DOM with Playwright
(document.getElementById("root").innerHTML) — audit what ships, not the source.
For a web-component app, serialize the flattened DOM (shadow roots included).
2. Run audit-a11y on that markup for structural WCAG (labels, roles, headings,
landmarks, alt text, ARIA). Read its stated limitations — it does NOT resolve
stylesheet colors.
3. Cover the gap: resolve every text/background pair to its real token hex and run
check-contrast on each against AA (4.5:1 normal, 3:1 large). List any fail.
4. For each failure, use the nearest token that passes; re-run check-contrast to
confirm. Re-run audit-a11y to confirm structure is still clean.
5. Finish with a live pass (keyboard + screen reader + axe DevTools) for the
runtime states static analysis can't judge.
Where the MCP server shows up
audit-a11y— Step 2 (structural WCAG 2.2 scan of the rendered DOM) and the Step 4 re-check.check-contrast— Step 3 (contrast for every text/surface pair — the 1.4.3 coverageaudit-a11ycan't provide) and the Step 4 fix verification.- On the design side, the Figma toolkit has matching a11y tools —
figma_scan_code_accessibility(axe on code) andfigma_audit_component_accessibility(per-component scorecard: focus quality, non-color differentiation, color-blind simulation) — for auditing before code exists. This demo is the code-side counterpart.