# Chrome DevTools MCP 與 Playwright MCP 有何不同 — 我為什麼在 Astro 部落格選了 Chrome DevTools

> Chrome DevTools MCP 與 Playwright MCP 交給編碼智能體的東西並不相同。各自提供什麼，以及一個 Astro 部落格為何留下其中一個、拒絕另一個。

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

---
## 引言

我打開 `.claude/settings.json` 想再核准一個瀏覽器工具，卻發現裡面躺著的不是一個、而是兩個瀏覽器自動化伺服器。兩者都已經啟用好幾個月，都能點擊和截圖，而整個儲存庫裡沒有任何一處說明該用哪一個。它們底下的權限清單已經長到**手動新增的 23 項**，一次一行，每次某個新工具第一次跳出確認就補一行，而且仍然不完整。

結果是：這個部落格現在只跑 **Chrome DevTools MCP**，Playwright MCP 被拒絕，而這條 deny 規則的運作方式是我們兩邊都沒料到的。它不是擋下那些工具，而是**把它們從智能體的脈絡中整個刪掉**。這讓權限規則不只是保持安全的手段，也成了把脈絡買回來的手段。

本文整理兩個伺服器實際交給編碼智能體什麼、它們真正的差別在哪、這個專案為何留下其中一個、另一個在什麼情況下更適合，以及權限那一側如何從 23 項縮成 2 行。最後還有一個在驗證過程中浮現、與 MCP 完全無關的陷阱。

## 兩個瀏覽器伺服器每輪載入 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 份綱要跟著跑**，而在這個儲存庫裡，一次工作階段大概只會用到其中四項。

這件事不會有任何警告。伺服器能動、工具會出現，成本又薄薄地攤在每一次請求上，因此從來不會浮現成一個該修的問題。

*Figure — ToolSurfaces: 三分之二的能力是共通的。真正分出兩個伺服器的，是只有其中一方擁有的那幾列。*

## 每個伺服器交給智能體什麼

很容易想把它總結成「一個負責量測、一個負責操作」，但這個說法是錯的，而且值得在它擴散之前先掐掉：**兩個伺服器都能點擊、輸入、停留、截圖、調整可視區域、執行 JavaScript、讀取主控台。**它們大約三分之二的能力是換了名字的同一件事，所以以「能不能點擊」為基準的比較只會打成平手。

差別在那塊共通區域之外。

| 能力 | 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 |

把獨有的那幾列從上讀到下，兩個伺服器就不再像競爭對手。**Chrome DevTools MCP 交給智能體的是 DevTools 的面板本身**：效能時間軸、網路瀑布圖、記憶體分析器、Lighthouse。那是瀏覽器自己的量測功能以工具的形式公開出來，而它僅限 Chromium，是因為那些面板本來就是 Chromium 的。

**Playwright MCP 交給智能體的是一套測試框架。**以角色為基礎的元素尋找、透過 `browser_run_code_unsafe` 執行任意 Playwright 程式碼、分頁管理，以及三個而非一個算繪引擎。它是為了用測試套件操作網站的那種方式而設計的。

## Chrome DevTools MCP 為什麼拿下這個位置

先把這個決定是怎麼做出來的講清楚，因為那會改變這個結論該有多少份量。**上面的比較是智能體依據功能涵蓋範圍整理的，而我從它提出的選項中挑選。**這裡沒有做任何基準測試。兩個伺服器都沒有計時，沒有量測每次呼叫的權杖成本，也沒有把同一項任務各跑一次來對照。以下是針對單一工作型態的適配性論證，不是哪個專案做得比較好的判決。

那個工作型態就是這個部落格，而這裡實際會用瀏覽器驗證的事情既狹窄又重複：

- 目錄有沒有在正確的捲動位置跟著讀者？
- 程式碼區塊在固定頁首底下是否仍正確堆疊？
- SVG 圖表的標籤翻成日文或繁體中文之後，會不會超出 viewBox？
- 輸入法正在組字時，搜尋框的行為是否正確？
- 版面調整之後，頁面是否仍達到 Core Web Vitals？

以上每一項都是**量測一個 Chromium 頁面，而不是跨瀏覽器自動化流程**。最後一項只有靠效能追蹤或 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` 把這件事當成一級概念。

這個專案一項都不符合，這就是選擇之所以簡單的原因。符合其中任何一項的專案，讀同一張表應該得到相反的結論。

有一個例外值得指出，因為智能體在更早的工作裡就栽在上面：**WebKit 完全無法透過 Chrome DevTools Protocol 操作。**在這個部落格上驗證 Safari 特有的 composition 事件順序怪癖時只能手動進行，換成哪個 MCP 伺服器都一樣。

## 23 條權限項目變成一個萬用字元

即使選擇定了，一開始引發這件事的權限清單仍然一團亂。13 項指名 Chrome DevTools 的工具，10 項指名 Playwright 的工具，而每一個還沒用過的瀏覽器工具都是一次等著跳出來的確認。

解決它的規則就寫在[權限文件](https://code.claude.com/docs/en/permissions)裡：

> allow 規則只接受位於字面 `mcp__<server>__` 前綴之後的工具名稱萬用字元。伺服器那一段不能含有萬用字元，好讓規則指名你設定過的特定伺服器。

於是 Chrome DevTools 的 13 行縮成一行：

```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 項工具從脈絡中移除

另外一半也只有一行：

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

deny 規則讀起來像一道關卡：智能體來問，規則說不行。文件描述的卻是另一回事。

> 被純名稱萬用字元 deny 規則命中的工具，會從 Claude 的脈絡中移除。

這是不同的機制，也帶來不同的回報。**那 24 項 Playwright 工具不是變成被禁止，而是不再存在。**規則存檔的那一刻，它們的綱要就離開了請求，不需要重新啟動，執行中的工作階段就這麼不再擁有它們。

### 權限檔同時也是一份脈絡預算

為了讓智能體保持安全而寫的規則是顯而易見的用法。**為了讓智能體保持精簡而寫的規則，幾乎沒有人寫。**在這個儲存庫裡，一行 JSON 換到了每輪 24 份綱要。這個節省不是理論上的，不會延到下一次工作階段才兌現，而且驗證起來毫無成本：工具要嘛在清單上，要嘛不在。

有一點值得直說：這是專案層級的規則。Playwright 外掛在整台機器上仍然啟用，因為其他儲存庫可能需要它，而某一個儲存庫設定裡的 deny 對此不表示任何意見。

## 有兩樣東西都叫 playwright

這是整個變更招來的陷阱，而且是個好陷阱：

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

那是 **Playwright 函式庫，不是 MCP 伺服器**，而這個部落格非常依賴它。`scripts/check-figure-fit.ts` 透過它啟動 Chromium，量測翻譯後的 SVG 標籤有沒有超出 viewBox，這正是 `pnpm figures:fit` 在做的事。前面提到的 Safari composition 驗證，也是由同一個函式庫開啟的 CDP 工作階段來驅動真正的輸入法組字。

拒絕 `mcp__plugin_playwright_playwright__*` 對上述任何一件事都沒有影響。兩者只共用一個名字：一個是把工具公開進智能體脈絡的伺服器，另一個是指令碼會 import 的套件。**未來某位讀者看到這條 deny 就把相依套件移除，會弄壞圖表檢查**，而型別檢查與建置都不會發現，因為那個指令碼只在需要時才執行。這就是為什麼專案規則會把套件名稱、指令碼名稱與指令名稱都明確寫出來，而不是指望這個區別不言自明。

## 附帶收穫：過期的開發伺服器讓已發布的文章變成 404

這與 MCP 無關。它在驗證變更的過程中浮現，實際花掉了時間，而且是兩者之中更通用的發現。

對本機伺服器執行圖表檢查，得到這樣的結果：

```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。比文章更早啟動的伺服器永遠不會提供那篇文章，而且它也不會察覺。

有兩件事讓這個問題難以看見。第一是當已經有伺服器在跑時，`pnpm dev` 不會啟動新的：

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

它印出這行然後**以結束代碼 0 結束**。這裡的成功結束代表的是「重用了一個不知道多舊的伺服器」，而不是「啟動了一個伺服器」，正是那種看起來成功、值得懷疑的輸出。

第二是線索埋在摘要行裡。三個語系共 30 篇文章對應的 `57 page(s) checked` 少了三頁，而這個頁數是整段輸出裡唯一知道有東西不對的數字。執行 `pnpm stop && pnpm dev` 之後，同一個指令說：

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

*Figure — StaleServerTimeline: 文章是在伺服器啟動七分鐘後提交的，而伺服器持續依據它在啟動時讀入的集合回應。*

### 旁邊那個瀏覽器為什麼不可能有同樣的問題

有意思的是兩者的對比。這次工作階段牽涉到兩個長時間存活的行程：Astro 開發伺服器，以及 MCP 伺服器持續維持的那個 Chromium。它們看起來是同一類東西。正確的處理方式卻相反，而理由不是方便。

**開發伺服器持有我的內容副本，瀏覽器沒有。**瀏覽器每次被指向某處都會以 HTTP 取得，所以一個開了三天的瀏覽器仍然呈現當下的真實。它沒有任何屬於我的東西被快取起來而可能過期，也就是說**讓它繼續執行不會產生錯誤的答案**。重新啟動它不會清掉任何東西，因為本來就沒有東西要清。

*Figure — WhereStateLives: 生命週期的原則來自一個事實：只有持有原始內容副本的行程，才可能回傳過期的東西。*

所以這條原則來自狀態存放在哪裡，而不是重新啟動要花多少成本：

| | Astro 開發伺服器 | 預覽伺服器 | MCP 瀏覽器 |
| --- | --- | --- | --- |
| 是否持有原始內容副本 | 是，啟動時的快照 | 否，但提供的建置產物本身就是快照 | 否，以 HTTP 取得 |
| 是否可能給出過期答案 | 會 | 會，來自建置產物 | 不會 |
| 靠什麼解除 | 重新啟動 | 重新建置，而非重新啟動 | 沒有東西要清 |
| 繼續執行的代價 | 錯誤的結果 | 悄悄占住一個連接埠 | 沒有 |
| 正確習慣 | 驗證結束就停掉 | 驗證結束就停掉 | 放著別動 |

### 預覽伺服器是第三種類型，不是第二個開發伺服器

預覽伺服器看起來是同一種生物，其實不是。它提供的是建置產物而不是內容集合，因此不持有原始內容的副本，它自己的啟動時間也說明不了任何事。**會過期的是它所提供的那份建置產物。**重新啟動預覽伺服器只是把同樣的位元組再送一次，正確的做法是 `pnpm build`。用重啟行程的方式追查預覽的 404，是把時間花在錯的對象上。

它真正的危險又是另一回事，而且很安靜。**對預覽伺服器來說，被占用的連接埠不是錯誤：它會取下一個，而且什麼都不說。**反覆執行會一路落在 4322、4323、4324 往上，而先前每一個行程都還活著。這裡在一天之內累積了六個，三天後仍然握著六個連接埠，對每一條路徑都回應 `404`，因為 `astro preview` 與部署用的轉接器根本不相容。

在這個儲存庫上，這比單純的雜亂更麻煩。`pnpm figures:fit` 會讀 `FIT_BASE_URL=http://localhost:4321`，所以只要有人占著那個連接埠，圖表檢查就會去量測一份好幾天前的建置產物，並回報與內容缺失無從分辨的 404。找出它的檢查只有一行：

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

### 一個什麼都沒命中的樣式，看起來就跟乾淨的機器一模一樣

清掉那六個的過程帶出了更好的教訓。看起來理所當然的指令什麼也沒做：

```bash
pkill -f "astro preview"   # 命中零個行程
```

真正的指令列是 `.../astro/bin/astro.mjs preview`，所以那個樣式從來沒有命中，`pkill` 安靜地結束，六個全部繼續執行。**一個什麼都沒命中的樣式，和一台本來就沒有東西可命中的機器無從分辨。**沒有錯誤、沒有輸出、沒有非零結束代碼。改成 `pkill -f "astro.mjs preview"` 之後，六個立刻全部結束。

同樣的錯誤那天其實已經出貨過一次，就在寫成 `pgrep -f "astro dev"` 的新鮮度檢查裡，即使開發伺服器正在回應請求，它也會回傳空結果。兩者是同一個錯誤：拿一個只存在於 shell 歷史、不存在於行程表裡的順口名稱去比對。

另外還有一個**不要**重新啟動瀏覽器的理由，而且不是專案規則以前給的那個。舊的規則說瀏覽器要保持暖機狀態，那是方便性的論點，而且是很弱的論點；智能體一直重複這個說法，直到我問「重新啟動明明很便宜，為什麼這會是問題」，這個理由就撐不住了。真正的理由是**可能有另一個工作階段正在操作那個瀏覽器**，而行程 ID 不會告訴你它屬於誰。真的必須結束某個瀏覽器時，用設定檔路徑來判斷它是不是可拋棄的：

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

路徑在 `~/.cache/chrome-devtools-mcp/` 底下的，是伺服器自己的拋棄式設定檔。其他的可能是一個開著真實分頁的真實瀏覽器。

## 總結

- **Chrome DevTools MCP 與 Playwright MCP 大約三分之二的能力重疊。**兩者都能點擊、輸入、截圖與執行 JavaScript，所以以這些為基準的比較只會打平。
- **差別在於它們在那之外各自公開什麼。**Chrome DevTools MCP 交給智能體的是 DevTools 的量測功能：效能追蹤、Lighthouse、網路、堆積、限速，且僅限 Chromium。Playwright MCP 交出的是測試框架：以角色尋找、任意自動化程式碼、分頁，以及三個引擎。
- **這個部落格留下 Chrome DevTools MCP，是因為它的瀏覽器工作是量測，而不是跨瀏覽器自動化。**這是依功能涵蓋範圍導出、針對單一工作型態的適配性論證，不是來自基準測試。需要 WebKit 的專案，或已經有 Playwright 測試套件的專案，應該從同一張表得到相反的結論。
- **allow 萬用字元只有在字面且不含萬用字元的 `mcp__<server>__` 前綴之後才有效。**這讓列舉的 13 項縮成一行；換成 `mcp__*` 則會被略過並發出警告。
- **deny 萬用字元是把工具從脈絡中移除，而不是擋下它們。**一行 JSON 立刻把 24 份工具綱要從每次請求中拿掉，而且不需要重新啟動。權限規則不只是讓智能體保持安全的手段，也是讓它保持精簡的手段。
- **npm 套件 `playwright` 不是 Playwright MCP。**拒絕伺服器對一個 import 該函式庫的指令碼毫無影響，而把兩者混為一談會弄壞任何建置步驟都不會檢查的東西。
- **驗證結束就停掉開發伺服器，MCP 瀏覽器則放著別動。**開發伺服器在啟動時取得內容快照，因此可能對你說謊。瀏覽器不持有你的任何東西，因此不會。
- **預覽伺服器是第三種類型，不是第二個開發伺服器。**它自己的啟動時間沒有意義，因為會過期的是它提供的建置產物，所以解法是重新建置而永遠不是重新啟動。但還是要停掉它：連接埠被占用時它會悄悄取下一個，於是孤立的行程會一路握著那些固定的 `FIT_BASE_URL` 日後可能命中的連接埠。
- **一個什麼都沒命中的行程樣式，看起來就跟乾淨的機器一模一樣。**`pkill -f "astro preview"` 與 `pgrep -f "astro dev"` 命中的行程都是零，因為行程表裡放的是 `astro.mjs`。兩者都不會回報任何東西，而那正是問題所在。去比對行程表裡真正有的東西，而不是你在 shell 裡打的名稱。

## 參考連結

- [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/)
