返回博客

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 都共享这同一个浏览器实例

--isolatedMCP server 的启动参数,不是 per-call 参数——它需要在启动 npx @playwright/mcp 时传入,而不是在每次工具调用时设置。你无法从工具调用层面绕过这个限制。

此外,即便你能"抢回"那个 MCP 浏览器,你的截图脚本和另一个 session 会互相 navigate 对方的页面——谁也截不了干净的图。

坑:强行复用共享 MCP 浏览器会劫持对方 session 的页面

两个 session 共享同一个 MCP 浏览器实例时,任何一方的 goto 都会把另一方正在操作的页面跳走。结果是两边都乱,而不是只有一方失败。不要尝试通过 kill 进程或删除 profile 来"释放"它——这会直接摧毁另一个 session 的工作状态。

正解

完全绕开 MCP 浏览器,自己启动一个独立的无头 Chromium 进程。

核心思路:playwright-corechromium.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
正解:/tmp 里的 playwright-core + executablePath,与 MCP 浏览器零交集

这个进程拥有独立的 browser 实例和独立的 context,完全不碰 MCP 的 profile 目录。另一个 session 的浏览器继续正常工作,截图 session 也能稳定批量出图。

细节:addInitScript 要在 newPage() 之前调用

addInitScript 挂载在 context 上,会在每个新页面的任何脚本执行之前注入。如果在 goto 之后才设置 localStorage,客户端框架可能已经读完主题配置,你的注入就晚了一步,截出来的主题是错的。顺序是:newContextaddInitScriptnewPagegoto

一句话外卖

共享资源被占用时,比"等待释放"更好的策略往往是"自己带一份"——找到缓存的二进制,在隔离目录里启动完全独立的进程。