DOCEXESIV.DEVREV2026.07
← RETURN // FIELD NOTES

Closing the loop with devtools-proxy

Ref
FLD-46BD7E
Published
2026-04-26
Kind
DEVLOG
Project
Sidebar Agent
Author
Roman Sanine

Here's the failure mode I want to retire.

Agent writes a Vue component. The diff looks correct — props are typed, logic is right, template reads sensibly. Agent says "done." User reloads the page. Panel is blank. Console has a hydration mismatch. The component never mounted because of an SSR-incompatible import the agent couldn't have known about from reading the file.

This isn't a prompting problem. The agent did everything it could from inside its sandbox. The real problem is that the sandbox doesn't include the running app.

devtools-proxy-mcp fixes that.

What it gives the agent

A Model Context Protocol server fronting a Chrome DevTools session. Each tool maps to a real browser operation. The full surface, in roughly the order I use them:

Tool What it does
navigate_page Load a URL in an owned tab
take_snapshot Structured a11y tree with element uids
take_screenshot PNG/JPEG of the viewport
list_console_messages All console.* since last navigation, filterable by type
list_network_requests XHR/fetch/document, filterable by resource type
evaluate_script Run arbitrary JS in the page world, return JSON
click / fill Interact via uid from a snapshot
wait_for Block until text appears

Tab ownership is sticky — a workflow that navigates, waits, screenshots, then dispatches synthetic pointer events all hit the same DOM.

A typical turn

The shape of a frontend task now:

  1. Make the code change.
  2. navigate_page to the affected route.
  3. list_console_messages({ types: ['error', 'warn'] }) — first signal.
  4. take_snapshot — cheap, gives uids for further interaction.
  5. take_screenshot when layout matters.
  6. If something's wrong: evaluate_script to introspect — read computed styles, count DOM nodes, inspect window.__NUXT__.
  7. Iterate.

CLAUDE.md codifies it: "Frontend changes must be verified with devtools-proxy MCP." Done = no console errors, snapshot matches intent, screenshot looks like what was asked for.

A worked example

Earlier this session I migrated a scratchpad page from the standalone ~/scratchpad project into a Nuxt layer. The page was supposed to mount a Paper.js infinite canvas. After moving the files, the route loaded — but a navigation later, the page rendered an error boundary instead.

Without devtools-proxy, the loop would have been: agent thinks file move is correct, asks user to test, user reports it's broken, agent guesses, etc.

With devtools-proxy:

> evaluate_script `() => document.getElementById('app-main')?.innerHTML.slice(0, 1500)`
=> "<div class='flex-1 flex flex-col items-center justify-center p-8 gap-4'>...
   <pre>Error: Injection `Symbol(TooltipProviderContext)` not found...</pre>"

The error boundary's text was the diagnosis. From there, comparing what the page imported before and after the move pointed at a composable I'd added that pulled paper.js at module top — paper needs window, breaks SSR. Wrap the import in onMounted + dynamic import(), error boundary clears, canvas mounts. Two round-trips.

The harness pattern

The other thing that drops out of having evaluate_script: agents can build their own test harnesses for things humans would normally have to test by hand.

For the same scratchpad page, the agent installed a window.__scratchpad object on mount:

window.__scratchpad = {
  ready: () => !!getWrapper() && !!canvas.value,
  tools: () => allTools.map(t => t.name),
  tool: (name) => selectTool(name),
  itemCount: () => paperMod.paper?.project?.activeLayer?.children?.length ?? 0,
  stroke: async (points, opts) => { /* dispatches PointerEvents */ },
  gesture: async (touches, opts) => { /* simultaneous multi-touch */ },
  clear: () => clearCanvas(false),
}

Then verified end-to-end without a human ever touching the canvas:

> evaluate_script `async () => {
    const h = window.__scratchpad
    h.tool('Diagram')
    const pts = []
    for (let x = -100; x <= 100; x += 5)
      pts.push([cx + x, cy + Math.sin(x/25)*30])
    return await h.stroke(pts, { type: 'pen', stepMs: 6 })
  }`
=> { delta: 1, after: 2 }

A single pen stroke produced one new path on the active layer. Multi-touch pinch raised the zoom from 1 → 3.4. The harness lives on the page in the same commit as the feature, deletable later, but right now it's the verification.

What's still hard

  • Session drops. Under load the MCP session occasionally returns No page found or screenshot timeouts. Re-navigating recovers, but it's noise.
  • Coordinate frames. Paper.js binds its pointer listeners to the inner <canvas>, not the wrapper. Dispatching to the wrapper silently produced zero results until I traced through addEventListener calls. Coords are canvas-local, not viewport-local. Off-by-one of that kind costs a debug round-trip.
  • Iframe and shadow-DOM blind spots. document.querySelector('canvas') doesn't traverse shadow roots. Recursive walk works, but you have to remember.
  • Hydration timing. Some pages legitimately need 1-2s after navigate_page before they're stable. Polling is the right answer; sleeping isn't.

None of these are dealbreakers. They're the texture of working with a real browser.

What it changes structurally

Three patterns I didn't expect:

  1. Test harnesses get cheap. When dispatching synthetic events is one tool call away, exposing a typed test API on window is faster than writing a Playwright spec — and the API doubles as a debug console.
  2. CSS regressions surface in seconds. I migrated this site's styling through Tailwind v3 → v4 → Bootstrap in one session. Each pass got navigated, screenshotted, console-checked, and regression-spotted before I committed it. Without the loop, each pass would have been a guess shipped to the user.
  3. The agent stops asking me to verify things. It verifies them itself.

Where this leads

Same primitive used in-session for verification can run on a schedule. A nightly agent that navigates each route, screenshots, checks the console, and opens an issue when something drifts — that's a few lines of orchestration on top of what's already here. That's the version of "frontend testing" that doesn't depend on someone remembering to write the test.

Sign in to react
Comments
No comments yet.
Comments are reviewed before appearing.
EXESIV.DEV — EXESIV lab builds agentic systems and components for the self-running business — autonomous, policy-driven operations, logged in the open across four sections: Almanac, Works, Field Notes, Seeds.