跳到主要内容

入口与包壳:cli.js / index.js / createConnection

30 秒导读: @playwright/mcp 这个 npm 包对外只有几十行代码。命令行入口 cli.js、编程式入口 index.js、几个 manifest(package.json / server.json / index.d.ts)——加起来不到 100 行。所有真活儿(命令怎么装、工具怎么注册、浏览器怎么开)都在依赖 playwright-core 打包好的 coreBundle 里。本章讲清这层「壳」如何把 monorepo 的核心接出来,以及为什么它可以这么薄。

本章讲什么:发布出去的 @playwright/mcp 包的表皮——它有哪几个入口、每个入口做什么、两种消费方式(命令行 vs 库),以及「薄」的设计意图。核心机制(工具注册、浏览器管理)不在本章,它们在 coreBundle 里;配置与能力开关见 02-config-and-capabilities.md,文档生成见 03-readme-generation-and-roll.md


1. 先建立直觉:这个包为什么这么薄

Playwright MCP 不是一个自包含的仓库。它发布的 npm 包 @playwright/mcp,真正的实现代码被打包进了它的依赖 playwright-core 里,以两个「bundle」的形式暴露:

  • playwright-core/lib/coreBundle —— MCP 命令装配 + 工具注册 + createConnection
  • playwright-core/lib/utilsBundle —— 通用工具,这里用到的是命令行解析器 program(commander 风格)。

所以这个仓库里你能看到的 cli.js / index.js,只是把 bundle 里的东西接出来、绑上本包版本号、再转发。一句话类比:

coreBundle 当成引擎和变速箱(在 monorepo 主仓 playwright 里造好、随 playwright-core 一起发布);这个仓库只是车壳 + 铭牌——负责把引擎装进一个叫 @playwright/mcp 的外壳里,钉死它配哪一版引擎,再把方向盘(命令行 / API)接出来。

这解释了后面所有细节:入口薄,是故意的——见 §6。


2. 顶层全景:一个包、两个入口、三份 manifest

@playwright/mcp (这个仓库 = 壳)
=============================

命令行用户 库/编程式用户
npx @playwright/mcp import { createConnection }
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ cli.js │ bin 入口 │ index.js │ exports "." 入口
│ (转发命令行) │ │ (导出一个函数) │
└───────┬───────┘ └───────┬───────┘
│ require │ require
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ playwright-core/lib/coreBundle (真正的核心) │
│ tools.decorateMCPCommand tools.createConnection │
│ libCli.decorateProgram ……工具注册 / 浏览器管理…… │
└──────────────────────────────────────────────────────────┘

三份 manifest 描述这个包:
package.json → bin / exports / mcpName / 把 playwright* 钉死在同一 alpha 版
index.d.ts → createConnection 的 TS 类型签名
server.json → 给 MCP registry 看的元数据(npm 包名 + stdio transport)

各文件一句话职责:

文件干什么面向谁
cli.js命令行入口,解析 argv 并转发给 coreBundlenpx @playwright/mcp 的用户
index.js编程式入口,只导出 createConnectionimport 这个库的代码
index.d.tscreateConnection 的 TypeScript 类型声明TS 用户 / 编辑器
package.json包元数据:binexports、版本钉死npm / Node 解析器
server.jsonMCP registry 元数据MCP 服务器目录

3. 命令行入口:cli.js 逐行走读

cli.jspackage.jsonbin 指向的可执行文件(见 §5),npx @playwright/mcp 最终跑的就是它。全文 32 行,可执行、#!/usr/bin/env node 开头。核心逻辑从第 18 行起。

3.1 先拉两个 bundle

cli.js:18-19playwright-core 里把两样东西 require 进来:

  • cli.js:18 const { program } = require('playwright-core/lib/utilsBundle') —— 命令行解析器实例(commander 风格,提供 .version() / .name() / .parseAsync())。
  • cli.js:19 const { tools, libCli } = require('playwright-core/lib/coreBundle') —— 核心:tools 负责挂 MCP 命令,libCli 负责浏览器安装那条路径。

注意:这里 require 的是依赖包内的路径,不是本仓库的文件。真代码不在这个克隆里,在 node_modules/playwright-core 里。

3.2 两条分支:装浏览器 vs 跑 MCP

cli.js 有两条互斥的路径,靠检查 argv 里有没有 install-browser 来分流。

分支 A —— 安装浏览器(cli.js:21-26):

// cli.js:21-26 — 真实源码,已核对行号
if (process.argv.includes('install-browser')) {
const argv = process.argv.map(arg => arg === 'install-browser' ? 'install' : arg);
libCli.decorateProgram(program);
void program.parseAsync(argv);
return;
}

这段在干嘛:如果用户敲了 install-browser 子命令,就把 argv 里的 install-browser 改写成 install(cli.js:22),然后走 Playwright 自己的 CLI(libCli.decorateProgram,cli.js:23)去解析——因为「装浏览器」本质上是 Playwright core 早就有的 playwright install 能力,这里只是借壳复用它。改写完就 return,不再往下走 MCP 那条路。

为什么要改名?因为对外这个包叫 playwright-mcp,给用户暴露的子命令名是 install-browser(语义清楚:装浏览器);但底层 libCli 认的是 Playwright 通用的 install。这一行 map 就是对外语义底层命令名的翻译。

分支 B —— 正常跑 MCP 服务器(cli.js:28-32):

// cli.js:28-32 — 真实源码,已核对行号
const packageJSON = require('./package.json');
const p = program.version('Version ' + packageJSON.version).name('Playwright MCP');
tools.decorateMCPCommand(p, packageJSON.version);

void program.parseAsync(process.argv);

这段在干嘛:读本包package.json(cli.js:28,唯一一处引用本仓库文件)拿版本号,把它挂到 program.version() / .name() 上(cli.js:29),然后 tools.decorateMCPCommand(p, version)(cli.js:30)——这一行才是把所有 MCP 命令、参数、工具注册装配上去的地方,但装配逻辑全在 coreBundle 内部,本仓库看不到。最后 parseAsync(process.argv)(cli.js:32)真正解析并执行。

记住这一点:cli.js 自己没有任何命令定义、没有任何参数声明。 命令面(--port--headless--caps 等)全部由 tools.decorateMCPCommand 在 coreBundle 里挂上。本仓库能看到的只是「调用点」。配置项细节见 02-config-and-capabilities.md

3.3 命令行的两种运行形态(stdio / SSE)

cli.js 跑起来后,MCP 服务器默认用 stdio transport(标准输入输出管道,MCP 客户端把它当子进程拉起、用管道对话)。这也是 server.json 声明的默认(见 §5)。

如果加 --port,则切到 HTTP transport(README 里称 SSE / streamable):

# README.md:749 —— 标准 HTTP 独立服务器
npx @playwright/mcp@latest --port 8931

README「Standalone MCP server」段(README.md:743-749)说明:在无显示器 / IDE worker 进程等场景下,用带 DISPLAY 的环境跑,并用 --port 开 HTTP transport;客户端再把配置里的 url 指向该 HTTP 端点。--port 参数本身也是 coreBundle 挂的(README.md:417 列出 --port <port>,对应环境变量 PLAYWRIGHT_MCP_PORT)。


4. 编程式入口:index.js 与 createConnection

除了命令行,这个包也能当库用。编程式入口是 index.js,全文有效代码就两行。

4.1 index.js:只导出一个函数

// index.js:18-19 — 真实源码,已核对行号
const { tools } = require('playwright-core/lib/coreBundle');
module.exports = { createConnection: tools.createConnection };

index.js:18 拉出 coreBundle 的 tools,index.js:19tools.createConnection 原样 re-export 成本包的 createConnection没有任何包装、没有任何逻辑——就是把 coreBundle 里的那个函数换个门牌号露出来。

4.2 index.d.ts:类型签名

index.d.ts 给这个 re-export 补上 TypeScript 类型(index.d.ts:22):

// index.d.ts:18-22 — 真实源码,已核对行号
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { Config } from './config';
import type { BrowserContext } from 'playwright';

export declare function createConnection(
config?: Config,
contextGetter?: () => Promise<BrowserContext>
): Promise<Server>;

逐项读这个签名:

部分类型含义
参数 config?Config(来自 ./config)可选配置对象;Config 类型见 02-config-and-capabilities.md
参数 contextGetter?() => Promise<BrowserContext>可选;自带一个 Playwright BrowserContext 工厂,让调用方接管浏览器上下文的创建
返回Promise<Server>Server 来自 @modelcontextprotocol/sdk —— 即 MCP SDK 官方的服务器类

关键点:返回的 ServerMCP SDK 的类型(@modelcontextprotocol/sdk/server/index.js),不是本项目自定义的。这意味着拿到 connection 后,你用的是标准 MCP SDK 的 API(比如 connection.connect(transport))。

4.3 README 的「Programmatic usage」示例

README 给了最小编程式用例(README.md:807-823),把上面的签名落成能跑的代码:

// README.md:807-823 —— 编程式用法,SSE transport
import http from 'http';
import { createConnection } from '@playwright/mcp';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';

http.createServer(async (req, res) => {
// Creates a headless Playwright MCP server with SSE transport
const connection = await createConnection({ browser: { launchOptions: { headless: true } } });
const transport = new SSEServerTransport('/messages', res);
await connection.connect(transport);
});

这段演示的三步套路:

  1. createConnection(config)(README.md:817)—— 传一个 config(这里只设了 headless),拿到 connection(即那个 MCP Server)。
  2. new SSEServerTransport('/messages', res)(README.md:818)—— 用 MCP SDK 自己的 transport,而不是本包的东西。
  3. connection.connect(transport)(README.md:819)—— 把 server 接到 transport 上。

重点看: transport(SSEServerTransport)来自 @modelcontextprotocol/sdk,不是本包提供的。本包只提供「一个配好了工具的 MCP Server」;transport 由调用方自选自装。这正是「壳很薄」的又一体现——连传输层都交给 MCP SDK。

4.4 命令行 vs 编程式:一张对照

维度命令行(cli.js)编程式(index.js)
入口bin: playwright-mcpcli.jsexports "."index.js
暴露的东西一套命令(decorateMCPCommand 挂)一个函数 createConnection
transport默认 stdio;--port 开 HTTP/SSE调用方自己 new(如 SSEServerTransport)
谁装配工具tools.decorateMCPCommand(coreBundle)tools.createConnection(coreBundle)
典型用法npx @playwright/mcp --port 8931await createConnection(config)

两条路殊途同归:都只是 coreBundle 里 tools.* 的一个入口。


5. 三份 manifest:bin / exports / registry

三个 JSON/TS 文件把「这是个什么包」讲清楚。

5.1 package.json 的关键字段

package.json 里跟入口/包壳直接相关的四组字段:

bin(package.json:48-50) —— 把命令名映射到可执行文件:

"bin": { "playwright-mcp": "cli.js" }

即安装后命令 playwright-mcp 就是跑 cli.js

exports(package.json:32-38) —— 定义 import/require 时的入口解析:

"exports": {
"./package.json": "./package.json",
".": { "types": "./index.d.ts", "default": "./index.js" }
}

import ... from '@playwright/mcp'index.js,类型走 index.d.ts;并额外允许 import '@playwright/mcp/package.json'

mcpName(package.json:17) —— MCP 生态里的规范名:

"mcpName": "io.github.microsoft/playwright-mcp"

这是反向域名风格的 MCP 服务器标识,和 server.json 里的 name 一致(见 §5.2)。

版本钉死(package.json:39-46) —— 把 playwright / playwright-core / @playwright/test 全部钉在同一个 alpha 版本 1.62.0-alpha-2026-06-29:

依赖位置版本
playwrightpackage.json:40(dependencies)1.62.0-alpha-2026-06-29
playwright-corepackage.json:41(dependencies)1.62.0-alpha-2026-06-29
@playwright/testpackage.json:45(devDependencies)1.62.0-alpha-2026-06-29

注意这里没有 ^ / ~,是精确钉死(exact pin)。package-lock.json:12-13package-lock.json:20 也锁到同一版。这就是本章开头说的「壳负责版本绑定」:因为真正的实现来自 playwright-corecoreBundle,壳必须精确锁定它配哪一版引擎,否则命令面/工具面会对不上。这个版本号如何被批量滚动升级,见 03-readme-generation-and-roll.mdroll.js 的讲解。

另外 @modelcontextprotocol/sdkdevDependencies(package.json:44,^1.25.2)——因为运行时用到的 MCP SDK 已经被打进 playwright-core 的 coreBundle 了,本包只在开发/类型阶段需要它。这也是「薄」的一个侧证。

5.2 server.json:给 MCP registry 的名片

server.json 是给 MCP 服务器目录(registry)看的元数据,遵循官方 schema(server.json:2)。它把包身份和消费方式讲给注册中心:

字段含义
name(server.json:3)io.github.microsoft/playwright-mcpregistry 里的规范名,与 package.jsonmcpName 一致
version(server.json:9)0.0.77package.json:3 的包版本一致
packages[].registryType(server.json:12)npm从 npm 安装
packages[].identifier(server.json:13)@playwright/mcpnpm 包名
packages[].transport.type(server.json:15-17)stdio声明默认 transport 是 stdio

也就是说,registry 里登记的是「用 npm 装 @playwright/mcp、以 stdio 方式对话」——正好对应 §3.3 说的命令行默认形态。


6. 为什么入口这么薄(设计意图)

把前面的观察汇总,「薄」不是偷懒,是一种刻意的架构分工:

  • 真正的命令装配、工具注册、浏览器管理全在 coreBundle cli.js 只有一处 tools.decorateMCPCommand(...)(cli.js:30)是「装命令」的调用点;index.js 只有一处 tools.createConnection(index.js:19)是「建连接」的调用点。两者都不含任何业务逻辑。

  • 壳只负责两件事:版本绑定 + 转发。

    • 版本绑定:package.json:40-45playwright* 精确钉在同一 alpha,保证壳配的是它期望的那一版引擎。
    • 转发:cli.js 把 argv 转给 program.parseAsync;index.jscreateConnection 原样露出。
  • 好处是「文档即真相」和「版本可滚动」得以成立。 因为命令/工具都在 coreBundle,升级只需把版本号一滚(roll.js)、README 一重生成(update-readme.js),壳几乎不用改。这两个机制见 03-readme-generation-and-roll.md

  • 代价:这个克隆里读不到核心实现。 decorateMCPCommand / createConnection 的真身在 node_modules/playwright-core/lib/coreBundle(打包产物),不在本仓库源码里。想读工具怎么注册,得去主 monorepo playwright 找源。本章能给的是「壳如何接出核心」,给不了核心本身。


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

主题文件路径符号 / 关键行
命令行入口 · require bundlecli.js:18-19program(utilsBundle)、tools / libCli(coreBundle)
install-browser 分支 · 改名转发cli.js:21-26install-browserinstalllibCli.decorateProgram
MCP 命令装配调用点cli.js:28-32tools.decorateMCPCommand(p, version)program.parseAsync
编程式入口 · re-exportindex.js:18-19createConnection: tools.createConnection
createConnection 类型签名index.d.ts:18-22createConnection(config?, contextGetter?) => Promise<Server>
MCP SDK Server 类型来源index.d.ts:18import type { Server } from '@modelcontextprotocol/sdk'
bin 映射package.json:48-50"playwright-mcp": "cli.js"
exports 入口package.json:32-38"." → index.js / index.d.ts
MCP 规范名package.json:17mcpName: io.github.microsoft/playwright-mcp
版本钉死(alpha)package.json:40-45playwright* = 1.62.0-alpha-2026-06-29
registry 元数据 · stdioserver.json:3,13,15-17nameidentifier @playwright/mcptransport stdio
编程式用法示例 · SSEREADME.md:807-823createConnection + SSEServerTransport
命令行 HTTP transportREADME.md:743-749--port 8931

下一步该读哪: 想知道 createConnection 那个 config 参数和 --caps 能力开关的形状 → 02-config-and-capabilities.md;想知道版本怎么滚、README 怎么自动生成 → 03-readme-generation-and-roll.md;全景与阅读地图见 index.md