標籤
白色卡片上的 Playwright 標誌,紅色皺眉面具與綠色微笑面具相互重疊,旁邊是黑色的 Playwright 文字標誌

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

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

本頁目錄

引言

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

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

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

兩個瀏覽器伺服器每輪載入 53 份工具綱要

兩個伺服器都在 ~/.claude/config.json 中對整台機器啟用:

~/.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 份綱要跟著跑,而在這個儲存庫裡,一次工作階段大概只會用到其中四項。

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

共通的部分,以及各自獨有的部分上方一條橫幅列出兩個伺服器都提供的六項能力:頁面導覽、點擊與輸入、螢幕截圖、調整可視區域、讀取主控台、執行 JavaScript。下方並排兩張卡片。Chrome DevTools MCP 的卡片列出效能追蹤、Lighthouse 稽核、堆積快照與 CPU 或網路限速,並標示僅限 Chromium。Playwright MCP 的卡片列出以角色與文字尋找、執行 Playwright 程式碼、分頁為一級物件與下拉選單選取,並標示 Chromium、Firefox、WebKit。兩個伺服器都具備的功能頁面導覽點擊與輸入螢幕截圖調整可視區域讀取主控台執行 JSChrome DevTools MCP效能追蹤Lighthouse 稽核堆積快照CPU / 網路限速僅限 ChromiumPlaywright MCP以角色與文字尋找執行 Playwright 程式碼分頁為一級物件下拉選單選取Chromium、Firefox、WebKit
三分之二的能力是共通的。真正分出兩個伺服器的,是只有其中一方擁有的那幾列。

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

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

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

能力 Chrome DevTools MCP Playwright MCP
頁面導覽、點擊、輸入、停留、截圖
讀取主控台、執行 JavaScript
效能追蹤與洞察分析 performance_start_traceperformance_analyze_insight 無對應功能
Lighthouse 稽核 lighthouse_audit 無對應功能
網路請求檢查 list_network_requestsget_network_request browser_network_requests
堆積快照 take_heapsnapshot 無對應功能
CPU 與網路限速 emulate 無對應功能
以角色與文字尋找元素 take_snapshot 回傳一棵樹 browser_find,專門為此設計
執行任意自動化程式碼 browser_run_code_unsafe
多個分頁視為一級物件 list_pagesselect_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 的工具,而每一個還沒用過的瀏覽器工具都是一次等著跳出來的確認。

解決它的規則就寫在權限文件裡:

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

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

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

**值得記住的是伺服器那一段不得含有萬用字元這個條件。**像 mcp__* 這樣的規則看起來應該會允許所有 MCP 工具,實際上不會;文件說,沒有錨定的 allow 萬用字元「會被略過並發出警告,不會自動核准任何東西」。伺服器那一段必須指名一個真實存在的伺服器,這個限制正是讓一個圖方便的萬用字元不會悄悄變成全面核准的原因。

兩種萬用字元各自縮短了什麼兩列由左至右的流程。上列從手動列出的 13 個項目開始,經過 allow 規則後變成 1 個項目,但 29 個工具仍留在脈絡中。下列從每次請求都夾帶的 24 個工具開始,經過 deny 規則後變成 0 個工具,結構描述也一併離開脈絡。最後一行寫著:allow 縮短設定檔,deny 縮短請求。allow 萬用字元13 個項目逐條手動列出allow: mcp__…__*1 個項目29 個工具仍在脈絡中deny 萬用字元24 個工具每次請求都夾帶deny: mcp__…__*0 個工具結構描述離開脈絡allow 縮短設定檔,deny 縮短請求。
allow 萬用字元是設定檔的整理工作。真正把重量從每次請求中拿掉的是 deny 萬用字元。

要證明生效的是萬用字元而不是殘留的舊項目,測試就必須用一個從未出現在舊清單上的工具。list_network_requests 從來沒有被個別核准過。它在沒有任何確認的情況下執行了。

deny 規則把 24 項工具從脈絡中移除

另外一半也只有一行:

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

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

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

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

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

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

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

有兩樣東西都叫 playwright

這是整個變更招來的陷阱,而且是個好陷阱:

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 無關。它在驗證變更的過程中浮現,實際花掉了時間,而且是兩者之中更通用的發現。

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

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 不會啟動新的:

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

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

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

Everything fits. 60 page(s) measured.
比它所回答的文章更舊的開發伺服器一條有三個標記的時間軸。16:52 開發伺服器啟動,集合中有 30 篇文章。16:59 文章提交,但執行中的伺服器沒有察覺。之後執行圖片檢查,60 頁中只量到 57 頁。時間軸上方的橫幅顯示伺服器所知的內容停留在啟動當下,三個時間點都沒有改變。下方的結果寫著:已發布的文章出現 3 次 404。開發伺服器所知的內容,停留在啟動當下16:52開發伺服器啟動集合中有 30 篇文章16:59文章提交執行中的伺服器沒有察覺之後執行圖片檢查60 頁中只量到 57 頁已發布的文章出現 3 次 404
文章是在伺服器啟動七分鐘後提交的,而伺服器持續依據它在啟動時讀入的集合回應。

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

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

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

哪一個行程持有原始內容的副本磁碟上的內容位於上方,兩個箭頭由此向下延伸。左邊的箭頭標示為啟動時複製一次,通往 Astro 開發伺服器;該伺服器持有啟動時取得的內容集合快照,並標示為可能回傳過期資料。右邊的箭頭標示為每次請求都以 HTTP 取得,通往 MCP 瀏覽器;它完全不保存原始內容的副本,並標示為不可能過期。最後的原則寫著:重啟持有副本的行程,另一個放著就好。磁碟上的內容啟動時複製一次每次請求都以 HTTP 取得Astro 開發伺服器啟動時取得的內容集合快照可能回傳過期資料MCP 瀏覽器完全不保存原始內容的副本不可能過期重啟持有副本的行程,另一個放著就好。
生命週期的原則來自一個事實:只有持有原始內容副本的行程,才可能回傳過期的東西。

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

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

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

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

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

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

終端機視窗
lsof -nP -iTCP:4321 -sTCP:LISTEN

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

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

終端機視窗
pkill -f "astro preview" # 命中零個行程

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

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

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

終端機視窗
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 裡打的名稱。

參考連結

分享這篇文章