An agent that changes a web app has to look at it. The question is how many tokens that look costs, and what it can see while it is there.

An AI agent that edits a component, a stylesheet or a route has a problem the moment it is done: it cannot tell whether the result is right. It can read the code back and reason about it, which is how a confident agent ships a card that overflows at 320 pixels, a focus ring that no longer matches hover, or a banner that loads Google Analytics before anyone clicked accept. The only honest check is the one a developer makes, open the page and look, interact, read the console, watch the network. The agent needs a browser.

How you give it one decides how expensive every check is, and how much it can see. This article compares the three ways I have used, argues for the one that won, and goes through what it unlocks. The reasoning is agent-agnostic. My setup is Claude Code, so the examples are Claude’s, but the same server speaks to Codex, Gemini CLI, Cursor and Copilot through the same protocol.

Three ways to give an agent eyes

Throwaway automation code. The agent writes a Playwright or Puppeteer script, runs it, reads the screenshot, and deletes the script. Every check is a small program: launch the browser, go to the URL, set a viewport, find an element by a selector, screenshot or print a measurement. When the selector was wrong or the page was not ready, it edits the program and runs it again.

A custom harness. You write the script once, properly: a CLI that takes a URL, a viewport, a theme and an element, and prints what the agent usually wants. This is better, and it is where most teams land after a month of the first option, but it answers only the questions you anticipated when you wrote it, and it needs maintaining like any other tool.

A browser exposed as tools. Chrome DevTools MCP is a Model Context Protocol server from the Chrome DevTools team that drives a real Chrome through the DevTools protocol and exposes it as typed tools: navigate, snapshot the page, click, fill, screenshot, evaluate a script, list network requests, read the console, record a performance trace, run Lighthouse. The agent calls a tool, gets a structured result, decides the next call. No code is written to look at the page.

The token argument

A throwaway script is expensive in a way that is easy to miss, because each individual step looks cheap.

The agent generates the script, forty to eighty lines it has to produce token by token and then pay to keep in context for the rest of the task. If anything was off, the selector, the wait, the viewport, it is edited and run again, and both versions now sit in context. The next check is a different page or a different state, so it needs a different script. Over a session that verifies a redesign in two themes, two locales and three viewports, you are paying for a dozen small programs and their outputs, none of which you keep.

The screenshot at the end of each script is the other cost. An image is read as image tokens, and an agent that only has screenshots asks for a lot of them, because a screenshot is the only way it can “see” anything.

The MCP server changes both. Each check is a tool call: a handful of tokens of arguments, no code, and a structured result. The default way to look at a page is not a screenshot but a snapshot: take_snapshot returns the page’s accessibility tree as text, every interactive element with a short id, its role, its name and its state. That is a few hundred to a couple of thousand tokens for a whole page, where the DOM would be tens of thousands and a screenshot is an image. The agent reads it the way it reads anything else, picks the element by id, and acts on it with click or fill. It asks for a screenshot only when it actually needs eyes. Measurements that a script would print, a bounding box, a computed style, a count, come from evaluate_script as JSON, in one call.

Round trips drop with it. A script is a batch: it runs to the end and the agent sees the result after the fact, so a wrong assumption half way through costs a whole re-run. A tool call is one step, and a mistake is caught at the step, not at the end.

An honest comparison also says where scripts still win.

In my own repository the agent’s instructions say to verify visual changes through the Chrome MCP first, with a fallback to the repo’s own Playwright when the MCP is down. That fallback got used, and it was the slow, expensive path every time.

Setting it up

The server runs on a Node LTS and needs the current stable Chrome. One command registers it for Claude Code, for the current user rather than a single project:

Bashclaude mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest

For any other client the configuration is the same shape, a command and its arguments:

JSON{ "mcpServers": { "chrome-devtools": { "command": "npx", "args": ["-y", "chrome-devtools-mcp@latest"] } } }

Or skip the manual step. An agent that can read documentation can install its own tools, and this is a good first thing to ask a fresh one for.

Started like that, the server launches its own Chrome with its own profile the first time a tool is called, and reuses that profile afterwards. Two flags are worth knowing on day one: --isolated gives every run a temporary profile that is deleted afterwards, the right choice for anything you would not want remembered, and --viewport=1280x720 saves the agent a resize_page call at the start of every session. When context is tight, --screenshotMaxWidth and --screenshotFormat=jpeg shrink the output of the one expensive tool.

What a session looks like

The reference lists eleven tool groups; a verification session lives in six of them: navigation, input, emulation, network, debugging and performance. The centre of the loop is the snapshot. Every input tool addresses elements by the ids from the last take_snapshot, emulate sets the viewport, the colour scheme and the throttling, and evaluate_script answers anything the tree cannot express. Snapshot, act, snapshot.

Three prompts from a Claude session that verifies a redesigned contact section, with the tools they turn into:

Claude calls new_page, emulate, take_snapshot, and answers from the tree. No screenshot yet.

hover, evaluate_script with a small function reading getComputedStyle, click on the field, press_key Tab, evaluate_script again. Two JSON results, one comparison. This is the check that catches a focus style drifting from the hover style, and it is a dozen lines of text in context, not two screenshots.

take_screenshot with the section’s id. One image, when an image is the right answer.

A profile where you are signed in

This is the use case that changes what an agent can do rather than how cheaply it does it.

The server keeps its own Chrome profile on disk, under ~/.cache/chrome-devtools-mcp/chrome-profile by default. Anything you do in that Chrome stays: sign in once, and every later session is signed in too. So the agent can open the CMS preview of a draft and check that a new embed renders, look at a protected staging environment, or walk an admin flow, all as you, without a single credential appearing in the conversation or in a script.

There are three ways to get there, in order of how much of your real browser you hand over. The server’s own profile is the default: launch it once, sign in to what the agent needs, close it, and the next tool call reuses it. This is the one I use, a dedicated Chrome that knows the CMS, Storybook on staging and the analytics property, and nothing else. Attach to a Chrome you started, with --browserUrl=http://127.0.0.1:9222: you launch Chrome with --remote-debugging-port=9222 and a non-default --user-data-dir, and you control exactly which profile is exposed. Attach to your running Chrome, with --autoConnect: Chrome 144 and newer can expose remote debugging for the running instance (chrome://inspect/#remote-debugging), and the server connects to the Chrome you already have open, tabs and all. The most convenient option, and the one to think hardest about.

Because a signed-in profile means the agent acts as you. A few flags keep that honest:

  • --allowedUrlPattern and --blockedUrlPattern fence the agent to the hosts a task needs.

  • --redactNetworkHeaders redacts the sensitive headers, cookies and authorization among them, from what get_network_request returns, so a session token never lands in the transcript.

  • --javascriptEvaluation=false removes the script tools for sessions that only need to read.

Use cases that earn their keep

Design verification at the awkward sizes. The check a developer skips because it takes eight clicks: a locale with longer words, the dark theme, the 320 pixel floor. The agent runs emulate three times and snapshots each, and only screenshots the one that wrapped.

Consent and analytics audits. Open the site in a fresh isolated browser context, which new_page creates on request, so the run starts with an empty cookie jar. Accept one category in the banner, then list_network_requests filtered to the analytics hosts. The agent reads the consent parameters off the hits, checks localStorage through evaluate_script, withdraws consent and confirms the storage is empty again. This is the check that found a tag firing before consent on my own site, and it is exactly the kind of multi-step, state-dependent probe that a throwaway script does badly.

Reproducing a bug report. The steps from the ticket become navigate_page, click and fill calls, list_console_messages returns the error with its stack, and get_css_styles explains why the element looks wrong by listing the rules that actually matched, in cascade order. The agent arrives at the fix with evidence instead of a guess.

Performance with a cause attached. performance_start_trace with a reload records a trace that comes back with its insights named, the LCP breakdown, render-blocking requests, layout shifts with the elements that shifted, and performance_analyze_insight drills into each one. Add emulate with CPU and network throttling and the agent sees the page the way a mid-range phone on a train does.

And when the flow is verified, --experimentalScreencast turns it into a short video for the pull request, a better review artefact than a paragraph describing it.

What you only learn by running it

A few things from my own notes, agent-agnostic because they are facts about Chrome rather than about Claude:

  • resize_page resizes the shared window, so it changes every open tab. Do the smallest viewport last and restore the size afterwards.

  • A background tab freezes CSS animations. An element measured in a tab that is not in front is mid-keyframe; bring the tab to front with select_page before measuring anything that moves.

  • A profile remembers page zoom. Mine had localhost at 125 percent, which turned an emulated 320 pixel viewport into 256 CSS pixels and produced false wraps. Emulate wider and read innerWidth before trusting a narrow pass.

  • Two tool calls fired in parallel on a cold server each try to launch a Chrome, and the second one fails on the first one’s profile lock. The first call of a session is one call.

  • localhost may resolve to IPv6 and land on another app listening there. Point the agent at 127.0.0.1.

None of these is a reason not to use the server. They are the kind of thing a team writes down once, and an agent with a memory writes down for you.

Where this lands

The browser was never the hard part of giving an agent eyes. The hard part is the cost of each look and what each look can see. A throwaway script makes every look a program, and the program can only see what you told it to print. A browser exposed as tools makes every look a call, lets the agent read the page as text and ask for pixels only when pixels matter, and puts the network, the console, the traces and your own signed-in sessions one call away. For the exploratory, state-dependent verification that follows every real change, that is the difference between an agent that reports what it did and one that shows you.