MCP 浏览器被并发 Session 占用时,用 executablePath 启动独立 Chromium
在跑多个并行 Agent session 的场景里,截图任务是个高频需求——验证组件渲染、捕捉主题切换效果、跑视觉回归。但有一天我需要在一个截图 session 里批量截图,而另一个 session 同时在用 Playwright MCP 做端到端测试。结果截图 session 直接挂了。
现象
调用 browser_take_screenshot 工具,报错:
Error: Browser is already in use for /…/ms-playwright/mcp-chrome-<hash>,
use --isolated to run multiple instances of the same browser
错误信息看起来给了答案——用 --isolated。但找遍工具参数,根本没有这个字段。
根因
MCP 服务端(playwright-mcp 或 chrome-devtools-mcp)在启动时绑定了一个固定的 browser profile 目录(…/ms-playwright/mcp-chrome-<hash>)。整台机器上所有调用该 MCP server 的 Claude session 都共享这同一个浏览器实例。
--isolated 是 MCP server 的启动参数,不是 per-call 参数——它需要在启动 npx @playwright/mcp 时传入,而不是在每次工具调用时设置。你无法从工具调用层面绕过这个限制。
此外,即便你能"抢回"那个 MCP 浏览器,你的截图脚本和另一个 session 会互相 navigate 对方的页面——谁也截不了干净的图。
两个 session 共享同一个 MCP 浏览器实例时,任何一方的 goto 都会把另一方正在操作的页面跳走。结果是两边都乱,而不是只有一方失败。不要尝试通过 kill 进程或删除 profile 来"释放"它——这会直接摧毁另一个 session 的工作状态。
正解
完全绕开 MCP 浏览器,自己启动一个独立的无头 Chromium 进程。
核心思路:playwright-core 的 chromium.launch({ executablePath }) 允许指向任意 Chromium 二进制文件,不需要重新下载浏览器,也不会和 MCP 的 browser profile 有任何交集。
第一步:找到已缓存的 Chromium 二进制
Playwright/MCP 装过之后会在本机缓存一份 Chromium:
# macOS
ls ~/Library/Caches/ms-playwright/chromium-*/chrome-mac/Chromium.app/Contents/MacOS/Chromium
# Linux
ls ~/.cache/ms-playwright/chromium-*/chrome-linux/chrome第二步:在 /tmp 安装 playwright-core(不要动项目目录)
mkdir -p /tmp/shot && cd /tmp/shot
npm init -y > /dev/null
npm i playwright-core > /dev/null第三步:写截图脚本
// /tmp/shot/shot.mjs
import { chromium } from "playwright-core";
const EXEC =
"/Users/me/Library/Caches/ms-playwright/chromium-1124/chrome-mac/Chromium.app/Contents/MacOS/Chromium";
const jobs = [
{ url: "http://localhost:3000/components/checkbox", theme: "light", waitFor: ".demo-root", out: "/tmp/checkbox-light.png" },
{ url: "http://localhost:3000/components/checkbox", theme: "dark", waitFor: ".demo-root", out: "/tmp/checkbox-dark.png" },
];
const browser = await chromium.launch({ executablePath: EXEC, headless: true });
for (const job of jobs) {
const ctx = await browser.newContext({
viewport: { width: 1280, height: 1400 },
deviceScaleFactor: 2,
});
// 在任何页面脚本执行前注入 localStorage,确保主题在首屏就生效
await ctx.addInitScript(
(theme) => { try { localStorage.setItem("app-theme", theme); } catch {} },
job.theme
);
const page = await ctx.newPage();
await page.goto(job.url, { waitUntil: "networkidle", timeout: 30_000 });
await page.waitForSelector(job.waitFor, { timeout: 15_000 });
await page.screenshot({ path: job.out, fullPage: true });
await ctx.close();
}
await browser.close();node /tmp/shot/shot.mjs这个进程拥有独立的 browser 实例和独立的 context,完全不碰 MCP 的 profile 目录。另一个 session 的浏览器继续正常工作,截图 session 也能稳定批量出图。
addInitScript 挂载在 context 上,会在每个新页面的任何脚本执行之前注入。如果在 goto 之后才设置 localStorage,客户端框架可能已经读完主题配置,你的注入就晚了一步,截出来的主题是错的。顺序是:newContext → addInitScript → newPage → goto。
一句话外卖
共享资源被占用时,比"等待释放"更好的策略往往是"自己带一份"——找到缓存的二进制,在隔离目录里启动完全独立的进程。