← Neumo Design System docs

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 (h1h2), 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:

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-500neutral-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:

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