自定义构建的 Puppeteer MCP 服务器配置
把 Puppeteer MCP 服务器指向指定 Chromium 构建与加密 profile,说明扁平启动对象与 Playwright 嵌套结构为何不能互换。
BotBrowser Team
Puppeteer MCP 服务器掌握浏览器启动过程,交给它的配置决定每个代理任务运行在哪个 Chromium 二进制与哪份 profile 上。Puppeteer 的 LaunchOptions 与 Playwright 的形状不同,在两者之间复制配置会静默丢掉对侧不存在的部分。故障是安静的:服务器启动,工具正常响应,浏览器仍是服务器本来会用的那个。Puppeteer 把 executablePath 记录为替代自带浏览器的路径,同时声明只保证支持自带构建,因此该选项属于显式决定,而不是透明设置。BotBrowser 从 Puppeteer 启动时使用 executablePath 加 args 中的 profile 参数,自身不发布 MCP 服务器,浏览器一侧无法修正一个从不转发这些值的服务器。
TL;DR
- Puppeteer 的启动对象把
executablePath、args、headless、userDataDir、ignoreDefaultArgs、pipe和browser放在同一层。Playwright 把同样的内容嵌在launchOptions下,两种形状不能互换。 - Puppeteer 的
headless接受true、false或字符串'shell',其中'shell'选择旧的 headless 实现。写一个 Playwright 风格的布尔值在这里合法;整份复制 Playwright 配置则不合法。 executablePath替代自带浏览器,Puppeteer 文档声明只保证自带构建可用。自定义路径属于受支持但未经证实的状态,需要回读进程来确认。- profile 在
args中占独立元素,和 Playwright 下一样,因为它是浏览器参数而不是 Puppeteer 选项。 ignoreDefaultArgs与pipe改变服务器交给进程的内容。设置ignoreDefaultArgs: true的服务器会丢掉 Puppeteer 本会追加的 flag,实际 argv 既不是配置里的,也不是 Puppeteer 默认的。
目录
Puppeteer 启动对象的位置
Puppeteer 的 LaunchOptions 是传给 puppeteer.launch() 的单个扁平对象。executablePath 是它的顶层成员,与 args、headless、userDataDir、ignoreDefaultArgs、pipe、browser、channel、debuggingPort 并列。该对象整体就是启动选项,因此不存在 launchOptions 包装层。
{
"mcpServers": {
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"],
"env": {
"PUPPETEER_EXECUTABLE_PATH": "/opt/botbrowser/chrome",
"PUPPETEER_LAUNCH_OPTIONS": "{\"headless\":true,\"args\":[\"--bot-profile=/etc/botbrowser/profile.enc\"]}"
}
}
}
}
上面的例子给出服务器接收取值的两种途径:可执行文件用专用环境变量,其余内容用一段 JSON。这两个名字在 Puppeteer 系服务器之间没有统一标准,因此第一步是查服务器自己的文档,确认它读取的确切变量或 flag,再确认该值真的到达启动对象。
| Puppeteer 成员 | 类型 | 对自定义构建的作用 |
|---|---|---|
executablePath | string | 替代自带浏览器;Puppeteer 只保证自身构建可用 |
args | string[] | 额外命令行参数,一个数组元素对应一个参数 |
headless | boolean 或 'shell' | true 使用新 headless 模式,'shell' 使用旧模式 |
userDataDir | string | 持久化 profile 目录,与 BotBrowser 的 profile 参数不同 |
ignoreDefaultArgs | boolean 或 string[] | 丢弃 Puppeteer 默认 flag,或按名称过滤掉指定项 |
pipe | boolean | 通过 stdio 而不是 WebSocket 承载协议 |
browser | SupportedBrowser | 选择 Puppeteer 应期待的浏览器家族 |
channel | ChromeReleaseChannel | 在已知系统位置查找已安装的 Chrome,而不是使用自带二进制 |
自定义构建部署中有两行需要特别处理。channel 与 executablePath 都能指向一个浏览器,Puppeteer 文档把 channel 描述为在系统已知位置查找常规 Chrome 安装。两者同时设置会让同一个问题有两个答案,最终哪个生效属于实现细节,消除其中一个比测试哪个胜出更可靠。
扁平形状为何容易误读
扁平对象没有结构信号区分哪些键属于启动层、哪些属于上下文层。在 Playwright 下,放错位置的键会落进另一个对象并被忽略;在 Puppeteer 下,LaunchOptions 中不存在的键根本不属于选项,puppeteer.launch() 也没有理由报错。两条路径通向同一个静默结果:服务器启动,某一项设置从未生效。
有效的检查方式是把对象展开,与文档列出的 LaunchOptions 成员逐键对照。不在这份清单上的键就不是启动选项,无论它看起来多像。
Puppeteer 遇到不可用路径时的行为
executablePath 指向一个文件,Puppeteer 启动该文件。路径不存在、在服务器工作目录下无法解析,或文件存在但运行服务器的账户没有执行权限,启动器会报错,而不会替换成另一个二进制文件;这一点来自 Puppeteer 把给定路径交给它启动的进程,属于对启动器行为的推断,而不是 LaunchOptions 页面上的原话。这一行为值得记住,因为它与安静失败相反:存在但写错的路径会产生一个能工作却非预期的浏览器,不存在的路径会产生操作者能看到的错误。
真正隐藏的故障是路径有效却指向别处。配置缺失时替换为默认路径的服务器正好制造这种结果:可执行文件有效、浏览器能工作、没有任何报错。检查必须比较观测到的路径与预期路径,而不是只确认某个路径存在。
服务器在配置之上追加的内容
Puppeteer 系 MCP 服务器是包装层:读取自身配置、构造 LaunchOptions 对象、调用 puppeteer.launch()。服务器在配置与这次调用之间追加的内容,从外部看不见。这正是回读在此处比直接脚本更重要的原因,直接脚本里的选项对象就是操作者写下的那一份。
这种封装在 Playwright 一侧的变体,包括它的嵌套结构与静默回退到内置浏览器,见为自定义 Chromium 构建配置 Playwright MCP;代理循环与浏览器进程之间的职责划分见面向 AI Agent 的 headless 浏览器。服务器可能追加默认 args 数组、headless 默认值或 ignoreDefaultArgs 取值。追加自身参数的服务器产生的 argv 是配置与默认值的并集,顺序也不由操作者决定。读取运行中的进程是观察这个并集的唯一方式。
实际 argv 为何比配置文件更重要
两次运行可以共用同一份配置文件却启动不同浏览器,只要服务器版本在两次之间改变了默认值。开始追加新默认 flag 或停止追加某个 flag 的服务器,会在操作者文件未变的情况下改变实际 argv。浏览器读取的是 argv 而不是服务器配置,可观察行为跟随 argv。
因此实际 argv 应与观测结果一起记录,而不只是记录产生它的配置。配置加服务器版本可复现;一旦服务器能改变追加内容,单独的配置不足以复现。
与 Playwright MCP 形状的差异
两个生态用不同结构回答同一个问题,差异正好落在复制配置会失效的位置。
| 关注点 | Playwright MCP | Puppeteer 启动对象 |
|---|---|---|
executablePath 的位置 | browser.launchOptions.executablePath | 顶层 executablePath |
args 的位置 | browser.launchOptions.args | 顶层 args |
| headless 取值 | launchOptions 内的布尔 headless | 布尔值或字符串 'shell' |
| 上下文级设置 | 独立的 browser.contextOptions 对象 | 不属于 LaunchOptions;viewport 由服务器或代码另行设置 |
| profile 持久化 | browser 下的 userDataDir | 顶层的 userDataDir |
| 连接已运行的浏览器 | cdpEndpoint 或 remoteEndpoint | LaunchOptions 之外的 browserWSEndpoint 或 connect() |
前两行解释了为何把 Playwright MCP 的 JSON 块粘进 Puppeteer 服务器不起作用:这些键不在 Puppeteer 服务器查找的两个位置中的任何一个。第三行更隐蔽,headless: true 在两边都合法,复制品看似能用,周围结构却是错的。
上下文拆分在这里不存在
Playwright 把启动选项与上下文选项分开,viewport、locale、权限因此放在另一个对象里。Puppeteer 的 LaunchOptions 没有这种拆分:viewport 与页面级设置在启动之后通过 page.setViewport() 或设备描述符应用。期待 contextOptions 块承载 locale 的配置找不到对应字段。
任务依赖 locale 或时区时,这一差异有直接影响。Playwright MCP 下它们属于上下文,在页面存在之前配置。Puppeteer 下它们要么是 args 中的浏览器参数,要么是启动后的脚本调用,服务器必须提供其中一条路径。
仿真调用是第三条路径
page.emulate() 与 page.setViewport() 在浏览器启动后改变页面报告的内容,因此 locale、时区、viewport 还有第三个来源。通过 args 设置 locale、通过仿真调用设置另一个属性、再让服务器默认值设置第三项的负载,有三个来源而没有唯一答案。
保持可控的规则是每个属性只用一个来源。浏览器在首个请求之前必须知道的内容放进 args,页面加载之后才需要的内容用仿真调用。记录每个属性走过的路径,因为在 args 中设置 locale 的运行与通过仿真设置的运行,比较的不是同一件事。
两种不同的 profile 概念
Puppeteer 的 userDataDir 与 BotBrowser 的 profile 参数都描述持久状态,两者并不相同。userDataDir 是 Puppeteer 透传给浏览器的 Chromium 用户数据目录。BotBrowser profile 是二进制通过自身 flag 加载的加密配置文件。设置其中一个不会设置另一个,只暴露 userDataDir 的服务器没有 profile flag 的位置,除非它转发任意 args。
传递 profile 与 headless 模式
profile flag 是浏览器参数,因此作为单个元素放进 args。headless 模式是 Puppeteer 选项,因此放进 headless。形状混淆之后,下一个要检查的配置错误就是把两者放反。
{
"executablePath": "/opt/botbrowser/chrome",
"headless": true,
"args": ["--bot-profile=/etc/botbrowser/profile.enc", "--bot-locale=en-US", "--no-first-run"]
}
每个元素是一个参数,不带 shell 引号。包含空格的单个字符串是一个畸形参数。例子中的第三个元素是普通 Chromium flag 而不是 BotBrowser flag,这说明 args 可以自由混合两类内容:Puppeteer 不区分它们。
引号、转义与 JSON 层
配置是 JSON 时,参数经过两层:JSON 字符串转义,然后是参数向量。C:\botbrowser\chrome.exe 这样的 Windows 路径需要在 JSON 中转义反斜杠,含引号的值需要 JSON 转义而不是 shell 转义。一层正确一层错误,产生的路径在文件里看着对,启动时却是错的。
不含空格与引号的 profile 路径绕开整类问题,短目录下的绝对路径也比深层目录中的长路径更容易逐字符核对。
headless、'shell' 与随之改变的内容
Puppeteer 文档记载 headless 接受 true 或字符串 'shell',true 选择新 headless 模式,'shell' 选择旧模式。区别不只是外观:两种模式在浏览器合成方式与哪些 API 报告什么上不同。比较渲染结果或读取媒体能力的负载应记录本次运行使用的模式,因为不同模式的两次运行不是同一个测量。
ignoreDefaultArgs 何时改变结论
ignoreDefaultArgs 接受 true 或 flag 名称数组。设为 true 时,Puppeteer 停止追加默认参数,浏览器只以 args 中的内容启动。传入数组时,指定默认项被过滤,其余保留。两者都改变实际 argv,也都可能由服务器自行设置。
运维后果是一个文档记载的 BotBrowser flag 可能被 Puppeteer 追加的默认项抵消,Puppeteer 依赖的默认项也可能被 ignoreDefaultArgs 移除。这两种情况在配置文件中都不可见,验证步骤因此读取进程而不是读取配置。
pipe 与服务器和浏览器的通信方式
pipe: true 通过标准输入输出承载自动化协议,不打开 WebSocket。把浏览器作为子进程运行的服务器因此少一个监听端口,故障模式与安全面都会变化。假定调试端口存在的配置在 pipe 打开时不成立,通过端口连接的工具无法到达管道浏览器。
记录每次运行使用的传输方式。管道上得到的结果与端口上得到的结果,只有在其余配置一致时才来自同一个浏览器,传输方式属于配置的一部分而非实现细节。
确认实际启动的二进制
验证有三个读数,回答不同问题。进程路径证明执行了哪个文件。user agent 描述页面报告的内容,自定义构建可以独立于二进制配置它。构建标记证明一个自带构建缺少的能力。
先读进程。在生成服务器的宿主机上找到子进程,把它的可执行文件与配置路径比较。未转发 executablePath 的服务器会显示 Puppeteer 缓存目录下的路径,这一次比较就能定论,不需要页面级探测。
| 读数 | 能证明 | 不能证明 |
|---|---|---|
| 子进程可执行文件 | 服务器执行了哪个文件 | profile 已加载 |
navigator.userAgent | 页面报告了什么 | 哪个二进制产生了它 |
| 构建标记 | 某个能力存在 | 配置按预期路径到达了它 |
| profile 加载证据 | 加密 profile 被接受 | 页面内容正确 |
进程树不可见时
运行在容器、远程主机或沙箱中的服务器,可能不向操作者的 shell 暴露子进程。此时路径必须来自拥有进程的环境内部,通过服务器写入的日志行、/proc 条目或平台提供的诊断工具获取。要求不变:在可获得观测的位置,把观测到的可执行文件与配置比较。
这些都不存在时,页面内的构建标记是退路证据,并且应被描述为较弱。标记证明能力存在,不证明提供页面的进程就是操作者配置的那个。
记录一次验证
把配置路径、观测路径、headless 模式、实际 args 与标记结果连同时间戳存在一起。后续运行可以直接比较,不必重新推导基线。服务器更新改变默认 args 时,这条记录让变化可见。
客户端看不到的内容
MCP 客户端看到服务器发布的工具,看不到启动对象、进程树或传入的参数。从客户端一侧,配置错误的服务器与正确的服务器无法区分,因此验证必须通过会话请求,并记录在会话之外。
Puppeteer 路径的症状表
| 症状 | 原因 | 确认方式 | 修复 |
|---|---|---|---|
| 浏览器启动,profile 设置无效 | args 缺少 profile flag | args 中没有以 profile flag 开头的元素 | 每个 flag 增加一个数组元素 |
| 浏览器从缓存目录启动 | executablePath 未进入启动对象 | 子进程路径落在 Puppeteer 缓存下 | 把键移到启动对象顶层 |
| 服务器以启动错误退出 | 路径缺失或不可执行 | 该路径对服务器账户无法解析 | 使用账户可执行的绝对路径 |
| 服务器更新后 flag 行为改变 | 服务器改变了默认 args | 实际 argv 与记录不同 | 记录 argv 并固定服务器版本 |
| 工具无法到达浏览器 | pipe 与端口传输不匹配 | 配置设置 pipe,工具却期待端口 | 每个部署只选一种传输 |
第二行是能躲过整个工作会话的一项。退回自带浏览器的服务器会正确回答每次工具调用,唯一证据是进程路径。
FAQ
Playwright MCP 配置能复用于 Puppeteer 服务器吗?
不能。Playwright MCP 把 executablePath 与 args 嵌在 browser.launchOptions 下,Puppeteer 要求两者位于启动对象顶层。粘贴的配置块让 Puppeteer 服务器得不到可执行文件路径,服务器于是启动自带浏览器并报告成功。
headless: 'shell' 与 headless: false 相同吗?
不同。'shell' 选择旧 headless 实现,仍然没有可见窗口,但合成方式与报告内容不同于新 headless 模式。false 运行有头浏览器。记录一次运行使用三者中的哪一个,才能让两次测量可比。
userDataDir 能替代 BotBrowser profile 吗?
不能。userDataDir 是 Puppeteer 传给浏览器的 Chromium 用户数据目录。BotBrowser profile 是二进制通过 args 中自身 flag 加载的加密配置文件。只暴露 userDataDir 的服务器没有 profile 的位置,除非它转发任意参数。
实践结论
扁平启动对象把 executablePath、args、headless 放在同一层,profile flag 作为独立元素放进 args。宿主机上的子进程可执行文件与配置路径比较之后,会话结果才具备可信依据。
下一步是一次启动验证:服务器启动后由客户端执行一次导航,再在宿主机上读取子进程路径。路径与配置一致,二进制得到证明;路径落在 Puppeteer 缓存里,说明服务器忽略了该选项,每个结果描述的都是一个未被选择的浏览器。
Sources
本文属于指南: 浏览器部署与发布:运维决策地图