# Chrome DevTools MCP と Playwright MCP はどう違うのか — Astro ブログで Chrome DevTools を選んだ理由

> Chrome DevTools MCP と Playwright MCP がコーディングエージェントに渡すものは違います。それぞれが何を提供するのか、そして Astro ブログが一方を残して他方を拒否した理由。

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

---
## はじめに

また 1 つブラウザツールを許可しようと `.claude/settings.json` を開いたところ、ブラウザ自動化サーバーが 1 つではなく 2 つ入っていることに気づきました。どちらも何か月も有効なままで、どちらもクリックとスクリーンショットができ、どちらを使うべきかはリポジトリのどこにも書かれていませんでした。その下の権限リストは**手作業で追加した 23 件**にまで膨らんでいました。新しいツールが初めて確認を求めるたびに 1 行ずつ足したもので、それでもまだ足りていませんでした。

結果として、このブログは現在**Chrome DevTools MCP のみ**を使い、Playwright MCP は拒否されています。そしてこの deny ルールは、私たちのどちらも想定していなかった動き方をしました。ツールをブロックするのではなく、**エージェントのコンテキストから完全に削除する**のです。つまり権限ルールは、安全を保つ手段であると同時に、コンテキストを取り戻す手段でもあります。

本記事では、2 つのサーバーがコーディングエージェントに実際に何を渡すのか、どこが本当に違うのか、このプロジェクトがなぜ一方を選んだのか、もう一方が適する場面はどこか、そして権限まわりが 23 件から 2 行に縮んだ経緯を整理します。最後に、検証中に浮かび上がった MCP とはまったく関係のない罠を 1 つ紹介します。

## ブラウザサーバー 2 つで 1 ターンあたり 53 個のツールスキーマ

どちらのサーバーも `~/.claude/config.json` でマシン全体に対して有効でした。

```json title="~/.claude/config.json"
"enabledPlugins": {
  "playwright@claude-plugins-official": true,
  "chrome-devtools-mcp@claude-plugins-official": true
}
```

MCP サーバーは待機中には何のコストもかかりません。コストになるのは**毎リクエストに載せるツールスキーマ**です。エージェントは呼ぶかどうかを判断する前に、何を呼べるのかを知らされている必要があるからです。Playwright MCP は 24 個、Chrome DevTools MCP は 29 個のツールを公開します。**毎ターン 53 個のスキーマが同乗している**ことになります。1 セッションでせいぜい 4 個しか使わないリポジトリでの話です。

これについて警告は出ません。サーバーは動き、ツールは現れ、コストは毎リクエストに薄く広がっているので、修正すべき問題として表に出てくることがありません。

*Figure — ToolSurfaces: 機能の 3 分の 2 は共通です。2 つのサーバーを分けるのは、片方にしかない行です。*

## それぞれのサーバーがエージェントに渡すもの

「一方は計測し、他方は操作する」とまとめたくなりますが、それは誤りで、広まる前に潰しておく価値があります。**どちらのサーバーもクリック、入力、ホバー、スクリーンショット、画面サイズ変更、JavaScript の実行、コンソールの読み取りができます。**機能のおよそ 3 分の 2 は名前が違うだけの同じ仕事なので、「クリックできるか」を基準にした比較は引き分けにしかなりません。

違いは、その共通部分の外側にあります。

| 機能 | Chrome DevTools MCP | Playwright MCP |
| --- | --- | --- |
| ページ遷移、クリック、入力、ホバー、スクリーンショット | あり | あり |
| コンソール読み取り、JavaScript の実行 | あり | あり |
| パフォーマンス計測とインサイト分析 | `performance_start_trace`、`performance_analyze_insight` | 相当機能なし |
| Lighthouse 監査 | `lighthouse_audit` | 相当機能なし |
| ネットワークリクエストの調査 | `list_network_requests`、`get_network_request` | `browser_network_requests` |
| ヒープスナップショット | `take_heapsnapshot` | 相当機能なし |
| CPU とネットワークの制限 | `emulate` | 相当機能なし |
| ロールとテキストによる要素検索 | `take_snapshot` がツリーを返す | `browser_find` が専用に用意されている |
| 任意の自動化コードの実行 | なし | `browser_run_code_unsafe` |
| 複数タブを第一級のオブジェクトとして扱う | `list_pages`、`select_page` | `browser_tabs` |
| ブラウザエンジン | Chromium のみ | Chromium、Firefox、WebKit |

固有の行を上から読んでいくと、2 つのサーバーは競合には見えなくなります。**Chrome DevTools MCP がエージェントに渡すのは DevTools のパネルそのもの**です。パフォーマンスタイムライン、ネットワークのウォーターフォール、メモリプロファイラ、Lighthouse。ブラウザ自身の計測機能をツールとして公開したもので、Chromium 限定なのはそれらのパネルが Chromium のものだからです。

**Playwright MCP がエージェントに渡すのはテストハーネスです。**ロールベースの要素検索、`browser_run_code_unsafe` による任意の Playwright コード、タブ管理、そして 1 つではなく 3 つのレンダリングエンジン。テストスイートがサイトを操作するのと同じやり方のために作られています。

## Chrome DevTools MCP がその枠を取った理由

この判断がどう下されたのかを先に明確にしておきます。それによって、この結論にどれだけの重みを置くべきかが変わるからです。**上の比較はエージェントが機能の網羅範囲から作ったもので、私は提示された選択肢から選びました。**ベンチマークは一切取っていません。どちらのサーバーも時間を計測しておらず、呼び出しあたりのトークンコストも測っておらず、同じタスクを 2 回走らせた比較もしていません。以下は 1 つのワークロードに対する適合性の議論であって、どちらのプロジェクトが優れているかの判定ではありません。

そのワークロードはこのブログであり、ここでブラウザを使って実際に検証していることは狭く反復的です。

- 目次は正しいスクロール位置で読者に追従しているか。
- コードブロックは固定ヘッダーの下で正しく重なっているか。
- SVG 図版のラベルは、日本語や繁体字中国語に訳したあとで viewBox からはみ出さないか。
- IME の変換中に検索ボックスは正しく振る舞うか。
- レイアウト変更のあとも Core Web Vitals を満たしているか。

いずれも**ブラウザをまたいだフローの自動化ではなく、Chromium のページの計測**です。最後の 1 つはパフォーマンス計測か Lighthouse でしか答えが出せず、Playwright MCP にはどちらもありません。このリストの中に、Firefox や WebKit やロールベースのロケーターをそれ以上に必要とするものはありません。

つまり決め手になったのは計測機能の広さであり、判断を誤ったときの代償も小さいものでした。静的サイトであり、ブラウザ作業は出荷されるテストスイートではなく検証だからです。

## Playwright MCP のほうが適する場面

上の理屈はきれいに反転するので、正直な反対側の事例を挙げます。

- **Firefox や WebKit が必要な場合。**Chrome DevTools MCP は Chromium 限定で、それを変える方法はありません。Safari で再現するバグなら、他の行を見るまでもなく決着します。
- **すでに Playwright のテストスイートがある場合。**`browser_run_code_unsafe` を使えば、エージェントはテストと同じ API でコードを書けます。ツールの数とは無関係の、実質的な節約です。
- **仕事が計測ではなくフローである場合。**複数ステップのフォーム、認証、カートの決済など。`browser_find` によるロールベースの検索は、スナップショットのツリーを読んで uid を拾うより明確に優れています。
- **複数のタブを同時に扱う場合。**`browser_tabs` はそれを第一級の概念として扱います。

このプロジェクトはどれにも当てはまらず、だからこそ選択は簡単でした。どれかに当てはまるプロジェクトは、同じ表を読んで逆の結論に達するべきです。

1 つだけ挙げておく例外があります。以前の作業でエージェントがこれに引っかかったからです。**WebKit は Chrome DevTools Protocol ではまったく操作できません。**このブログで Safari 固有の composition イベント順序の癖を検証したときは手作業でやるほかなく、どの MCP サーバーを選んでいても結果は同じでした。

## 23 件の権限エントリが 1 つのグロブになった

選択が決まっても、そもそもの発端だった権限リストは散らかったままでした。13 件が Chrome DevTools のツール、10 件が Playwright のツールを指名しており、まだ使っていないブラウザツールはすべて確認プロンプト予備軍でした。

これを解決するルールは[権限のドキュメント](https://code.claude.com/docs/en/permissions)にあります。

> allow ルールがツール名のグロブを受け付けるのは、リテラルの `mcp__<server>__` 接頭辞のあとに限られる。ルールが設定済みの特定のサーバーを名指しするよう、サーバーの部分にはグロブを含められない。

これで Chrome DevTools の 13 行は 1 行になります。

```json title=".claude/settings.json"
"allow": [
  "mcp__plugin_chrome-devtools-mcp_chrome-devtools__*"
]
```

**覚えておく価値があるのは、サーバー部分にグロブを使えないという条件のほうです。**`mcp__*` のようなルールは、すべての MCP ツールを許可しそうに見えて実際にはしません。ドキュメントによれば、起点の定まらない allow グロブは「警告とともにスキップされ、何も自動承認しない」とされています。サーバー部分が実在のサーバーを名指しする必要があるという制約が、便利のためのワイルドカードが黙って全面承認に化けることを防いでいます。

*Figure — PermissionGlobs: allow グロブは設定ファイルの整理です。毎リクエストから重さを取り除くのは deny グロブのほうです。*

グロブが効いていることを、残っていたエントリのおかげではないと確かめるには、旧リストに**一度も**載っていなかったツールを使う必要があります。`list_network_requests` は個別に承認されたことが一度もありません。これが確認なしで実行されました。

## deny ルールは 24 個のツールをコンテキストから削除した

もう一方は 1 行です。

```json title=".claude/settings.json"
"deny": [
  "mcp__plugin_playwright_playwright__*"
]
```

deny ルールは門番のように読めます。エージェントが尋ね、ルールが拒む、と。ドキュメントが説明しているのは別のものです。

> 名前のみのグロブによる deny ルールに一致したツールは、Claude のコンテキストから削除される。

これは仕組みも見返りも異なります。**24 個の Playwright ツールは禁止されたのではありません。存在しなくなったのです。**ルールを保存した瞬間にスキーマはリクエストから消え、再起動もなく、実行中のセッションはそれらを持たなくなりました。

### 権限ファイルはコンテキストの予算表でもある

エージェントを安全に保つために書くルールは分かりやすい使い方です。**エージェントを小さく保つために書くルールは、ほとんど誰も書きません。**このリポジトリでは、JSON 1 行が毎ターン 24 個のスキーマ分の価値を持ちました。この節約は理論上のものではなく、次のセッションまで持ち越されるものでもなく、確認にコストもかかりません。ツールはリストにあるか、ないかのどちらかです。

はっきり書いておくと、これはプロジェクト単位のルールです。Playwright のプラグインはマシン全体では有効なままです。他のリポジトリでは必要かもしれず、1 つのリポジトリの設定にある deny はそれについて何も主張しないからです。

## playwright という名前のものが 2 つある

この変更が招く罠であり、良い罠です。

```json title="package.json"
"devDependencies": {
  "playwright": "^1.62.1"
}
```

これは **MCP サーバーではなく Playwright のライブラリ**で、このブログはこれに大きく依存しています。`scripts/check-figure-fit.ts` はこれを通じて Chromium を起動し、訳された SVG のラベルが viewBox からはみ出していないかを計測します。`pnpm figures:fit` が実行しているのがこれです。先に触れた Safari の composition 検証も、同じライブラリが開いた CDP セッションで実際の IME 変換を動かしています。

`mcp__plugin_playwright_playwright__*` を拒否しても、そのどれにも影響しません。2 つは名前を共有しているだけです。一方はエージェントのコンテキストにツールを公開するサーバーで、他方はスクリプトが import するパッケージです。**deny を見て依存関係を削除した将来の読み手は、図版チェックを壊します。**型チェックもビルドもそれを検知しません。スクリプトは必要なときにしか走らないからです。だからプロジェクトのルールは、この区別が自明だと期待せずに、パッケージ名とスクリプト名とコマンド名を明示しています。

## おまけ: 古い開発サーバーが公開済みの記事を 404 にする

これは MCP とは無関係です。変更の検証中に表面化し、実際に時間を奪い、そして 2 つのうちより汎用的に役立つ発見でした。

ローカルサーバーに対して図版チェックを走らせると、こうなりました。

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

記事はコミット済みで、`status` は `'published'`、`publishedAt` も過去の時刻でした。直感はルーティングの不具合かフロントマターの不具合だと言いますが、どちらも外れです。**記事は正常でした。古かったのはサーバーです。**

`astro dev` はここでは常駐サーバーとして動き、起動時にコンテンツコレクションのスナップショットを取ります。このサーバーは 16:52 に起動しており、記事のコミットは 16:59 でした。記事が存在するより前に起動したサーバーはその記事を配信することがなく、そのことに気づきもしません。

見えにくくしていた要因が 2 つあります。1 つは、すでにサーバーが動いていると `pnpm dev` が新しいサーバーを起動しないことです。

```text
$ pnpm dev
Dev server already running at http://localhost:4321 (pid 41460)
```

これを表示して**終了コード 0 で終わります**。ここでの成功終了は「起動した」ではなく「いつ起動したか分からないサーバーを再利用した」という意味で、まさに疑ってかかるべき成功風の出力です。

もう 1 つは、手がかりが要約行に埋もれていたことです。3 ロケール 30 記事に対する `57 page(s) checked` は 3 ページ足りず、出力の中で何かがおかしいと知っていた数値はこのページ数だけでした。`pnpm stop && pnpm dev` のあと、同じコマンドはこう言いました。

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

*Figure — StaleServerTimeline: 記事がコミットされたのはサーバー起動の 7 分後で、サーバーは起動時に読み込んだコレクションから応答し続けていました。*

### 隣のブラウザが同じ問題を起こしえない理由

面白いのは対比のほうです。このセッションには長命なプロセスが 2 つ関わっていました。Astro の開発サーバーと、MCP サーバーが生かし続けている Chromium です。同じもののように見えます。正しい扱いは正反対で、その理由は利便性ではありません。

**開発サーバーは私のコンテンツの複製を持ち、ブラウザは持ちません。**ブラウザは指し示されるたびに HTTP で取得するので、3 日開きっぱなしのブラウザでも今の真実を映します。古くなりうる複製を何も持っていないということは、**動かしたままにしても誤った答えは生まれない**ということです。再起動しても何も消えません。消すものがなかったからです。

*Figure — WhereStateLives: ライフサイクルの原則は 1 つの事実から導かれます。古い内容を返しうるのは、ソースの複製を持つプロセスだけです。*

つまり原則は、再起動のコストではなく状態がどこにあるかから導かれます。

| | Astro 開発サーバー | プレビューサーバー | MCP ブラウザ |
| --- | --- | --- | --- |
| ソースの複製を持つか | 持つ。起動時のスナップショット | 持たないが、スナップショットであるビルドを配信する | 持たない。HTTP で取得する |
| 古い答えを返しうるか | ありうる | ありうる。ビルド出力に由来する | ない |
| 解消する方法 | 再起動 | 再ビルド。再起動ではない | 消すものがない |
| 動かしたままのコスト | 誤った結果 | ポートを黙って占有する | なし |
| 正しい習慣 | 検証が終わったら止める | 検証が終わったら止める | そのままにしておく |

### プレビューサーバーは 2 つ目の開発サーバーではなく第 3 の類型

プレビューサーバーは同じ生き物に見えますが、違います。配信するのはコンテンツコレクションではなくビルド出力なので、ソースの複製は持たず、それ自体の起動時刻からは何も分かりません。**古くなるのは、それが配信しているビルドのほうです。**プレビューサーバーを再起動しても同じバイト列を配信し直すだけで、直し方は `pnpm build` です。プレビューの 404 をプロセスの再起動で調べるのは、対象を間違えた時間の使い方です。

本当の危険はまた別で、静かなものです。**プレビューサーバーにとって使用中のポートはエラーではありません。次のポートを取って、何も言いません。**繰り返し実行するたびに 4322、4323、4324 と上がっていき、それ以前のプロセスはすべて生き残ります。ここでは 1 日のうちに 6 つ溜まり、3 日後もまだ 6 つのポートを握ったまま、すべてのパスに `404` を返していました。`astro preview` はデプロイ用のアダプターとはまったく動作しないからです。

これはこのリポジトリに限っては、散らかっている以上に厄介です。`pnpm figures:fit` は `FIT_BASE_URL=http://localhost:4321` を読むので、そのポートに居座られていたら、図版チェックは何日も前のビルドを計測し、コンテンツの欠落と見分けのつかない 404 を報告していたはずです。これを見つけるチェックは 1 行です。

```bash
lsof -nP -iTCP:4321 -sTCP:LISTEN
```

### 何にも一致しないパターンは、きれいなマシンとまったく同じに見える

その 6 つを片付ける過程で、より良い教訓が出てきました。当然だと思ったコマンドが何もしなかったのです。

```bash
pkill -f "astro preview"   # 一致するプロセスはゼロ
```

実際のコマンドラインは `.../astro/bin/astro.mjs preview` なので、パターンは一度も一致せず、`pkill` は静かに終了し、6 つとも動き続けました。**何にも一致しないパターンは、一致するものが何もないマシンと区別がつきません。**エラーも出力も非ゼロ終了もありません。`pkill -f "astro.mjs preview"` にしたところ、6 つとも即座に終了しました。

同じ間違いはその日すでに 1 度出荷されていました。`pgrep -f "astro dev"` と書かれた鮮度チェックで、開発サーバーが応答している最中でも空の結果を返すものです。どちらも同じ誤りでした。プロセステーブルではなくシェルの履歴にしか存在しない、呼びやすい名前に一致させようとしていたのです。

ブラウザを再起動**しない**理由もあり、それはプロジェクトのルールがかつて挙げていたものとは違います。以前のルールはブラウザを温かいまま保つためだと書いていました。これは利便性の議論であり、弱い議論です。エージェントはそれを繰り返していましたが、私が「再起動が安いのになぜそれが問題なのか」と尋ねたところ、その理由は問いに耐えませんでした。本当の理由は、**別のセッションがそのブラウザを操作しているかもしれない**ことで、プロセス ID はそれが誰のものかを教えてくれません。どうしても終了させる必要があるときは、プロファイルのパスが使い捨てかどうかを見分けます。

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

`~/.cache/chrome-devtools-mcp/` 配下のパスなら、それはサーバー自身の使い捨てプロファイルです。それ以外は、実際のタブが開かれた本物のブラウザかもしれません。

## まとめ

- **Chrome DevTools MCP と Playwright MCP は機能のおよそ 3 分の 2 が重なります。**どちらもクリック、入力、スクリーンショット、JavaScript の実行ができるので、そこを基準にした比較は引き分けになります。
- **違いはその先で何を公開するかにあります。**Chrome DevTools MCP がエージェントに渡すのは DevTools の計測機能です。パフォーマンス計測、Lighthouse、ネットワーク、ヒープ、速度制限、そして Chromium 限定。Playwright MCP が渡すのはテストハーネスです。ロールベースの検索、任意の自動化コード、タブ、3 つのエンジン。
- **このブログが Chrome DevTools MCP を残したのは、ブラウザ作業がクロスブラウザの自動化ではなく計測だからです。**これは機能の網羅範囲から導いた、1 つのワークロードに対する適合性の議論であって、ベンチマークによるものではありません。WebKit が必要なプロジェクトや、既存の Playwright スイートがあるプロジェクトは、同じ表から逆の結論に達するべきです。
- **allow グロブが効くのは、リテラルでグロブを含まない `mcp__<server>__` 接頭辞のあとだけです。**これで列挙されていた 13 件が 1 行になりました。`mcp__*` なら代わりに警告付きでスキップされていました。
- **deny グロブはツールをブロックするのではなくコンテキストから削除します。**JSON 1 行が、24 個のツールスキーマを毎リクエストから即座に、再起動なしで取り除きました。権限ルールはエージェントを安全に保つ手段であるだけでなく、小さく保つ手段でもあります。
- **npm パッケージの `playwright` は Playwright MCP ではありません。**サーバーを拒否しても、ライブラリを import するスクリプトには何の影響もなく、両者を混同するとビルド工程が検知しないものが壊れます。
- **検証が終わったら開発サーバーは止め、MCP ブラウザはそのままにします。**開発サーバーは起動時にコンテンツのスナップショットを取るので、嘘をつきうるからです。ブラウザは何も持たないので、つけません。
- **プレビューサーバーは 2 つ目の開発サーバーではなく第 3 の類型です。**それ自体の起動時刻は無意味で、古くなるのは配信しているビルドのほうなので、直し方は再ビルドであって再起動ではありません。それでも止めてください。使用中のポートがあると黙って次を取るため、固定した `FIT_BASE_URL` が後で当たるポートを孤立プロセスが握り続けます。
- **何にも一致しないプロセスパターンは、きれいなマシンとまったく同じに見えます。**`pkill -f "astro preview"` も `pgrep -f "astro dev"` も一致するプロセスはゼロです。プロセステーブルにあるのは `astro.mjs` だからです。どちらも何も報告せず、そこが問題です。シェルで打つ名前ではなく、プロセステーブルにあるものに一致させてください。

## 参考リンク

- [Claude Code ドキュメント: 権限の設定。allow グロブがリテラルでグロブを含まない `mcp__<server>__` 接頭辞のあとにのみ受け付けられること、およびグロブによる deny ルールがツールをコンテキストから削除することを含む](https://code.claude.com/docs/en/permissions)
- [chrome-devtools-mcp。DevTools のパフォーマンス、ネットワーク、メモリの各機能を MCP ツールとして公開するサーバー](https://github.com/ChromeDevTools/chrome-devtools-mcp)
- [Playwright MCP。`browser_run_code_unsafe` と、反対側の事例を決めるクロスエンジン対応を含む](https://github.com/microsoft/playwright-mcp)
- [Astro のコンテンツコレクション。ビルド時読み込みが、記事より前に起動した開発サーバーがその記事を配信しない理由](https://docs.astro.build/en/guides/content-collections/)
