跳到主要内容

数据截至 (上游 commit fd0b7e1d9ed9)

第 7 章:CLI、daemon 与代码生成

这章讲什么: 同一份工具定义怎么变成三样东西(MCP 工具、CLI 命令、参考文档),以及命令行下"浏览器状态在多条命令之间保持"是怎么做到的。


7.1 一份定义,三处使用

全景

src/tools/*.ts 里的 zod schema
│ 唯一真相
┌───────────────┼───────────────┐
▼ ▼ ▼
MCP 工具 CLI 命令表 参考文档
(运行时注册) chrome-devtools- docs/tool-reference.md
cli-options.ts
(生成物,勿手改) (生成物)
▲ ▲ ▲
│ │ │
直接用 scripts/generate- scripts/generate-
cli.ts docs.ts

生成器的做法有点意外

scripts/generate-cli.ts 不是去静态分析源码,而是真的把 server 跑起来:

const transport = new StdioClientTransport({
command: 'node',
args: [serverPath, '--viaCli'],
env: {...process.env, CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
});
const client = new Client({name: 'chrome-devtools-cli-generator',});
await client.connect(transport);
const toolsResponse = await client.listTools();

用 MCP 客户端去问一个 MCP 服务端要工具表。 好处很实在:拿到的是 zod 经过 SDK 转换后的最终 JSON Schema,和模型看到的一模一样,不会因为静态分析漏掉 .optional().default() 或 transform。

注意它带了 --viaCli——这样被类别开关禁用的工具也会注册(见第 1 章),命令表才完整。

然后 schemaToCLIOptions 把 JSON Schema 摊平成 CLI 选项。这里有一条硬约束:

if (typeof prop.type !== 'string') {
throw new Error(`Property ${name} has a complex type not supported by CLI.`);
}

参数类型太复杂就直接让生成失败。 这也解释了为什么 fill_formelements(对象数组)在 CLI 里用不了。

生成物顶部写着:// NOTE: do not edit manually. Auto-generated by 'npm run cli:generate'.


7.2 CLI 的参数约定

规则

必填参数 → 位置参数(不带 --)
可选参数 → 选项(带 --)

于是:

chrome-devtools new_page "https://example.com" # url 必填 → 位置
chrome-devtools take_screenshot --filePath shot.png # filePath 可选 → 选项
chrome-devtools click 1_12 --dblClick # uid 必填,dblClick 可选

命令字符串在 src/bin/chrome-devtools.ts:225-233 拼出来:必填拼成 <name>,可选拼成 [--name]

给 agent 的错误提示

yargs.fail()(src/bin/chrome-devtools.ts:91-126)在遇到"参数不够"或"未知参数"时,会打一段抬头是 💡 TIP FOR AI AGENT / DEVELOPER: 的说明:

1. Required parameters MUST be passed as positional arguments (without flags).
- INCORRECT: chrome-devtools evaluate_script --expression "() => document.title"
- CORRECT: chrome-devtools evaluate_script "() => document.title"
2. Optional parameters are passed as double-dash options/flags (e.g. --pageId 1).
3. Make sure to escape quotes properly for your shell environment.

错误信息的目标读者被明确写成了 AI agent。 正例反例并排给,是很有效的纠错形式。

CLI 改掉的几个默认值

getCliOptions(src/bin/chrome-devtools.ts:52-68)对选项表做了改造:

改动为什么
删掉 viewport尚未支持 CLI 序列化
删掉 experimentalStructuredContent / experimentalInteropTools / experimentalPageIdRoutingCLI 默认关掉实验项(注释原话「Change the defaults for the CLI」)
headless 默认true命令行场景通常不需要看窗口(:147-148)
isolated 默认true(未给 userDataDir 时)每次干净环境(:143-145)

固定追加的只有 --viaCli(DEFAULT_CLI_ARGS,:44);--output-format json 仍按命令提供(:228-231)。旧版「headless 必须有默认、isolated 必须没有默认」的启动期自检已删除,默认值改为在 start 命令里直接补(:143-148,注释明说「Defaults but we do not want to affect the yargs conflict resolution」)——既给了默认又不干扰 yargs 的冲突解析。


7.3 daemon:状态怎么在命令之间活下来

问题

chrome-devtools new_page "https://app.local/login"
chrome-devtools fill 1_4 "user@example.com"
chrome-devtools click 1_7

三条命令是三个进程。如果每条都自己开一台 Chrome,第二条就找不到第一条打开的页面了。

三层进程

① chrome-devtools <cmd> 每次新起,用完就退
│ unix socket / named pipe,PipeTransport 传 JSON

② daemon.js 常驻,持有 socket 服务端
│ 它自己是一个 MCP client(StdioClientTransport)

③ chrome-devtools-mcp.js 真正的 MCP server + Chrome

第 ② 层是个转接器:对上是自定义 socket 协议,对下是标准 MCP。 好处是 CLI 不需要重新实现工具执行,直接复用 server 的全部逻辑。

自动启动

// src/bin/chrome-devtools.ts:278-280
if (!isDaemonRunning(sessionId)) {
await start(serializeArgs(cliOptions, argv), sessionId);
}

第一条命令自动拉起 daemon,后续复用。chrome-devtools start/stop/status 是手动控制。

存活检测

isDaemonRunning(src/daemon/utils.ts)读 pid 文件,然后 process.kill(pid, 0)——信号 0 不发信号,只检查进程存在。抛异常说明是陈旧 pid 文件,继续走启动流程。

版本不一致会提示

status 命令和每次工具调用都会比对 daemon 版本与 CLI 版本(src/bin/chrome-devtools.ts:185-190:274-276),不一致就 warn:"Run 'chrome-devtools start' to update and restart the daemon."

升级了 npm 包但没重启后台进程,是个很容易踩的坑。


7.4 daemon 的安全细节

src/daemon/daemon.ts 开头一大段全是防御,值得逐条看。

pid 目录检查(仅 POSIX)

mkdirSync(pidDir, {mode: 0o700})

├─ statSync: uid 是当前用户吗? 否 → 报 "Possible tampering" 并退出
└─ mode 有 S_IWGRP 或 S_IWOTH? 是 → 报 "insecure permissions" 并退出

别人能写的目录里放 pid 文件是危险的——可以被替换成指向别处的符号链接。

pid 文件写入

openSync(pidFilePath,
O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0o600);

O_NOFOLLOW 让符号链接直接打开失败。和 McpContext.#writeFile(src/McpContext.ts:651)是同一套写法——同一个安全模式在项目里被贯彻了两次

sessionId 白名单

if (!/^[a-fA-F0-9-]+$/.test(sessionId)) throw new Error(`Invalid sessionId: ${sessionId}`);

assertValidSessionId每一个用到 sessionId 的函数里都被调一次(getSocketPathgetRuntimeHomegetPidFilePathgetDaemonPidisDaemonRunning)。因为 sessionId 会拼进文件路径,不校验就是路径穿越。

在每个入口重复校验而不是只在边界校验一次,是这里的取舍。

socket 路径的现实约束

getSocketPath(src/daemon/utils.ts:35)带了注释:"Using these paths due to strict limits on the POSIX socket path length."(约 104 字符)

Windows → \\.\pipe\chrome-devtools-mcp-<username>\server.sock
有 XDG_RUNTIME_DIR → $XDG_RUNTIME_DIR/chrome-devtools-mcp/server.sock
其他(含 macOS) → /tmp/chrome-devtools-mcp-<uid>.sock

macOS 不用 ~/Library/Application Support/ 就是因为太长。Windows 的路径里拼 username,注释写明是"prevent cross-user named pipe squatting"。

socket 监听权限

server.listen({path: socketPath, readableAll: false, writableAll: false});

不给其他用户读写。


7.5 输出:图片也要落地

handleResponse(src/daemon/client.ts:222)把 MCP 的 CallToolResult 转成终端能看的东西:

content 里的 text → 直接拼进输出
content 里的 image → base64 解码,按 mimeType 定扩展名,
写进临时文件,输出 "Saved to <path>."
其他类型 → 抛错

--output-format json 时,输出的是 structuredContent 加上一个 images 数组(路径 + mimeType)。

终端里没法显示图片,那就给路径。 和第 4 章的"大资产走文件"是同一条原则。


7.6 进程收尾

MCP server 侧

src/bin/chrome-devtools-mcp-main.ts:36-72 监听五个退出信号:stdin endstdin closeSIGTERMSIGINTSIGHUP

stdin 那两个是 stdio 传输的约定——客户端关掉管道就是要你退出。注释说明了不处理的后果:

Without this, an active Chrome subprocess keeps the Node event loop ref'd after stdin closes and the server hangs until something else kills it.

还有一个 5 秒兜底:

setTimeout(() => {
logger?.('Shutdown timeout exceeded, forcing exit');
process.exit(0);
}, 5000).unref();

.unref() 很关键——正常关闭路径上,这个定时器不该把进程留住。退出码用 0,注释解释:关闭请求毕竟是被响应了,异常情况靠日志留痕。

daemon 侧

cleanup()(src/daemon/daemon.ts:268)依次关 MCP client、关 transport、关 socket 服务、删 socket 文件、删 pid 文件。除了信号,还挂了 uncaughtExceptionunhandledRejection——崩溃也要清干净 pid 文件,否则下次启动会以为还有个 daemon 在跑。


7.7 代码地图

主题文件路径符号名
CLI 主程序src/bin/chrome-devtools.tsstartdefaultArgsstartCliOptions.fail()
生成的命令表src/bin/chrome-devtools-cli-options.tscommandsArgDefCommands
CLI 生成器scripts/generate-cli.tsfetchToolsschemaToCLIOptionsgenerateCli
文档生成器scripts/generate-docs.ts
daemon 主体src/daemon/daemon.tssetupMCPClienthandleRequeststartSocketServercleanup
daemon 客户端src/daemon/client.tsstartDaemonstopDaemonsendCommandhandleResponseverifyDaemonVersion
路径与存活检测src/daemon/utils.tsgetSocketPathgetPidFilePathisDaemonRunningassertValidSessionIdserializeArgs
server 参数表src/bin/chrome-devtools-mcp-cli-options.tscliOptionsparseArguments
server 入口与收尾src/bin/chrome-devtools-mcp-main.tsshutdowncheckForUpdates