Back to Insights
20 May 20265 min readAI Agents

Drove our dashboard through an MCP. Found 5 UX failures hiding in plain sight.

We shipped a feature called "background resume build" — submit a build, get redirected to the dashboard immediately, watch live progress cards while doing other work. Six phases (A→F), tests passing, manual QA fine, dev deploy green.

Then we drove the dashboard through chrome-devtools-mcp — Chrome controlled by an MCP server, instructed by an agent to navigate, click, and observe. One pass surfaced five compounding UX failures the human QA missed.

This is the audit, the fixes, and what this kind of automation actually unlocks.

What chrome-devtools-mcp lets you do

chrome-devtools-mcp lets an agent drive a real Chrome session. Not Playwright synthetic-DOM scripting — actual Chrome, with the agent observing screenshots, network traffic, console messages, and DOM state. The agent decides what to click next based on what it just saw.

The interesting capability isn't "automated clicking." It's automated reading. The agent sees the dashboard the way a confused user would, can't predict what's wrong, and reports what looks weird.

The five failures it caught

1. Every completed build rendered TWICE
   — once as a green "Just generated" card in the recently_completed rail
   — once as a normal Application card in the proper grid below
   Dev hit 19 stacked green cards after one regression-suite day.

2. Stats triplet (Applications / Résumés / Credits) didn't refresh on build completion.
   The just-built application was reachable only through the placeholder card.

3. Polling firehose.
   Poll request asked for status=queued,running,done,failed every 3s.
   Steady-state payload: ~50KB of recently_completed JSON
   even when nothing was in flight.

4. Stale aria-live announcement.
   "Building your resume in the background…" stayed stuck
   indefinitely after completion — no follow-up toast cleared it.

5. Identical card text on every green card.
   meta.input_summary was returned by the API but never rendered.
   Every card showed "Building resume — Complete" instead of
   "Senior Engineer @ Stripe — Complete".

None of these were in the spec. They emerged from the spec touching production.

Phase A — the fix that compounds

The temptation with this list is to write five separate fixes. The right move is the single seam that addresses 1, 2, and 4 at once.

The fix: a CustomEvent on the build-completion transition. Every consumer of the dashboard state listens to it independently. No new shared state, no React Query, no prop drilling.

// useInFlightJobs.ts — on every in_flight → terminal transition:
window.dispatchEvent(
  new CustomEvent('reqsume:buildCompleted', {
    detail: { job_id, status, applicationId }
  })
);

// StatsCards.tsx, useCreditsBalance, ApplicationGrid each register:
useEffect(() => {
  const handler = (e: CustomEvent) => refetch();
  window.addEventListener('reqsume:buildCompleted', handler);
  return () => window.removeEventListener('reqsume:buildCompleted', handler);
}, []);

That single event collapses the duplicate-rendering bug (failure 1), the stale-stats bug (failure 2), and the stuck-aria-live bug (failure 4) into one mechanism. The pattern already existed in the codebase (reqsume:profileChanged fan-out) — chrome-devtools-mcp just made it obvious that the build-completion side needed the same treatment.

For the polling firehose (failure 3): split into two queries.

// Before: ?status=queued,running,done,failed  every 3s
// After:
//   ?status=queued,running   every 3s (stops when in_flight empties)
//   ?status=done,failed       fetched once on mount + on buildCompleted

Steady-state payload dropped by ~90%.

For the identical-text bug (failure 5): the API was already returning meta.input_summary. The card just wasn't reading it. One-line fix.

Phase B — the freshness window

Phase A's application_id dedup only covered the rendered grid page (6 cards). Two cases still leaked stale green cards:

  1. Pagination. Applications on grid page ≥ 2 don't appear in the dedup set; their placeholder persists.
  2. Orphan rows. Regression suites delete applications during teardown but the jobs row sticks around for server-side 24h retention. The green card points at a now-deleted application.

The fix: a 5-minute UI freshness window at the hook boundary.

// useInFlightJobs.ts — filter recently_completed by freshness on every set
const fresh = recently_completed.filter(row => {
  const t = row.completed_at ?? row.submitted_at;
  return Date.now() - new Date(t).getTime() < 5 * 60 * 1000;
});

Why at the hook, not the component: every page that consumes recently_completed (the dashboard, the navbar badge, future per-action surfaces) gets the same rule for free. One seam, one rule.

Why 5 minutes: long enough to survive a slow grid refetch + tab switch + browser refresh during a build. Short enough that opening the dashboard hours later doesn't show stale rails.

The meta-lesson

Manual QA tests the happy path because that's what manual QA is. Live regressions, pagination edges, stale-data drift, polling cost — those are hidden in the steady-state behavior of the running app, not in the user-story acceptance criteria.

chrome-devtools-mcp (and similar MCP-driven browser automation) shifts the QA loop in a specific way: the agent observes what's there without knowing what should be there. It reports 19 stacked green cards as 19 stacked green cards. A human QA running the user story would notice one card looks redundant and move on.

The bug class chrome-devtools-mcp catches is the one no story-driven QA catches: the steady-state aftermath. Things that accumulate. Polling that doesn't stop. Aria-live regions that don't clear. Pagination that doesn't dedupe.

If you're shipping React-heavy interactive surfaces and your test pyramid stops at unit + Playwright user-flow tests, this is the gap.

What's runnable today

# install chrome-devtools-mcp
npm install -g chrome-devtools-mcp

# wire it into your agent config (Claude Code example)
{
  "mcpServers": {
    "chrome-devtools": { "command": "chrome-devtools-mcp" }
  }
}

# then ask the agent: "open the dashboard, find what looks broken"

Sign into the surface manually in Chrome first — the agent drives your authenticated session, never accepts credentials.

Where the rest lives

More notes on what AI-driven browser inspection actually changes in the QA loop: deemwar.com

— deemwar

References

  • Anthropic — Introducing the Model Context Protocol (2024). Announcement post
  • Model Context Protocol — Official specification and docs (2024). modelcontextprotocol.io
  • Model Context Protocol — GitHub organization (SDKs, reference servers) (2024). modelcontextprotocol on GitHub
  • ChromeDevTools — chrome-devtools-mcp: MCP server for Chrome DevTools (2025). ChromeDevTools/chrome-devtools-mcp on GitHub
  • Chrome DevTools team — Chrome DevTools Protocol reference (ongoing). chromedevtools.github.io/devtools-protocol
  • Nielsen Norman Group — Scrolling and Attention (2018). nngroup.com
  • Nielsen Norman Group — Dashboards: Making Charts and Graphs Easier to Understand (2020). nngroup.com
MCPagentsfrontendUX

Found this useful?