Tags
The Playwright logo, two overlapping theatre masks with a red frowning one behind a green smiling one, beside the Playwright wordmark in black, on a white card

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:

~/.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.

Shared surface, and the exclusive edgesA wide band across the top lists six capabilities both servers provide: navigate, click and type, screenshot, resize viewport, read console, evaluate JavaScript. Below it two cards sit side by side. The Chrome DevTools MCP card lists performance trace, Lighthouse audit, heap snapshot and CPU or network throttling, and is marked Chromium only. The Playwright MCP card lists find by role and text, run Playwright code, tabs as first-class and select dropdown options, and is marked Chromium, Firefox, WebKit.Both servers do thisNavigateClick and typeScreenshotResize viewportRead consoleEvaluate JSChrome DevTools MCPPerformance traceLighthouse auditHeap snapshotCPU / network throttlingChromium onlyPlaywright MCPFind by role and textRun Playwright codeTabs as first-classSelect dropdown optionsChromium, Firefox, WebKit
Two thirds of the surface is shared. The rows that decide between the two servers are the ones only one of them has.

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_unsafe lets 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_find is genuinely better than reading a snapshot tree and picking a uid out of it.
  • You need several tabs at once. browser_tabs treats 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:

.claude/settings.json
"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.

What each glob rule actually shortensTwo rows, each running left to right. The allow row starts with 13 hand-listed entries, passes through an allow rule, and ends with one entry while 29 tools remain in context. The deny row starts with 24 tools present in every request, passes through a deny rule, and ends with zero tools and their schemas gone from context. A closing line reads: allow shortens the file, deny shortens the request.Allow glob13 entrieslisted by handallow: mcp__…__*1 entry29 tools still in contextDeny glob24 toolsin every requestdeny: mcp__…__*0 toolsschemas gone from contextAllow shortens the file. Deny shortens the request.
An allow glob is housekeeping for the settings file. A deny glob is the one that takes weight out of every request.

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:

.claude/settings.json
"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:

package.json
"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 dev
Dev 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.
A dev server older than the article it was asked aboutA timeline with three markers. At 16:52 the dev server starts with 30 articles in its collection. At 16:59 an article is committed, and the running server never sees it. Later a figure check runs and measures 57 of 60 pages. A band above the timeline shows that what the server knows is the content as it stood at boot, unchanged across all three points. The outcome below reads: three 404s for a published article.What the dev server knows: the content as it stood at boot16:52Dev server starts30 articles in the collection16:59Article commitsThe running server never sees itlaterFigure check runs57 of 60 pages measured3 × 404 for a published article
The article was committed seven minutes after the server booted, and the server carried on answering from the collection it read at start.

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.

Which process holds a copy of your sourceYour content on disk sits at the top. Two arrows lead down from it. The left arrow, labelled copied once at boot, reaches the Astro dev server, which holds a snapshot of the content collection taken at start and is marked as able to answer with stale data. The right arrow, labelled fetched over HTTP every time, reaches the MCP browser, which holds no copy of your source at all and is marked as unable to go stale. The closing rule reads: restart the process that holds a copy, leave the one that does not.Your content on diskcopied once, at bootfetched over HTTP, every timeAstro dev serverA snapshot of the contentcollection, taken at startCan answer with stale dataMCP browserHolds no copy ofyour source at allCannot go staleRestart the process that holds a copy. Leave the one that does not.
The lifecycle rule falls out of one fact: only the process holding a copy of your source can hand back something out of date.

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:

Terminal window
lsof -nP -iTCP:4321 -sTCP:LISTEN

A pattern that matches nothing looks exactly like a clean machine

Clearing those six turned up the better lesson. The obvious command did nothing:

Terminal window
pkill -f "astro preview" # matches zero processes

The 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:

Terminal window
ps -p <pid> -o command= | tr ' ' '\n' | grep user-data-dir

A 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.
  • playwright the 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_URL may later land on.
  • A process pattern that matches nothing looks exactly like a clean machine. pkill -f "astro preview" and pgrep -f "astro dev" both match zero processes, because what the process table holds is astro.mjs. Neither reports anything, which is the problem. Match what is in the process table, not what you type in a shell.

References

Share this article