跳到主要内容

数据截至 (上游 commit 7e0457a7cbf8)

Playwright MCP — 架构与原理(总览)

30 秒导读: Playwright MCP 把浏览器自动化框架 Playwright 包成一个 MCP server,让大模型能像调工具一样开网页、点按钮、填表单。它最关键的取舍是:不给模型看截图,给模型看无障碍树快照——一段带 [ref=e2] 编号的结构化文本,模型直接引用编号来指定要点哪个元素。还有一个必须先知道的事实:你现在读的这个仓库已经没有核心源码了,它是一个 npm 发行壳,69 个工具的实现全部搬进了 Playwright 主仓,壳靠一行 require('playwright-core/lib/coreBundle') 把它们引进来。

本页是这组文档的总入口。先讲「这是什么」(零基础)、给顶层全景图、端到端走一遍「一次点击」的主线,再给阅读地图。各机制的细节在后续章节,本页不重复。


1. 这是什么(零基础也能懂)

一句话定义

Playwright MCP = 一个 MCP server,它把「操作浏览器」这件事拆成几十个工具,交给大模型调用;模型看到的页面不是图片,是一棵带编号的文本树。

拆开这句话里的三个词:

  • Playwright —— 微软的浏览器自动化框架,能用代码驱动 Chromium / Firefox / WebKit 打开网页、点击、输入、截图。
  • MCP(Model Context Protocol) —— 一套标准协议。你写一个 MCP server 暴露若干工具,任何 MCP client(VS Code、Claude、Cursor、Codex……)都能发现并调用它们。
  • 无障碍树快照(accessibility snapshot) —— 浏览器本来就为读屏软件维护着一棵语义树:每个可见元素是什么角色(button / link / textbox)、叫什么名字。这里把它序列化成文本喂给模型,代替截图

README 开篇把这个定位说得很直白(README.md:3):它让 LLM 通过结构化的无障碍快照与网页交互,「绕开截图和视觉调校模型的需要」。

解决谁的什么问题

场景:你让一个 AI agent「帮我去这个网站上把订单状态查出来」。

agent 必须回答两个问题——页面上现在有什么,以及我要操作的那个东西在哪。业界主要有两条路:

路线模型看到的定位方式代价
截图 / 视觉一张图片模型猜像素坐标需要视觉模型;坐标易错;图片吃 token
无障碍快照(本项目)一棵文本树模型引用节点编号 ref=e2需要页面语义可用;树大时也吃 token

Playwright MCP 选第二条。README 把这条路的好处列成三点(README.md:15-17):快且轻(用无障碍树而非像素输入)、对 LLM 友好(不需要视觉模型)、动作应用是确定性的(避免截图方案常见的歧义)。

它能做什么

默认开箱就有 24 个工具(见 §3 主线),加上按需开启的能力,一共 69 个工具,覆盖:

  • 看页面 —— 抓快照、在快照里搜索、截图、读 console、读网络请求。
  • 动页面 —— 点击、输入、拖放、选下拉、上传文件、批量填表、按键、处理弹窗。
  • 管会话 —— 导航、后退、多标签页、改窗口尺寸、关闭。
  • 按需开启 —— 网络 mock、cookie/localStorage 读写、PDF 导出、像素级鼠标动作、录屏与 trace、测试断言。

用起来什么样

绝大多数 MCP client 用同一份配置就能接上(README.md:32-46):

{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}

也可以当成一个独立 HTTP 服务跑(README.md:777-796):

npx @playwright/mcp@latest --port 8931
# 客户端侧只需配 url: http://localhost:8931/mcp

环境要求只有 Node.js 18+(README.md:20)。

一句话直觉

把网页当成一份带行号的大纲,而不是一张照片。 模型读大纲、说「我要动第 e2 行」,server 负责把「第 e2 行」翻译成一次真实点击。整篇文档剩下的内容,都是在讲这份大纲怎么生成、编号怎么保证指得准、以及这个能力怎么被打包发出去。


2. 顶层全景(它大概怎么转)

一个必须先接受的事实:这个仓库是空的

打开 src/ 目录,里面只有一个 src/README.md,内容是一句话:源码在 Playwright 主仓的 packages/playwright-core/src/tools/mcp(src/README.md:1-3)。CONTRIBUTING.md:25-27 用一个 WARNING 块重申了同一件事。

所以这个仓库的真实身份是:发行壳 + 配置规格 + 集成测试 + 发布流水线。它是「怎么把上游那套工具打包、配置、验证、发出去」的完整答案,而不是「工具怎么实现」的答案。

顶层结构图

怎么读:从上往下是一次调用的穿越顺序;虚线框内是本仓库能读到的代码,虚线框外只能看到它的调用面。

MCP client(VS Code / Claude / Cursor / Codex …)
│ stdio(默认) 或 HTTP(--port)

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ 本仓库:@playwright/mcp 发行壳 │
│ cli.js 命令行入口,装配后转发 │
│ index.js 库入口,导出 createConnection │
│ config.d.ts 配置类型 = 唯一的「规格书」 │
└ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
│ require('playwright-core/lib/coreBundle')

playwright-core 里的 tools 模块(上游,本仓库不含)
· 装配 CLI 命令 · 注册 69 个工具
· 生成 aria 快照 · 执行动作、组装响应


Chromium / Firefox / WebKit

部件一句话职责

部件干什么在哪个文件
CLI 入口特判 install-browser,否则把命令交给上游装配,再 parseAsynccli.js:20-33
库入口只导出一个 createConnection,给程序化嵌入用index.js:18-19
类型声明声明 createConnection(config?, contextGetter?) 的签名index.d.ts:17-22
配置规格Config 类型:浏览器、server、capabilities、超时、网络、快照、护栏config.d.ts:33-250
工具目录README 里 69 个工具的完整清单,由脚本从编译产物生成README.md:862-1604
生成器读编译产物 + 跑 --help + 抽 Config 类型,回填 README 三段update-readme.js:238-246
版本滚动把 playwright 三个包钉到新版、复制 config.d.ts、重跑生成roll.js:37-42
集成测试用真 MCP client 起一个真 server 子进程,黑盒断言响应tests/fixtures.ts:77-119
容器化四阶段构建,运行期只有 headless chromiumDockerfile:1-67

一句话说明主线

客户端把参数交给 cli.jscli.js 立刻把命令装配和执行权交给上游的 tools 模块 → 上游按 capability 挑出该暴露哪些工具 → 每次工具调用回来一段分节的 markdown,里面同时装着「等价的 Playwright 代码」和「新的页面快照」→ 模型读快照里的 ref 编号,决定下一步点谁。


3. 主线走一遍:一次 browser_click 的端到端

这一节不进代码细节,只把顺序串起来。每一步都有集成测试作证据。

时序图

① 客户端 spawn:node cli.js [--headless --browser=chrome …]
依据:package.json:47-49(bin)、tests/fixtures.ts:202-215


② tools/list → 默认 24 个工具名
依据:tests/capabilities.spec.ts:19-47


③ browser_navigate { url }
响应 ### Ran Playwright code : await page.goto('…');
响应 ### Snapshot : - button "Submit" [ref=e2]
依据:tests/click.spec.ts:31-37


④ 模型从快照里读出编号 e2


⑤ browser_click { element:"Submit button", target:"e2" }
响应 ### Ran Playwright code : page.getByRole('button',{name:'Submit'}).click()
响应 ### Snapshot : button "Submit" [active] [ref=e2]
依据:tests/click.spec.ts:39-48

每步在讲什么

① 启动。 package.json:47-49playwright-mcp 这个 bin 指向 cli.js;测试里也是直接 node cli.js 起一个 stdio 子进程(tests/fixtures.ts:202-215)。没有编译步骤——package.json:26 里的 build 脚本字面就是 echo OK

② 工具清单。 不带任何 --caps 时,client 能看到的工具正好 24 个,测试把这个集合逐字钉死(tests/capabilities.spec.ts:21-46)。而 README 生成出的完整目录有 69 个——差额全是需要显式开启的能力。这就是 capability 门控,详见 02 章

③ 快照登场。 导航后的响应里,页面被序列化成一棵 YAML 风格的树,每个可交互节点带一个 [ref=eN] 编号。测试断言的字面量就是 - button "Submit" [ref=e2](tests/click.spec.ts:36)。

④ 模型只需要读编号。 它不需要知道 CSS 选择器、不需要坐标、不需要看图。

⑤ 动作 + 回执。 browser_click 收两个关键参数:element 是给人看的自然语言描述,target 是那个 e2。响应里回吐的不是「点击成功」,而是一行等价的 Playwright 代码 page.getByRole('button', { name: 'Submit' }).click()(tests/click.spec.ts:46),外加动作之后的新快照。

这条主线里最值得记住的一点

每次动作都返回「代码 + 新快照」这一对。 前者让整个 agent 会话天然可以被沉淀成一份可复现的 Playwright 脚本;后者让模型不必再单独调一次 browser_snapshot 就能看到动作后的世界。响应的完整分节结构在 02 章 里拆。


4. 阅读地图

建议按顺序读;每章都能独立跳。

章节讲什么什么时候该翻它
01-packaging-and-entrypoints.md一个只发 5 个文件的 npm 包怎么把请求转给上游;cli.js 的两条分支;Docker 四阶段想知道「壳里到底有什么」「怎么嵌进自己的服务」
02-tool-surface.md69 个工具的完整分组表、快照-ref 定位模型、响应分节格式、read-only 语义想知道「模型能调什么」「一次调用回来什么」
03-config-and-guardrails.md46 个 CLI 选项 / 环境变量 / 配置文件三条入口;profile 隔离;那些自称「不是安全边界」的护栏要部署、要调参、要评估风险
04-generated-readme-and-roll.mdREADME 的三段生成器、roll.js 版本滚动、CI 用 git diff --exit-code 钉一致性想学「文档不漂移」的工程做法
05-test-harness-and-release.md真 client + 真进程的黑盒集成测试、自定义断言、四条发布线要给 MCP server 写测试;要看发布工程

5. 巧妙之处(可借鉴的技术)

5.1 快照即坐标系:把「指哪」这件事从视觉问题降级成字符串问题

妙在哪: 视觉方案里,「点哪」是一个坐标预测问题,天然带误差。这里给每个节点发一个短编号,模型只需要复述编号,server 侧再把编号解析回一个真实的 Playwright locator。误差被消灭在协议层,而不是靠模型更聪明。

证据在快照的字面格式上:- button "Submit" [ref=e2](tests/click.spec.ts:36),动作后同一节点变成 button "Submit" [active] [ref=e2](tests/click.spec.ts:47)——编号在动作前后保持稳定,[active] 这类状态标记单独叠加。

5.2 每个动作参数都拆成「给人看的」和「给机器用的」两半

妙在哪: 几乎所有动作类工具都同时收 elementtarget。README 对 element 的原文描述是「用于取得与该元素交互的许可的人类可读描述」(README.md:874),target 才是「快照里的精确引用或唯一选择器」(README.md:875)。

一个参数服务于用户授权界面(让人看懂 agent 要点什么),另一个服务于执行。同一个工具签名里就把「可解释性」和「可执行性」分开了——这在 browser_drag 上更明显,它有 startElement/startTarget/endElement/endTarget 四个参数(README.md:901-909)。

5.3 动作回执是代码,不是「OK」

妙在哪: 响应里带一节 ### Ran Playwright code(tests/fixtures.ts:256),内容是本次动作等价的 Playwright 语句。这带来三个额外收益,而实现成本几乎为零:

  • agent 的探索过程可以直接沉淀成一份回归测试脚本;
  • 人类审查 agent 干了什么时,读的是精确代码而不是自然语言复述;
  • 配合 --codegen 选项还能换语言,支持 typescript / python / java / csharp / none(config.d.ts:246-249)。

5.4 默认只暴露 24/69:能力门控既省 token 又缩攻击面

妙在哪: 工具 schema 是要进模型上下文的,69 个工具的 schema 是一笔不小的固定开销。这里把工具按 capability 分成 9 组,默认只挂 core 相关的 24 个(tests/capabilities.spec.ts:21-46),--caps=pdf / vision / devtools / network / storage / testing / config 按需追加。

顺带把「能删 cookie」「能 mock 网络」「能跑任意 Playwright 代码」这类高权限工具默认关在门外。

5.5 browser_find:给快照配一个 grep

妙在哪: 全量快照在复杂页面上很贵。browser_find 让模型用文本或正则在快照里搜,只返回命中节点及其上下文,并附上它从根节点起的路径——README 自己的说法是「当你只需要定位一个元素和它的 ref 时,这比抓整棵树便宜」(README.md:957)。正则还支持 /error/i 这样带 flag 的写法(README.md:960)。

这是「渐进式披露」在工具层的实现:先搜后取,而不是每轮都拉全量。

5.6 README 是编译产物的投影,CI 拿 git diff 钉死

妙在哪: update-readme.js 不是解析源码,而是 require('playwright-core/lib/coreBundle') 拿到真实注册的工具对象,再把 schema 渲染成 markdown(update-readme.js:23update-readme.js:124-141)。CI 的 lint job 跑完生成器后直接 git diff --exit-code(.github/workflows/ci.yml:20-22)——文档一旦和代码不一致,构建就红。

更狠的一处:遇到 capability 映射表里没有的新 capability,生成器直接抛异常(update-readme.js:41-43),逼人去更新映射。

5.7 .npmignore 用反向允许清单,把发行包压到 5 个文件

妙在哪: 常规写法是列出要排除什么,容易漏。这里第一行直接 **/* 全排,再逐条 ! 放行(.npmignore:1-6)。发出去的 tarball 只剩 README、LICENSE、cli.jsindex.*config.d.ts 加上 npm 强制包含的 package.json

5.8 持久 profile 目录名里塞 workspace 哈希

妙在哪: 默认 profile 路径形如 ~/.cache/ms-playwright/mcp-{channel}-{workspace-hash}(README.md:475),而 {workspace-hash} 来自 MCP client 的 workspace 根目录,「所以不同项目自动拿到各自的 profile」(README.md:478)。登录态按项目隔离,不需要用户手动配 --user-data-dir


6. 边界与局限(诚实)

6.1 项目自己声明的边界

边界原文位置
Playwright MCP 不是安全边界README.md:800
--allowed-origins / --blocked-origins 不是安全边界,且不影响重定向README.md:410README.md:412
secrets 只是「便利,不是安全特性」,务必在 client 侧自己检查config.d.ts:149-154
allowUnrestrictedFileAccess 是防误伤的护栏,「刻意绕开很容易」config.d.ts:238-244
browser_run_code_unsafe 「等价于 RCE」README.md:1043-1046
Docker 镜像目前只支持 headless chromiumREADME.md:804
持久 profile 同时只能被一个浏览器实例使用,并发 client 会冲突README.md:481

6.2 项目自己承认的定位收缩

README 有一整节专门讲「什么时候用 MCP」(README.md:5-11):对编码 agent,它推荐改用 Playwright CLI + SKILLS,理由是 CLI 调用更省 token——不用把庞大的工具 schema 和冗长的无障碍树塞进上下文。MCP 被留给「需要持久状态、丰富自省、对页面结构反复推理」的场景。

一个 MCP server 的官方 README 主动劝退一部分用户,这本身是这个项目最值得注意的一条边界。

6.3 从这份克隆里读不到的东西

本仓库不含工具实现,因此以下问题在这里没有答案,必须去上游 packages/playwright-core/src/tools/ 读:

  • ref=eN 编号是怎么分配的、页面变化后怎么保持稳定;
  • 快照的裁剪、depth 限制、boxes 坐标是怎么算的;
  • --caps 到底是怎么过滤工具注册的;
  • 46 个 CLI 选项如何合并进 Config 对象;
  • HTTP transport、--extension 连接、sharedBrowserContext 的实现。

本文所有关于这些的描述,都只到「对外可观察的行为」为止。

6.4 观察到的两处不一致

其一:--caps 的帮助文本落后于实际能力集。 CLI 帮助说可选值是 vision, pdf, devtools(README.md:415),但同一份 README 生成出的工具目录里还有 --caps=config--caps=network--caps=storage--caps=testing 四组(README.md:1136README.md:1149README.md:1194README.md:1547)。

其二:config.d.ts 顶部的 import 路径没被 roll.js 改写。 roll.js:9-12 的意图是把复制过来的 config.d.ts 里的 import type * as playwright from 'playwright-core'; 替换成 'playwright',但当前文件里这一行是 import type * as playwright from '../../..';(config.d.ts:17)——一个只在 Playwright 主仓目录结构下成立的相对路径,替换未命中。详见 04 章

6.5 「Browser installation」是个空分组

README 里 Browser installation 这一节渲染出来是空的(README.md:1130-1134)。原因在生成器里:它过滤掉了所有 skillOnly 的工具(update-readme.js:49)。也就是说 core-install 这一类工具存在,但被标成「只给 skill 用」,不出现在 MCP 工具目录里。


7. 横向对比(同货架的兄弟项目)

本货架 browser-agents 区里,几乎每个项目都要回答同一组问题:页面怎么表示给模型、元素怎么定位、浏览器从哪来。放在一起看差异就很清楚:

项目页面表示定位方式浏览器来源交付形态
playwright-mcp(本文)无障碍树快照(YAML)ref=eN 编号Playwright 自己拉起 / CDP / 扩展MCP server
chrome-devtools-mcp文本快照uid 编号Puppeteer 驱动真实 ChromeMCP server
agent-browser快照 + refsref常驻 daemon + CDPclient/daemon
midscene截图为主视觉定位(坐标)多适配SDK / 多模型适配
browser-use-web-uiDOM 抽取索引化元素PlaywrightGradio 应用
notte自建 perception 层动作空间自管agent 框架
steel-browser不做页面表示提供浏览器基础设施浏览器 API 服务

和最接近的兄弟 chrome-devtools-mcp 比: 两者形态几乎一样(都是 MCP server、都用文本快照 + 编号定位),分歧在底座和侧重。chrome-devtools-mcp 走 Puppeteer + 真实 DevTools 引擎,强项在调试与性能分析;playwright-mcp 走 Playwright,强项在跨三种浏览器引擎、以及把动作回吐成可复现的测试代码。

和视觉派(midscene / 各 computer-use 项目)比: 本项目彻底不碰坐标——vision 那一组像素级鼠标工具需要显式 --caps=vision 才出现(tests/capabilities.spec.ts:58-67),摆明了是逃生舱而非主路。

独一份的一点: 在整个货架里,这是少数把发行工程本身当作仓库主体内容的项目——源码搬走后,剩下的全是打包、配置规格、生成、测试、发布。0405 两章的可借鉴价值,反而超过它的浏览器能力本身。


8. 代码地图(导航索引)

主题文件路径符号名 / 锚点
CLI 入口与 install-browser 特判cli.jsdecorateProgramdecorateMCPCommandparseAsync
库入口(程序化嵌入)index.jscreateConnection
库入口类型签名index.d.tscreateConnection(config?, contextGetter?)
配置全量规格config.d.tsConfigToolCapability
能力→标题映射 + 未知能力守卫update-readme.jscapabilitiesunknownCapabilitiescapabilityTitle
工具目录生成update-readme.jsupdateToolsformatToolForReadme
选项表生成(跑 --help 再解析)update-readme.jsupdateOptionsoptionEnvName
配置 schema 生成(正则抽 Config)update-readme.jsupdateConfig
生成段落的通用替换update-readme.jsupdateSection
版本滚动roll.jsdoRollupdatePlaywrightVersioncopyConfig
测试:起 MCP client / servertests/fixtures.tsstartClientcreateTransport
测试:响应分节解析tests/fixtures.tsparseResponseparseSectionstoHaveResponse
测试:默认工具集合(24 个)tests/capabilities.spec.tstest('test snapshot tool list')
测试:快照 + ref + 代码回执tests/click.spec.tstest('browser_click')
测试:CommonJS 可用性回归tests/library.spec.tstest('library can be used from CommonJS')
测试用 HTTP/HTTPS 服务器tests/testserver/index.tsTestServersetContentreset
容器化(四阶段)DockerfilebasebuilderbrowserENTRYPOINT
发布包文件清单.npmignore反向允许清单
MCP Registry 元数据server.jsonnamepackages[].transport
CI(lint / 三平台测试 / docker 测试).github/workflows/ci.ymllinttesttest_mcp_docker
发布(npm canary / release / registry / docker).github/workflows/publish.ymlpublish-mcp-canary-npmpublish-mcp-release-registry
ESRP alpha 发布.azure-pipelines/publish.ymlEsrpRelease@11
源码去哪了src/README.mdCONTRIBUTING.md指向 packages/playwright-core/src/tools/mcp