讓每個專案裡的 chrome-devtools MCP 各自用一份 Chrome 設定檔 — 並設置 gitignore 的 .mcp.json
chrome-devtools-mcp 讓所有專案共用同一份 Chrome 設定檔,兩個 repo 無法同時驅動 Chrome。這個問題只要在每個專案設置各自的 .mcp.json 搭配 --userDataDir 就能解決。

本頁目錄
引言
我有兩個專案透過 chrome-devtools-mcp 驅動 Chrome:這個部落格,以及一個處理足球資料的應用。當我開著部落格那邊的瀏覽器,並切到另一個 repo 要求抓一個頁面時,卻發生了錯誤:
The browser is already running for /Users/me/.cache/chrome-devtools-mcp/chrome-profile.Use --isolated to run multiple browser instances.這是因為 Chrome 規定每一個設定檔目錄只能有一個瀏覽器行程。而官方外掛給機器上每一個專案的都是同一個目錄。解決辦法是 在每個 repo 放一份 .mcp.json,並各自傳入自己的 --userDataDir。這個檔案需要被 gitignore 掉:因為絕對路徑只在一台電腦上成立,repo 卻是要跨電腦共用的。
現在兩個專案可以同時驅動 Chrome,各自的登入狀態也都保留著。
每個專案一份 .mcp.json,以及它指定的設定檔目錄
每個 repo 在根目錄宣告自己的 chrome-devtools 伺服器,兩個 repo 之間唯一不同的就是一條路徑。
{ "mcpServers": { "chrome-devtools": { "command": "npx", "args": [ "chrome-devtools-mcp@1.9.0", "--userDataDir=/Users/me/.cache/chrome-devtools-mcp/profiles/oharu-tech-blog" ] } }}那條路徑的最後一段就是 repo 自己的目錄名稱,兩份設定檔因此不會互相衝突,也不需要任何人去維護一份「哪個專案用哪個目錄」的清單。
這個檔案本身不會進版控,因為那條絕對路徑只在一台機器上成立:
# Per-project chrome-devtools MCP server — gives this repo its own Chrome# profile so it does not fight another project for the shared profile lock.# The --userDataDir is an absolute path on one machine, so it is not portable..mcp.json輸入git check-ignore -v .mcp.json輸出.gitignore:36:.mcp.json .mcp.json如果你以為這個檔案會像一般的組態檔那樣運作,那有兩件事會讓你感到意外。MCP 伺服器是在 session 開始時解析的,所以寫好檔案之後,需要到下一個 session 才會生效;另外,要把伺服器列進 enabledMcpjsonServers,它才不會每個 session 都跳一次授權詢問。
這裡使用 npx 是刻意的,即使這台電腦的慣例是使用 pnpm dlx。這是因為這是一份長時間存在的伺服器定義,不是 shell 指令,所以應該和官方外掛實際執行的內容保持一致。
三個符號連結、一次交接,以及拒絕交接的那個 flag
Chrome 強制一個設定檔目錄只有一個瀏覽器行程,靠的是放在該目錄裡的三個符號連結。
這個鎖是由什麼組成的
三個都是符號連結,資訊在連結目標的字串裡,不在檔案內容中,所以這裡沒有任何一個是你打得開來讀的檔案:
輸入ls -l ~/.cache/chrome-devtools-mcp/profiles/oharu-tech-blog | grep Singleton輸出lrwxr-xr-x@ 1 me staff 20 Sep 19 11:25 SingletonCookie -> 15445160568116473017lrwxr-xr-x@ 1 me staff 19 Sep 19 11:25 SingletonLock -> mymac.local-78752lrwxr-xr-x@ 1 me staff 89 Sep 19 11:25 SingletonSocket -> /var/folders/h2/…/com.google.Chrome.22RNK3/SingletonSocketSingletonLock 標示的是目前持有這份設定檔的主機名稱與行程 ID。 SingletonSocket 指向一個放在私有暫存目錄裡的 Unix domain socket,這樣即使設定檔放在網路檔案系統上,socket 也不會跟著放上去。
SingletonCookie 是一個同時存在兩邊的亂數 token,連線前後都會進行比對。有了它,行程才能確認自己連上的 socket 屬於這份設定檔。
Chromium 在 chrome/browser/process_singleton_posix.cc 開頭一段很長的註解裡說明了這個設計。
為什麼第二個 Chrome 通常只是開另一個視窗
對著另一個 Chrome 正在持有的設定檔啟動第二個 Chrome,會成功:
輸入"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --user-data-dir=/tmp/p about:blank輸出既存のブラウザ セッションで開いています。那一行是 IDS_USED_EXISTING_BROWSER,會以 Chrome 當下執行的語言顯示。那個行程沒有畫出任何視窗,就以結束碼 0 退出。
這是 ProcessSingleton::NotifyOtherProcessOrCreate 的 PROCESS_NOTIFIED 分支:新的行程會連上 SingletonSocket,把自己的命令列交給回應的瀏覽器,等到收到確認回覆就結束。你開兩次 Chrome 只得到一個視窗,而不是第二個瀏覽器,這正是這段程式碼正常運作的結果。
讓交接不再發生的 flag
只要 其中任何一次 啟動加上 --enable-automation,交接就不會發生。四種組合的條件都一樣:同一個設定檔目錄,第二次啟動在第一次的五秒後。結果如下:
| 第一次啟動 | 第二次啟動 | 第二次的結束碼 | 第二次印出的內容 |
|---|---|---|---|
| 一般 | 一般 | 0 | IDS_USED_EXISTING_BROWSER |
| 一般 | --enable-automation | 21 | ProcessSingleton 失敗 |
--enable-automation | 一般 | 21 | ProcessSingleton 失敗 |
--enable-automation | --enable-automation | 21 | ProcessSingleton 失敗 |
失敗時是兩行:
[ERROR:chrome/browser/process_singleton_posix.cc:347] Failed to create /tmp/p/SingletonLock: File exists (17)[ERROR:chrome/app/chrome_main_delegate.cc:550] Failed to create a ProcessSingleton for your profile directory. This means that running multiple instances would start multiple browser processes rather than opening a new window in the existing process. Aborting now to avoid profile corruption.Chrome 只有在尋找並空手而回之後,才會去 建立 這個鎖。NotifyOtherProcessWithTimeoutOrCreate 會在通知步驟回傳 PROCESS_NONE 時呼叫 Create()。而 Create() 撞上已存在的符號連結失敗時,結果就是 LOCK_ERROR 與結束碼 21。
在自動化模式下,兩個瀏覽器永遠找不到彼此,所以第二個會一路運行到建立鎖的那一步,但是那個鎖早就被佔走了。
關於這個 flag 究竟在 Chromium 的哪裡造成這個效果,我翻遍幾個最明顯的地方都找不到答案。process_singleton_posix.cc 完全沒有提到 automation。chrome_main_delegate.cc 除了一份 Windows 降權清單之外也沒有。上面那張表是實測後的結果,表背後的機制則不是。
Puppeteer 把 --enable-automation 放在每次啟動都會傳入的預設參數清單裡,而 chrome-devtools-mcp 是透過 Puppeteer 啟動的。這個伺服器開出來的瀏覽器,無一例外都落在表格裡會中止的那幾種組合。
錯誤訊息的三個層次
那句要你用 --isolated 的建議,其實是 Chrome 寫進自己標準錯誤輸出的那行字經過層層改寫之後的第三個版本。
Puppeteer 在啟動失敗後讀取瀏覽器最近的日誌,並比對訊息字串:
if (logs.includes('Failed to create a ProcessSingleton for your profile directory') || …) { // … throw new Error( `The browser is already running for ${launchArgs.userDataDir}. ` + 'Use a different `userDataDir` or stop the running browser first.', );}chrome-devtools-mcp 接住那個錯誤,接著比對 它的 字串:
if (userDataDir && error.message.includes('The browser is already running')) { throw new Error( `The browser is already running for ${userDataDir}. ` + 'Use --isolated to run multiple browser instances.', {cause: error}, );}於是這個建議之前已經經歷了兩次改寫。你想要的明明是第二份設定檔,得到的卻是叫你把設定檔丟掉。
為什麼正確的修法是第二份設定檔
手動清掉那個鎖
把正在執行的瀏覽器關掉,確實會釋放設定檔,但 這並不是一個永久方案。下一次兩個專案同時需要瀏覽器,衝突就回來了。整個結構根本沒有改變。
而且通常也沒有殘留物可刪。收到 SIGTERM 的 Chrome 會在結束時刪掉自己的 SingletonLock,這是關掉一個測試用的瀏覽器之後確認的。鎖活得比行程久,只有在強制砍掉行程時才會完全釋放;即使如此,只要連結目標裡的行程 ID 已經不存在,Chrome 也會自己把這個孤兒鎖刪掉。
—isolated,以及它丟掉的登入
--isolated 是錯誤訊息建議的做法,它確實能解掉衝突。它的說明本身就寫明了代價:
creates a temporary user-data-dir that is automatically cleaned up after the browser is closed
關掉瀏覽器就清乾淨。意思是 每個 session 都從全部登出的狀態開始。這個伺服器的價值就在於驅動真實瀏覽器去看真實頁面,所以每個 session 的前幾分鐘都變成了重新登入的時間。
這兩個 flag 本來就就是互斥的選項,只能二選一,不可能並用。
關掉官方外掛,以及換來的取捨
官方外掛沒辦法指向每個專案自己的設定檔,因為它根本沒有傳任何參數給伺服器:
{ "mcpServers": { "chrome-devtools": { "type": "stdio", "command": "npx", "args": ["--prefix", "${PLUGIN_DATA}", "chrome-devtools-mcp@1.9.0"] } }}在 userDataDir 未定義的情況下,伺服器會退回到一條由家目錄組出來的路徑:
if (!isolated && !userDataDir) { userDataDir = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', profileDirName);}在 stable 頻道上 profileDirName 就是 chrome-profile,所以 機器上每個專案都會解析到同一個目錄,衝突的成因就在這一行程式碼裡。
因此把外掛全域關掉是修法的一部分,不是可做可不做的整理。代價也很明確:沒有 .mcp.json 的 repo 從此完全沒有 Chrome 相關工具可用。工具名稱也會跟著變:它們會以 mcp__chrome-devtools__* 出現,而不是外掛的 mcp__plugin_chrome-devtools-mcp_chrome-devtools__*,所以任何針對舊前綴寫的權限規則都會悄悄失效。
這個修法到不了的地方:同一個 repo 的兩個 session
這件事是在測試這套配置時發現的。部落格的設定檔上原本就有一個瀏覽器在跑,我又在足球專案的設定檔上啟動了第二個,兩者在同一個時刻各自持有自己的鎖:
輸入for d in oharu-tech-blog agentic-football-cup; do echo "$d -> $(readlink .../profiles/$d/SingletonLock)"; done輸出oharu-tech-blog -> mymac.local-78752agentic-football-cup -> mymac.local-4494接著,第三個 session 裡的一次 Chrome 呼叫,跳出了一模一樣的錯誤。那個 session 在部落格的 repo 裡,持有瀏覽器的那個 session 也在同一個 repo:同一個 repo 先後啟動過兩個伺服器,中間隔了好幾個小時,而瀏覽器屬於比較舊的那一個。
--userDataDir 讓每個 repo 各有一份設定檔,但不會讓每個 session 各有一份。因此在同一個專案裡開兩個 session,就會撞上同一個鎖,和從前兩個專案互撞的情形完全一樣。
把同樣的修法再往下推一層,就會變成每個 session 一份設定檔,等於用衝突換來一份永遠累積不了登入的設定檔。那只是多繞幾步的 --isolated。這也正是應該以 repo 為單位的理由。
一份設定檔佔掉多少磁碟
這裡日常使用的設定檔大約落在數百 MB,而 其中大部分是 Chrome 依自己的配額管理的快取,不是隔離造成的。其中一份量測到的內容如下,合計 244 MB:
| 目錄 | 大小 | 內容 |
|---|---|---|
Default/Cache | 82 MB | 瀏覽過頁面的 HTTP 快取 |
Default/Service Worker | 66 MB | Service Worker 的快取 |
component_crx_cache | 36 MB | Chrome 的元件 |
WasmTtsEngine | 22 MB | 內建的語音合成引擎 |
Default/Code Cache | 7.9 MB | 編譯後的指令碼快取 |
OnDeviceHeadSuggestModel | 7.7 MB | 網址列的建議模型 |
上面那幾項瀏覽相關的快取會隨著使用增加,並依配額被淘汰。底下那些元件目錄才是第二份設定檔真正重複佔用的部分,因為 每一份設定檔都會把另一份已經下載過的元件再下載一次。
這個量不會固定在某個數字上。component_crx_cache 在一份設定檔量到 36 MB,另一份是 63 MB。WasmTtsEngine 則是 22 MB 對 45 MB。數字比較大的那一份,就是啟動次數比較多的那一份。與其當成常數,不如以每個專案數十 MB 來抓預算。
刪掉那兩個快取目錄,從其中一份設定檔回收了 162 MB。它的 cookie 與已儲存的登入都還在。一個專案用完之後整份設定檔刪掉也沒問題,失去的只有那份登入。
總結
- Chrome 規定一個
user-data-dir只能有一個瀏覽器行程,並以標示持有者主機與行程的SingletonLock符號連結強制執行。 - 人不會碰到這個限制,因為第二次啟動會透過
SingletonSocket把命令列交給第一個,然後自己結束。 - 任一側只要帶上
--enable-automation,這個交接就不會發生,而 Puppeteer 每次啟動都會傳入它,所以人手開的瀏覽器會合流,自動化開的瀏覽器卻會相撞。 - 官方外掛執行伺服器時不帶任何參數,於是所有專案共用同一個設定檔目錄。
- 每個專案一份傳入
--userDataDir的.mcp.json可以解決,代價是登入變成每個專案各自一份,以及重複下載一整套 Chrome 元件。 - 它是以 repo 為單位的,所以同一個 repo 裡的兩個 session 仍然會相撞。
參考連結
chrome/browser/process_singleton_posix.cc,開頭註解說明了 SingletonLock、SingletonSocket 與 SingletonCookie 的設計chrome/app/chrome_main_delegate.cc,AcquireProcessSingleton 在這裡把 LOCK_ERROR 轉成結束碼 21- chrome-devtools-mcp,含 —userDataDir 與 —isolated 等伺服器選項的完整清單
- Claude Code 的 MCP 文件,說明 .mcp.json 與 enabledMcpjsonServers


