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:
- Make the code change.
navigate_pageto the affected route.list_console_messages({ types: ['error', 'warn'] })— first signal.take_snapshot— cheap, gives uids for further interaction.take_screenshotwhen layout matters.- If something's wrong:
evaluate_scriptto introspect — read computed styles, count DOM nodes, inspectwindow.__NUXT__. - 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 foundor 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 throughaddEventListenercalls. 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_pagebefore 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:
- Test harnesses get cheap. When dispatching synthetic events is one tool call away, exposing a typed test API on
windowis faster than writing a Playwright spec — and the API doubles as a debug console. - 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.
- 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.