
How do Chrome DevTools MCP and Playwright MCP differ, and why I chose Chrome DevTools for my Astro blog
Chrome DevTools MCP and Playwright MCP expose different surfaces to a coding agent. What each gives you, and why an Astro blog kept one and denied the other.
On this page
Introduction
I opened .claude/settings.json to approve yet another browser tool and found
two browser-automation servers sitting in it rather than one. Both had been
enabled for months, both could click and screenshot, and nothing anywhere in the
repo said which one to reach for. The permission list underneath them had grown
to 23 hand-added entries, one line at a time, each added the first time a new
tool raised its first prompt, and it was still incomplete.
The outcome: this blog now runs Chrome DevTools MCP only, Playwright MCP is denied, and the deny rule turned out to work in a way neither of us had assumed. It does not block those tools. It deletes them from the agent’s context entirely, which makes a permission rule a way to buy back context rather than only a way to stay safe.
This article walks through what the two servers actually hand a coding agent, where they genuinely differ, why this project kept the one it did, when the other is the better pick, and how the permission side collapsed from 23 entries to two lines. At the end there is a trap that surfaced while verifying all of it, and which has nothing to do with MCP at all.
Two browser servers were loading 53 tool schemas a turn
Both servers were on machine-wide, in ~/.claude/config.json:
"enabledPlugins": { "playwright@claude-plugins-official": true, "chrome-devtools-mcp@claude-plugins-official": true}An MCP server does not cost anything while idle. What it costs is the tool schemas it publishes into every request, because the agent has to be told what it can call before it can decide whether to call it. Playwright MCP publishes 24 tools. Chrome DevTools MCP publishes 29. That is 53 schemas riding along in every single turn, in a repository where perhaps four of them ever got used in a session.
Nothing warns about this. The servers work, the tools appear, and the cost is spread thinly enough across every request that it never shows up as a problem to fix.
What each server hands the agent
The tempting summary is that one measures and the other drives. That summary is wrong, and it is worth killing before it spreads: both servers click, type, hover, screenshot, resize, evaluate JavaScript, and read the console. Roughly two thirds of their surface is the same work under different names, so a comparison built on “can it click” produces a tie.
The difference is in what sits outside that shared middle.
| Capability | Chrome DevTools MCP | Playwright MCP |
|---|---|---|
| Navigate, click, type, hover, screenshot | Yes | Yes |
| Read console, evaluate JavaScript | Yes | Yes |
| Performance trace and insight analysis | performance_start_trace, performance_analyze_insight |
No equivalent |
| Lighthouse audit | lighthouse_audit |
No equivalent |
| Network request inspection | list_network_requests, get_network_request |
browser_network_requests |
| Heap snapshot | take_heapsnapshot |
No equivalent |
| CPU and network throttling | emulate |
No equivalent |
| Find elements by role and text | take_snapshot returns a tree |
browser_find, purpose-built |
| Run arbitrary automation code | No | browser_run_code_unsafe |
| Multiple tabs as first-class objects | list_pages, select_page |
browser_tabs |
| Browser engines | Chromium only | Chromium, Firefox, WebKit |
Read down the exclusive rows and the two servers stop looking like competitors. Chrome DevTools MCP hands the agent the DevTools panels: the performance timeline, the network waterfall, the memory profiler, Lighthouse. It is the browser’s own instrumentation, exposed as tools, and it is Chromium only because those panels are Chromium’s.
Playwright MCP hands the agent a test harness. Role-based element finding,
arbitrary Playwright code through browser_run_code_unsafe, tab management, and
three rendering engines instead of one. It is built for driving a site the way a
test suite drives it.
Why Chrome DevTools MCP won the slot
Be clear about how this decision was made, because it changes how much weight it deserves. The comparison above was produced by the agent, from capability coverage, and I picked from the options it presented. Nothing here was benchmarked. Neither server was timed, neither was measured for token cost per call, and no head-to-head task was run twice. What follows is a fit argument for one workload, not a verdict on which project is better engineered.
The workload is this blog, and what actually gets verified in a browser here is narrow and repetitive:
- Does the table of contents follow the reader at the right scroll offsets?
- Does a code block still stack correctly under the sticky header?
- Do the SVG figure labels overflow their viewBox once translated into Japanese or Traditional Chinese?
- Does the search box behave when an IME is mid-composition?
- Does the page still hit its Core Web Vitals after a layout change?
Every one of those is measuring a Chromium page, not automating a flow across browsers. The last one is only answerable with a performance trace or a Lighthouse run, and Playwright MCP has neither. Nothing in the list needs Firefox, WebKit, or a role-based locator badly enough to outweigh that.
So the deciding capability was the instrumentation surface, and the cost of being wrong was low: this is a static site, and the browser work is verification rather than a shipped test suite.
When Playwright MCP is the right pick
The honest counter-case, because the reasoning above inverts cleanly:
- You need Firefox or WebKit. Chrome DevTools MCP is Chromium only and cannot be talked into being otherwise. If a bug reproduces in Safari, that settles it before any other row matters.
- You already have a Playwright test suite.
browser_run_code_unsafelets an agent write code in the same API your tests are written in, which is a meaningful saving that has nothing to do with tool counts. - Your work is flows, not measurements. Multi-step forms, authentication,
cart checkouts. Role-based finding through
browser_findis genuinely better than reading a snapshot tree and picking a uid out of it. - You need several tabs at once.
browser_tabstreats that as a first-class concept.
This project hits none of those, which is why the choice was easy. A project that hits any of them should read the same table and reach the opposite conclusion.
One exception is worth naming, because it caught the agent out on an earlier piece of work: WebKit cannot be driven over the Chrome DevTools Protocol at all. Verifying a Safari-only composition-event ordering quirk on this blog had to be done by hand, and no choice of MCP server would have changed that.
Twenty-three permission entries became one glob
With the choice made, the permission list was still the mess that started this. Thirteen entries named Chrome DevTools tools, ten named Playwright tools, and every browser tool that had not yet been used was a prompt waiting to happen.
The permissions documentation has the rule that fixes it:
Allow rules accept tool-name globs only after a literal
mcp__<server>__prefix. The server segment must be glob-free so the rule names a specific server you configured.
So the thirteen Chrome DevTools lines collapse into one:
"allow": [ "mcp__plugin_chrome-devtools-mcp_chrome-devtools__*"]The glob-free requirement is the part worth remembering. A rule like
mcp__* looks like it should allow every MCP tool and does not; the docs say an
unanchored allow glob “is skipped with a warning and doesn’t auto-approve
anything”. The server segment has to name a real server, which is what keeps a
convenience wildcard from quietly becoming a blanket approval.
To prove the glob rather than a leftover entry, the test has to use a tool that
was never on the old list. list_network_requests had never been approved
individually. It ran with no prompt.
The deny rule removed 24 tools from context
The other half is one line:
"deny": [ "mcp__plugin_playwright_playwright__*"]A deny rule reads like a gate: the agent asks, the rule says no. The docs describe something else.
A tool matched by a bare-name glob deny rule is removed from Claude’s context.
That is a different mechanism with a different payoff. The 24 Playwright tools did not become forbidden. They stopped existing. Their schemas left the request the moment the rule was saved, with no restart, and the running session simply stopped having them.
A permission file is also a context budget
A rule written to keep an agent safe is the obvious use. A rule written to keep an agent small is the one almost nobody writes, and on this repo it was worth 24 schemas a turn for a single line of JSON. The saving is not theoretical, it is not deferred to the next session, and it costs nothing to verify: the tools are either in the list or they are not.
Worth stating plainly: this is a project-scoped rule. The Playwright plugin stays enabled machine-wide, because other repositories may want it, and a deny in one repo’s settings has no opinion about that.
Two different things are called playwright
The trap this whole change invites, and it is a good one:
"devDependencies": { "playwright": "^1.62.1"}That is the Playwright library, not the MCP server, and this blog leans on it
hard. scripts/check-figure-fit.ts launches Chromium through it to measure
whether translated SVG labels overflow their viewBox, which is what pnpm figures:fit runs. The Safari composition testing mentioned earlier drives real
IME composition through a CDP session opened by that same library.
Denying mcp__plugin_playwright_playwright__* says nothing about any of that.
The two share a name and nothing else: one is a server publishing tools into an
agent’s context, the other is a package a script imports. A future reader who
sees the deny and removes the dependency breaks the figure check, and no type
check or build would catch it, because the script only runs on demand. That is
why the project rules name the package, the script, and the command explicitly
rather than trusting the distinction to be obvious.
Bonus: a stale dev server 404s a published article
This has nothing to do with MCP. It surfaced while verifying the change, it cost real time, and it is the more generally useful finding of the two.
Running the figure check against a local server produced this:
FAIL astro-like-counter-upstash-redis-vercel-serverless-route [en] — 404 at http://localhost:4321/blog/astro-like-counter-upstash-redis-vercel-serverless-route/FAIL astro-like-counter-upstash-redis-vercel-serverless-route [ja] — 404 at http://localhost:4321/ja/blog/astro-like-counter-upstash-redis-vercel-serverless-route/FAIL astro-like-counter-upstash-redis-vercel-serverless-route [zh-tw] — 404 at http://localhost:4321/zh-tw/blog/astro-like-counter-upstash-redis-vercel-serverless-route/No overflow found, but some pages could not be measured. 57 page(s) checked.The article was committed, its status was 'published', and its publishedAt
was in the past. Every instinct says routing bug or frontmatter bug, and both are
wrong. The article was fine. The server was old.
astro dev runs as a persistent server here, and it snapshots the content
collection when it boots. This one had started at 16:52; the article’s commit
landed at 16:59. A server that starts before an article exists never serves that
article, and it does not notice.
Two things made it hard to see. The first is that pnpm dev does not start a
fresh server when one is already up:
$ pnpm devDev server already running at http://localhost:4321 (pid 41460)It prints that and exits 0. A green exit code there means “reused a server of unknown age”, which is exactly the kind of successful-looking output worth distrusting.
The second is that the tell was buried in the summary line. 57 page(s) checked
against 30 articles across three locales is three pages short, and the page count
was the only number in the output that knew anything was wrong. After pnpm stop && pnpm dev, the same command said:
Everything fits. 60 page(s) measured.Why the browser beside it could not have the same problem
The interesting part is the contrast. Two long-lived processes were involved in this session: the Astro dev server, and the Chromium instance the MCP server keeps alive. They look equivalent. Their correct handling is opposite, and the reason is not convenience.
The dev server holds a copy of my content. The browser holds none of it. The browser fetches over HTTP every time it is pointed somewhere, so a browser that has been open for three days still shows the current truth. It has nothing of mine cached that could go stale, which means leaving it running cannot produce a wrong answer. Restarting it clears nothing, because there was nothing to clear.
So the rule follows from where the state lives rather than from what a restart costs:
| Astro dev server | Preview server | MCP browser | |
|---|---|---|---|
| Holds a copy of your source | Yes, snapshotted at boot | No, but serves a build that is one | No, fetches over HTTP |
| Can give a stale answer | Yes | Yes, from the build | No |
| Cleared by | Restarting it | Rebuilding, not restarting | Nothing to clear |
| Leaving it running costs | Wrong results | A held port, silently | Nothing |
| Correct habit | Stop it when testing is done | Stop it when testing is done | Leave it alone |
The preview server is a third case, not a second dev server
The preview server looks like the same animal and is not. It serves the built
output rather than the content collection, so it holds no copy of the source and
its own age tells you nothing. What goes stale is the build it is serving.
Restarting a preview server re-serves the same bytes; the fix is pnpm build.
Debugging a preview 404 by bouncing the process is time spent on the wrong
object.
Its real hazard is different again, and it is a quiet one. A busy port is not
an error to a preview server: it takes the next one and says nothing. Repeated
runs land on 4322, 4323, 4324 and upward, and every earlier process stays alive.
Six of them accumulated here across a single day and were still holding six ports
three days later, each answering 404 to every path, because astro preview
does not work with a deployment adapter at all.
That is worse than untidy on this repo specifically. pnpm figures:fit reads
FIT_BASE_URL=http://localhost:4321, so a squatter on that port would have had
the figure check measure a days-old build and report 404s indistinguishable from
missing content. The check that finds it is one line:
lsof -nP -iTCP:4321 -sTCP:LISTENA pattern that matches nothing looks exactly like a clean machine
Clearing those six turned up the better lesson. The obvious command did nothing:
pkill -f "astro preview" # matches zero processesThe real command line is .../astro/bin/astro.mjs preview, so the pattern never
matched, pkill exited quietly, and all six kept running. A process-matching
pattern that matches nothing is indistinguishable from a machine with nothing to
match — no error, no output, no non-zero exit. pkill -f "astro.mjs preview"
killed all six immediately.
The same mistake had already shipped once that day, in a freshness check written
as pgrep -f "astro dev", which reports an empty result while a dev server is
answering requests. Both were the same error: matching a friendly name that only
ever existed in the shell history, not in the process table.
There is also a reason not to restart the browser, and it is not the one the project rules used to give. Those said the browser was kept alive to stay warm, which is a convenience argument and a weak one; the agent repeated it until I asked why a cheap restart mattered, and it did not survive the question. The real reason is that a concurrent session may be driving that browser, and a process ID does not say whose it is. When one does need killing, the profile path is what identifies it as disposable:
ps -p <pid> -o command= | tr ' ' '\n' | grep user-data-dirA path under ~/.cache/chrome-devtools-mcp/ is the server’s own throwaway
profile. Anything else may be a real browser with real tabs open.
Summary
- Chrome DevTools MCP and Playwright MCP overlap on about two thirds of their surface. Both click, type, screenshot and evaluate JavaScript, so a comparison built on those ends in a tie.
- They differ in what they expose beyond that. Chrome DevTools MCP gives an agent the DevTools instrumentation: performance traces, Lighthouse, network, heap, throttling, Chromium only. Playwright MCP gives it a test harness: role-based finding, arbitrary automation code, tabs, and three engines.
- This blog kept Chrome DevTools MCP because its browser work is measurement, not cross-browser automation. That is a fit argument for one workload produced from capability coverage, not from benchmarks. A project needing WebKit, or one with an existing Playwright suite, should reach the opposite conclusion from the same table.
- An allow glob works only after a literal, glob-free
mcp__<server>__prefix. That collapsed 13 enumerated entries into one line;mcp__*would have been skipped with a warning instead. - A deny glob removes tools from context rather than blocking them. One line of JSON took 24 tool schemas out of every request, immediately, with no restart. Permission rules are a way to keep an agent small, not only a way to keep it safe.
playwrightthe npm package is not Playwright MCP. Denying the server has no bearing on a script that imports the library, and conflating them breaks things no build step checks.- Stop the dev server when testing is done; leave the MCP browser alone. The dev server snapshots your content at boot and can therefore lie to you. The browser caches nothing of yours and cannot.
- A preview server is a third case rather than a second dev server. Its own
age means nothing, because what goes stale is the build it serves, so the fix
is a rebuild and never a restart. Stop it anyway: a busy port makes it take the
next one silently, so orphans stack up holding ports that a fixed
FIT_BASE_URLmay later land on. - A process pattern that matches nothing looks exactly like a clean machine.
pkill -f "astro preview"andpgrep -f "astro dev"both match zero processes, because what the process table holds isastro.mjs. Neither reports anything, which is the problem. Match what is in the process table, not what you type in a shell.
References
- Claude Code documentation: configure permissions, including the rule that allow globs are accepted only after a literal glob-free
mcp__<server>__prefix and that a glob deny rule removes the tool from context - chrome-devtools-mcp, the server that exposes the DevTools performance, network and memory surfaces as MCP tools
- Playwright MCP, including
browser_run_code_unsafeand the cross-engine support that decides the counter-case - Astro content collections, whose build-time loading is why a dev server started before an article never serves it