# 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.

- Source: https://oharu121.com/blog/chrome-devtools-mcp-vs-playwright-mcp-astro-blog/
- Published: 2026-08-16T22:35:56+09:00
- Tags: MCP, Claude Code, Playwright

---
## 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`:

```json title="~/.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.

*Figure — ToolSurfaces: 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](https://code.claude.com/docs/en/permissions) 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:

```json title=".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.

*Figure — PermissionGlobs: 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:

```json title=".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:

```json title="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:

```text
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:

```text
$ 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:

```text
Everything fits. 60 page(s) measured.
```

*Figure — StaleServerTimeline: 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.

*Figure — WhereStateLives: 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:

```bash
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:

```bash
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:

```bash
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

- [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](https://code.claude.com/docs/en/permissions)
- [chrome-devtools-mcp, the server that exposes the DevTools performance, network and memory surfaces as MCP tools](https://github.com/ChromeDevTools/chrome-devtools-mcp)
- [Playwright MCP, including `browser_run_code_unsafe` and the cross-engine support that decides the counter-case](https://github.com/microsoft/playwright-mcp)
- [Astro content collections, whose build-time loading is why a dev server started before an article never serves it](https://docs.astro.build/en/guides/content-collections/)
