深入解析 Google 开源的 chrome-devtools-mcp 项目,探索 AI 与浏览器自动化的完美结合
大纲
- 一、引言:AI 时代的浏览器自动化新范式
- 二、核心架构:分层设计的智慧
- 三、核心技术实现深度剖析
- 四、26 个工具的功能解析
- 五、技术创新点与特色功能
- 六、代码质量与工程化实践
- 七、性能优化措施
- 八、使用场景与最佳实践
- 九、技术挑战与解决方案
- 十、项目的不足与改进空间
- 十一、技术栈选型分析
- 十二、学习与借鉴
- 十三、实战演练:从零构建一个简单的 MCP 工具
本文主要介绍第一~第四部分的内容,让我们开始吧。
一、引言:AI 时代的浏览器自动化新范式
1.1 传统浏览器自动化的痛点
在过去的十多年里,浏览器自动化技术已经成为 Web 开发和测试领域不可或缺的一部分。从 Selenium 到 Puppeteer,从 Playwright 到 WebDriver,我们见证了这个领域的不断演进。然而,传统的浏览器自动化方式始终存在着一些根深蒂固的痛点:
脚本编写门槛高:即使是使用 Puppeteer 这样现代化的工具,开发者仍需要对浏览器 API、DOM 操作、异步编程有深入的理解。一个看似简单的"登录并提交表单"的测试场景,可能需要编写几十行代码,包括元素选择器的定义、等待条件的设置、错误处理的逻辑等。这对于非专业的测试人员或产品经理来说,几乎是不可能完成的任务。
调试困难,需要反复运行:当自动化脚本出现问题时,调试过程往往非常痛苦。你需要:
- 运行脚本,等待失败
- 查看错误日志或截图
- 猜测可能的原因
- 修改代码
- 再次运行,继续等待
这个循环可能重复数十次,尤其是在处理复杂的异步交互或动态加载内容时。更糟糕的是,由于网络波动或页面加载时序的不确定性,脚本可能会出现"偶尔失败"的情况,让调试变得更加复杂。
性能分析依赖人工经验:虽然 Chrome DevTools 提供了强大的性能分析工具,但解读这些数据仍然需要丰富的经验。面对一份包含数千个事件的 Trace 文件,普通开发者往往不知道从何入手:
- 哪些指标是关键的?
- LCP(最大内容绘制)为什么这么慢?
- 这个长任务是由什么代码引起的?
- 如何优化才能获得最大的性能提升?
这些问题通常需要资深的性能工程师才能回答,而这样的专家在大多数团队中都是稀缺资源。
无法利用 AI 的理解能力:在 2023 年以前,浏览器自动化工具基本上都是"硬编码"的模式。你必须精确地告诉程序"点击坐标 (100, 200)"或"找到 class 为 'submit-button' 的按钮"。但人类在使用浏览器时并不是这样工作的——我们会说"点击那个蓝色的提交按钮"或"填写第二个表单"。随着 GPT-4、Claude 等大语言模型的出现,AI 已经具备了理解这种自然语言指令的能力,但传统的自动化工具却无法与这些 AI 模型有效地连接起来。
这些痛点的根本原因在于:传统浏览器自动化工具是为程序员设计的,而不是为 AI 或非技术用户设计的。它们的 API 假设用户已经知道要做什么、如何做,只是需要一个执行工具。但在 AI 时代,我们需要的是一种新的范式——让 AI 能够像人类一样理解任务,然后自主决定如何操作浏览器来完成任务。
1.2 MCP 协议:连接 AI 与工具的桥梁
Model Context Protocol 简介
就在这样的背景下,Anthropic 公司于 2024 年推出了 Model Context Protocol(MCP)。这是一个开放标准,旨在解决一个核心问题:如何让大语言模型(LLM)能够安全、高效地访问和使用外部工具?
在 MCP 之前,如果你想让 AI 访问数据库、文件系统或浏览器,通常有两种做法:
- 函数调用(Function Calling):在每次对话时,将所有可用的工具描述传递给 LLM,让它选择调用哪个工具。这种方式的问题是,每次都要传递工具描述会消耗大量的 token,而且工具之间的状态管理非常困难。
- 定制化集成:为每个 AI 应用单独开发工具集成方案。这种方式的问题是开发成本高、维护困难,而且不同的 AI 应用之间无法复用。
MCP 提供了第三种方案:标准化的客户端-服务器架构。在这个架构中:
- MCP Server:封装特定的工具能力(如浏览器控制、数据库访问、文件操作),通过标准化的接口暴露给 AI
- MCP Client:集成在 AI 应用中(如 Claude Desktop、Cursor IDE),负责发现和调用 MCP Server
- MCP Protocol:定义了 Client 和 Server 之间的通信规范,包括工具发现、参数验证、调用执行、结果返回等
这种架构的优势是显而易见的:
- 工具开发者只需要实现一次 MCP Server,就可以被所有支持 MCP 的 AI 应用使用
- AI 应用开发者不需要为每个工具单独开发集成方案,只需要实现 MCP Client
- 最终用户可以自由组合不同的 MCP Server,构建个性化的 AI 工作流
MCP 的设计理念与架构
MCP 的设计遵循几个核心理念:
-
协议优于实现:MCP 只定义通信协议,不限制具体的实现方式。你可以用 TypeScript、Python、Rust 或任何其他语言实现 MCP Server。
-
简单至上:MCP 的通信机制默认使用 stdio(标准输入/输出),这是最简单、最可靠的进程间通信方式。不需要配置网络端口、不需要处理认证,只需要启动一个子进程。
-
类型安全:MCP 使用 JSON Schema 定义工具的参数和返回值,确保类型安全和自动验证。
-
可扩展性:MCP 支持三种类型的能力暴露:
- Tools:AI 可以主动调用的函数
- Resources:AI 可以读取的数据源(如文件、数据库记录)
- Prompts:预定义的提示词模板
MCP 的架构可以用一个简单的图来表示:
┌─────────────────────────────────────────┐
│ AI 应用 (MCP Client) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Claude │ │ Cursor │ │ 其他 IDE │ │
│ │ Desktop │ │ IDE │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────┬───────────────────────────┘
│ MCP Protocol (JSON-RPC over stdio/HTTP)
┌─────────────┴───────────────────────────┐
│ MCP Servers (工具层) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Browser │ │ Database │ │ File │ │
│ │ Control │ │ Access │ │ System │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────┘
为什么选择 MCP 而非传统 API
你可能会问:MCP 和传统的 REST API 或 gRPC 有什么本质区别?为什么不直接让 AI 调用 HTTP API?
这个问题的答案在于 设计目标的不同:
-
传统 API 是为人类开发者设计的:
- API 文档需要人类阅读和理解
- 参数命名和结构是为人类直觉优化的
- 错误处理依赖人类的判断和重试逻辑
-
MCP 是为 AI 设计的:
- 工具描述(JSON Schema)是机器可读的,AI 可以自动理解工具的能力和限制
- 返回的数据格式经过优化,便于 AI 理解和处理(如使用 Markdown 格式的可访问性树,而不是复杂的 HTML DOM)
- 内置了状态管理机制,AI 可以在多个工具调用之间保持上下文
此外,MCP 的 stdio 通信方式还带来了额外的好处:
- 安全性:MCP Server 运行在本地,不需要暴露网络端口,减少了攻击面
- 简单性:不需要配置认证、CORS、TLS 等复杂的网络安全机制
- 可移植性:stdio 是跨平台的标准,Windows、macOS、Linux 都原生支持
1.3 chrome-devtools-mcp 项目概述
项目定位与核心价值
chrome-devtools-mcp 是 Google 官方开源的一个 MCP Server 实现,它的定位非常明确:让 AI 编码助手能够像人类开发者一样使用 Chrome DevTools,进行浏览器自动化、性能分析和调试。
这个项目的核心价值在于:
-
降低浏览器自动化的门槛:
- 用户不需要编写 Puppeteer 或 Playwright 脚本,只需要用自然语言告诉 AI:"打开这个网页,点击登录按钮,然后截图"
- AI 会自动选择合适的 MCP 工具(navigate_page、click、take_screenshot),处理等待时机、元素定位等细节
-
让性能分析变得智能:
- 传统方式:手动启动 Performance 录制 → 操作页面 → 停止录制 → 分析 Trace 文件 → 寻找瓶颈
- MCP 方式:告诉 AI "分析这个页面的加载性能",AI 会自动完成录制、分析,并用人类语言解释发现的问题
-
实现跨工具的工作流自动化:
- 可以将浏览器操作与其他 MCP Server(如文件系统、数据库、Git)组合起来
- 例如:"运行性能测试,将结果截图保存到 test-results 文件夹,并创建一个 GitHub Issue 记录发现的性能问题"
由 Google 团队开发维护
chrome-devtools-mcp 项目由 Google Chrome DevTools 团队的成员开发和维护,这意味着:
- 技术权威性:项目深度集成了 Chrome DevTools 的内部能力,例如直接复用了 DevTools Frontend 中的 TraceEngine 进行性能分析,这是其他第三方工具无法做到的
- 持续更新:随着 Chrome 和 DevTools 的更新,项目会及时跟进最新的 API 和最佳实践
- 社区支持:作为 Google 官方项目,它有更好的社区活跃度和文档支持
26 个工具,6 大功能类别
chrome-devtools-mcp 提供了 26 个 MCP 工具,覆盖了浏览器自动化的各个方面:
-
输入自动化(8 个工具):
click- 点击元素fill/fill_form- 表单填充drag- 拖拽操作hover- 悬停press_key- 键盘输入upload_file- 文件上传handle_dialog- 对话框处理
-
导航自动化(6 个工具):
navigate_page- 页面导航new_page/close_page- 页面生命周期管理list_pages/select_page- 多页面管理wait_for- 条件等待
-
性能分析(3 个工具):
performance_start_trace- 开始性能追踪performance_stop_trace- 停止追踪并分析performance_analyze_insight- 深度性能洞察
-
网络调试(2 个工具):
list_network_requests- 列出网络请求get_network_request- 获取请求详情
-
调试工具(5 个工具):
take_snapshot- 页面结构快照take_screenshot- 截图evaluate_script- 执行 JavaScriptlist_console_messages/get_console_message- 控制台消息
-
模拟(2 个工具):
emulate- 网络和 CPU 节流模拟resize_page- 视口大小调整
这 26 个工具涵盖了从基础交互到高级性能分析的完整能力,使得 AI 能够完成几乎所有人类开发者在 Chrome DevTools 中能做的事情。
Apache 2.0 许可,商业友好
项目采用 Apache 2.0 开源许可证,这是一个非常宽松、商业友好的许可证:
- 允许商业使用
- 允许修改和再分发
- 不要求修改后的代码开源(与 GPL 不同)
- 提供了专利授权保护
这意味着你可以:
- 在公司项目中自由使用
- 基于此项目开发商业产品
- 修改代码以适应特定需求
- 集成到闭源软件中
Google 选择 Apache 2.0 许可证,体现了他们希望这个项目能够被广泛采用的愿景。
二、核心架构:分层设计的智慧
2.1 整体架构设计
MCP Client-Server 架构模式
chrome-devtools-mcp 完全遵循 MCP 协议的标准架构,采用典型的客户端-服务器模式:
┌─────────────────────────────────────────────────────────────┐
│ AI 编码助手 │
│ (MCP Client) │
│ ┌────────────┐ ┌────────────┐ ┌─────────────┐ │
│ │ Claude │ │ Cursor IDE │ │ Continue │ │
│ │ Desktop │ │ │ │ │ │
│ └────────────┘ └────────────┘ └─────────────┘ │
└─────────────────────┬───────────────────────────────────────┘
│
│ MCP Protocol
│ (JSON-RPC 2.0 over stdio)
│
┌─────────────────────┴───────────────────────────────────────┐
│ chrome-devtools-mcp Server │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 工具层 (Tools Layer) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │click │ │fill │ │trace │ │shot │ │... │ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 响应层 (Response Layer) │ │
│ │ McpResponse (构建器模式) │ │
│ │ - 文本格式化 - Markdown 生成 - 分页逻辑 │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 上下文层 (Context Layer) │ │
│ │ McpContext (状态管理器) │ │
│ │ - 页面集合 - 快照系统 - 事件收集器 │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 浏览器层 (Browser Layer) │ │
│ │ Puppeteer │ │
│ │ - 连接管理 - CDP 通信 - 资源控制 │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
└─────────────────────────┬───────────────────────────────────┘
│
│ Chrome DevTools Protocol
│
┌─────────────────────────┴───────────────────────────────────┐
│ Chrome 浏览器 │
│ - 渲染引擎 (Blink) │
│ - JavaScript 引擎 (V8) │
│ - DevTools 调试接口 │
└─────────────────────────────────────────────────────────────┘
这个架构的精妙之处在于:
- 上层不依赖下层的具体实现:工具层不需要知道响应是如何构建的,响应层不需要知道浏览器是如何连接的
- 每一层都有明确的职责:工具层负责业务逻辑,响应层负责格式化,上下文层负责状态管理,浏览器层负责底层通信
- 易于测试和维护:每一层都可以独立测试,修改一层不会影响其他层
stdio 通信机制的选择
chrome-devtools-mcp 选择使用 stdio(标准输入/输出)作为与 MCP Client 的通信方式,而不是更常见的 HTTP 或 WebSocket。这个选择背后有深刻的考虑:
技术层面的优势:
- 零配置:不需要分配端口、不需要处理端口冲突、不需要配置防火墙
- 自动生命周期管理:当父进程(MCP Client)退出时,子进程(MCP Server)会自动被操作系统清理,不会留下僵尸进程
- 简单可靠:stdio 是最成熟的 IPC(进程间通信)机制,跨平台支持完善,不会出现网络相关的各种边界情况
安全层面的优势:
- 不暴露网络端口:减少了攻击面,不会被外部网络访问
- 天然的权限隔离:子进程继承父进程的权限,不需要额外的认证机制
- 审计友好:所有的输入输出都可以被记录,便于调试和安全审计
实现方式:
// MCP Server 通过 stdio 接收请求
process.stdin.pipe(/* 解析 JSON-RPC 请求 */);
// 通过 stdout 返回响应
process.stdout.write(
JSON.stringify({
jsonrpc: '2.0',
id: requestId,
result: {
/* 工具执行结果 */
}
})
);
// 错误和日志通过 stderr 输出
process.stderr.write('Debug info...\n');
四层架构详解
让我们深入了解每一层的职责和设计:
工具层(Tools Layer)
这是最上层,直接面向 AI 的抽象层。每个工具都是一个独立的功能单元,通过统一的 defineTool 函数定义:
// 简化的工具定义示例
export const clickTool = defineTool({
name: 'click',
description: 'Click an element on the page',
schema: z.object({
ref: z.string().describe('Element reference from snapshot'),
button: z.enum(['left', 'right', 'middle']).optional(),
clickCount: z.number().optional()
}),
handler: async (args, context) => {
// 1. 获取当前页面
const page = context.getCurrentPage();
// 2. 根据 ref 定位元素
const element = await context.getElementByRef(args.ref);
// 3. 执行点击操作
await element.click({
button: args.button || 'left',
clickCount: args.clickCount || 1
});
// 4. 等待页面稳定
await context.waitForStability();
// 5. 构建响应
return context.response().addText(`Clicked element ${args.ref}`).withSnapshot().withNetworkRequests();
}
});
工具层的关键设计:
- 声明式 Schema:使用 Zod 定义参数,AI 可以自动理解参数的类型、约束和描述
- 统一的错误处理:所有工具的错误都会被框架捕获并格式化成标准的 MCP 错误响应
- 自动文档生成:Schema 中的描述会自动转换为工具文档,AI 可以读取并理解
响应层(Response Layer)
响应层负责将工具的执行结果格式化成 AI 友好的形式。核心是 McpResponse 类:
class McpResponse {
private texts: string[] = [];
private includeSnapshot = false;
private includeNetworkRequests = false;
addText(text: string): this {
this.texts.push(text);
return this; // 链式调用
}
withSnapshot(): this {
this.includeSnapshot = true;
return this;
}
withNetworkRequests(): this {
this.includeNetworkRequests = true;
return this;
}
async build(context: McpContext) {
let result = this.texts.join('\n\n');
// 延迟计算:只有在 build 时才生成快照和网络请求数据
if (this.includeSnapshot) {
const snapshot = await context.generateSnapshot();
result += '\n\n## Page Snapshot\n' + snapshot;
}
if (this.includeNetworkRequests) {
const requests = context.getNetworkRequests();
result += '\n\n## Network Requests\n' + formatRequests(requests);
}
return result;
}
}
响应层的关键设计:
- 构建器模式:通过链式调用构建响应,代码清晰易读
- 延迟计算:快照和网络请求数据只有在需要时才生成,避免不必要的性能开销
- Markdown 格式:所有的输出都使用 Markdown 格式,便于 AI 理解和用户阅读
上下文层(Context Layer)
上下文层是整个架构的核心,负责管理所有的状态和资源。McpContext 类是一个复杂的状态管理器:
class McpContext {
private browser: Browser;
private pages: Map<string, Page> = new Map();
private currentPageId: string;
private snapshotCounter = 0;
private networkCollector: NetworkCollector;
private consoleCollector: ConsoleCollector;
private mutex: Mutex; // 互斥锁,保证工具调用的原子性
// 页面管理
getCurrentPage(): Page {
/* ... */
}
setCurrentPage(pageId: string) {
/* ... */
}
// 快照系统
async generateSnapshot(): Promise<string> {
const page = this.getCurrentPage();
const tree = await page.accessibility.snapshot();
return this.formatAccessibilityTree(tree, ++this.snapshotCounter);
}
// 事件收集
collectNetworkRequest(request: NetworkRequest) {
/* ... */
}
collectConsoleMessage(message: ConsoleMessage) {
/* ... */
}
// 响应构建
response(): McpResponse {
return new McpResponse(this);
}
}
上下文层的关键设计:
- 单例模式:整个 MCP Server 只有一个 McpContext 实例,确保状态一致性
- 收集器模式:使用专门的 Collector 类管理网络请求和控制台消息,支持跨导航保留历史数据
- 智能超时管理:根据当前的 CPU 和网络节流设置,自动调整等待超时时间
浏览器层(Browser Layer)
浏览器层封装了与 Chrome 浏览器的所有交互,基于 Puppeteer 实现:
async function initializeBrowser(options: BrowserOptions): Promise<Browser> {
// 两种连接模式
if (options.remoteDebuggingUrl) {
// 模式 1: 连接到已运行的浏览器
return await puppeteer.connect({
browserURL: options.remoteDebuggingUrl,
transport: createCustomTransport(options.headers)
});
} else {
// 模式 2: 启动新的浏览器实例
return await puppeteer.launch({
channel: options.channel || 'chrome',
headless: options.headless ?? false,
userDataDir: options.userDataDir,
args: ['--no-first-run', '--no-default-browser-check', ...options.extraArgs]
});
}
}
浏览器层的关键设计:
- 灵活的连接模式:支持启动新浏览器或连接现有浏览器,适应不同的使用场景
- 目标过滤:可以过滤特定类型的浏览上下文(如只连接到普通页面,不连接 DevTools 窗口)
- 用户数据目录管理:支持持久化用户数据(如 Cookies、LocalStorage),加速重复测试
2.2 关键设计模式
chrome-devtools-mcp 项目中应用了多个经典的设计模式,让我们逐一分析:
工具定义模式(Tool Definition Pattern)
这是一个自定义的模式,用于统一定义所有的 MCP 工具:
// 工具定义的类型签名
interface ToolDefinition<TSchema extends z.ZodType> {
name: string;
description: string;
schema: TSchema;
handler: (args: z.infer<TSchema>, context: McpContext) => Promise<McpResponse>;
}
// 使用工厂函数创建工具
function defineTool<TSchema extends z.ZodType>(definition: ToolDefinition<TSchema>): MCPTool {
return {
name: definition.name,
description: definition.description,
inputSchema: zodToJsonSchema(definition.schema), // 转换为 JSON Schema
handler: async (rawArgs: unknown) => {
// 1. 验证参数
const args = definition.schema.parse(rawArgs);
// 2. 获取互斥锁
await mutex.lock();
try {
// 3. 执行工具逻辑
const response = await definition.handler(args, context);
// 4. 构建最终响应
return await response.build(context);
} finally {
// 5. 释放互斥锁
mutex.unlock();
}
}
};
}
这个模式的优势:
- 类型安全:TypeScript 可以推断出 handler 中 args 的类型
- 统一的验证:所有工具的参数都会自动验证
- 统一的错误处理:框架层统一处理所有错误
- 可测试性:每个工具的 handler 是一个纯函数,易于单元测试
收集器模式(Collector Pattern)
用于收集和管理跨导航保留的数据(如网络请求、控制台消息):
class PageCollector<T extends CollectableItem> {
private history: T[] = [];
private idMap: WeakMap<T, string> = new WeakMap();
private idCounter = 0;
private maxNavigations = 3; // 最多保留 3 次导航的历史
// 添加新项目
add(item: T, navigationIndex: number) {
// 生成稳定的 ID
const id = `${navigationIndex}-${++this.idCounter}`;
this.idMap.set(item, id);
// 添加到历史
this.history.push(item);
// 清理过期数据
this.cleanupOldNavigations(navigationIndex);
}
// 根据 ID 查找项目
findById(id: string): T | undefined {
return this.history.find(item => this.idMap.get(item) === id);
}
// 获取所有项目(支持过滤)
getAll(filter?: (item: T) => boolean): Array<T & { id: string }> {
return this.history.filter(filter || (() => true)).map(item => ({
...item,
id: this.idMap.get(item)!
}));
}
// 清理过期数据
private cleanupOldNavigations(currentNav: number) {
const minNav = currentNav - this.maxNavigations;
this.history = this.history.filter(item => {
const itemNav = parseInt(this.idMap.get(item)!.split('-')[0]);
return itemNav > minNav;
});
}
}
这个模式解决了几个关键问题:
- 跨导航保留数据:当页面导航到新 URL 时,之前的网络请求和控制台消息不会丢失
- 稳定的 ID 系统:每个项目都有一个全局唯一的 ID,AI 可以通过 ID 引用特定的请求或消息
- 内存管理:通过限制保留的导航次数,避免内存无限增长
- WeakMap 防止泄漏:使用 WeakMap 存储 ID,当对象被垃圾回收时,ID 映射也会自动清理
响应构建器模式(Response Builder)
已在前面的"响应层"部分详细介绍,这里补充一个高级用法:
// 支持分页的网络请求列表
context
.response()
.addText('Network Requests')
.withNetworkRequests({
filter: req => req.resourceType === 'fetch',
page: 1,
perPage: 10
})
.build();
// 输出示例:
// Network Requests
//
// ## Network Requests (Page 1/3)
// 1. [req-001] GET https://api.example.com/users (200 OK, 145ms)
// 2. [req-002] POST https://api.example.com/login (200 OK, 234ms)
// ...
// 10. [req-010] GET https://api.example.com/profile (200 OK, 98ms)
//
// Use list_network_requests with page=2 to see more
互斥锁模式(Mutex Pattern)
为了保证工具调用的原子性,项目使用了互斥锁:
class Mutex {
private locked = false;
private queue: Array<() => void> = [];
async lock(): Promise<void> {
if (!this.locked) {
this.locked = true;
return;
}
// 如果已锁定,等待解锁
return new Promise(resolve => {
this.queue.push(resolve);
});
}
unlock(): void {
if (this.queue.length > 0) {
// 如果有等待的调用,立即授予锁
const next = this.queue.shift()!;
next();
} else {
this.locked = false;
}
}
}
// 在工具执行时使用
await mutex.lock();
try {
await executeToolLogic();
} finally {
mutex.unlock();
}
为什么需要互斥锁?考虑这个场景:
- AI 同时发起两个工具调用:
click(button1)和take_snapshot() - 如果没有互斥锁,
take_snapshot()可能在click()还在执行时就开始了 - 结果:快照中可能包含了处于中间状态的页面
互斥锁确保:
- 同一时间只有一个工具在执行
- 每个工具执行完成后,页面处于稳定状态
- 快照和网络请求等数据是准确的
延迟初始化模式
浏览器的启动和连接是昂贵的操作,因此项目采用延迟初始化:
class BrowserManager {
private browser: Browser | null = null;
async getBrowser(): Promise<Browser> {
if (!this.browser) {
// 只有在第一次调用时才启动浏览器
this.browser = await initializeBrowser(this.options);
// 注册清理函数
process.on('exit', () => {
this.browser?.close();
});
}
return this.browser;
}
}
// 在工具中使用
const browser = await browserManager.getBrowser();
延迟初始化的好处:
- 启动速度更快:MCP Server 启动时不需要等待浏览器启动
- 资源节省:如果用户只是查询工具列表,不会启动浏览器
- 容错性更好:如果浏览器连接失败,不会影响 MCP Server 的启动
2.3 目录结构与职责划分
chrome-devtools-mcp 的目录结构非常清晰,每个目录都有明确的职责:
chrome-devtools-mcp/
├── src/
│ ├── tools/ # 工具层:26 个 MCP 工具实现
│ │ ├── click.ts # 点击工具
│ │ ├── fill.ts # 表单填充工具
│ │ ├── navigate_page.ts # 导航工具
│ │ ├── performance_start_trace.ts # 性能追踪工具
│ │ └── ... # 其他 23 个工具
│ │
│ ├── formatters/ # 数据格式化层
│ │ ├── AccessibilityFormatter.ts # 可访问性树格式化
│ │ ├── NetworkFormatter.ts # 网络请求格式化
│ │ ├── ConsoleFormatter.ts # 控制台消息格式化
│ │ └── PerformanceTraceFormatter.ts # 性能追踪格式化
│ │
│ ├── trace-processing/ # 性能分析引擎
│ │ ├── parse.ts # 复用 DevTools Frontend 的 TraceEngine
│ │ ├── insights.ts # 提取性能洞察
│ │ └── format.ts # 格式化 Trace 数据为 Markdown
│ │
│ ├── collectors/ # 事件收集器
│ │ ├── PageCollector.ts # 泛型收集器基类
│ │ ├── NetworkCollector.ts # 网络请求收集器
│ │ └── ConsoleCollector.ts # 控制台消息收集器
│ │
│ ├── McpContext.ts # 核心状态管理器
│ ├── McpResponse.ts # 响应构建器
│ ├── browser.ts # 浏览器连接管理
│ ├── main.ts # MCP Server 入口
│ └── types.ts # 类型定义
│
├── tests/ # 测试文件
│ ├── tools/ # 工具测试
│ ├── formatters/ # 格式化器测试
│ └── integration/ # 集成测试
│
├── scripts/ # 构建脚本
│ ├── build.js # Rollup 打包
│ └── generate-licenses.js # 生成第三方许可证声明
│
└── package.json
各目录的职责详解:
src/tools/ - 工具实现目录
每个工具都是一个独立的文件,遵循统一的结构:
// tools/click.ts
import { defineTool } from '../tool-utils.js';
import { z } from 'zod';
export const clickTool = defineTool({
name: 'click',
description: '...',
schema: z.object({
/* ... */
}),
handler: async (args, context) => {
/* ... */
}
});
这种组织方式的好处:
- 易于扩展:添加新工具只需要创建新文件,不需要修改现有代码
- 易于维护:每个工具的逻辑独立,不会相互影响
- 易于测试:每个工具可以单独测试
src/formatters/ - 格式化器目录
格式化器负责将原始数据转换为 AI 友好的 Markdown 格式:
// formatters/NetworkFormatter.ts
export function formatNetworkRequest(request: NetworkRequest): string {
return `
### Request Details
- **URL**: ${request.url}
- **Method**: ${request.method}
- **Status**: ${request.status} ${request.statusText}
- **Type**: ${request.resourceType}
- **Size**: ${formatBytes(request.responseSize)}
- **Time**: ${request.duration}ms
#### Request Headers
\`\`\`
${formatHeaders(request.requestHeaders)}
\`\`\`
#### Response Headers
\`\`\`
${formatHeaders(request.responseHeaders)}
\`\`\`
`.trim();
}
src/trace-processing/ - 性能分析引擎
这是项目最复杂的部分之一,直接集成了 Chrome DevTools Frontend 的 TraceEngine:
// trace-processing/parse.ts
import { TraceEngine } from '@devtools-frontend/models-trace';
export async function parseTrace(traceData: unknown) {
const processor = TraceEngine.TraceProcessor.create();
// 解析 Trace 数据
await processor.parse(traceData);
// 提取性能指标
const insights = processor.insights;
return {
lcp: insights.largestContentfulPaint,
fcp: insights.firstContentfulPaint,
tbt: insights.totalBlockingTime,
longTasks: insights.longTasks
// ... 更多指标
};
}
src/McpContext.ts - 状态管理核心
这是整个架构的心脏,管理所有状态和生命周期:
- 页面集合管理(多标签页支持)
- 快照生成与 ID 系统
- 网络请求收集
- 控制台消息收集
- DevTools 窗口检测
- 智能超时管理
- 节流模拟(网络和 CPU)
src/McpResponse.ts - 响应构建器
提供链式 API 构建工具响应,支持:
- 文本添加
- 快照包含
- 网络请求列表
- 控制台消息列表
- 分页支持
- Markdown 格式化
至此,我们已经深入了解了 chrome-devtools-mcp 的整体架构和设计理念。
三、核心技术实现深度剖析
3.1 MCP 服务器的生命周期
启动流程:从参数解析到工具注册
chrome-devtools-mcp 的启动过程经过精心设计,遵循"渐进式初始化"的原则。让我们从 main.ts 的代码来理解整个生命周期:
// 1. 参数解析
export const args = parseArguments(VERSION);
// 2. 初始化日志系统
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
// 3. 创建 MCP Server 实例
const server = new McpServer(
{
name: 'chrome_devtools',
title: 'Chrome DevTools MCP server',
version: VERSION
},
{ capabilities: { logging: {} } }
);
// 4. 注册所有工具 (共 26 个)
const toolModules = [
consoleTools, // 控制台相关工具
emulationTools, // 模拟工具
inputTools, // 输入自动化工具
networkTools, // 网络调试工具
pagesTools, // 页面管理工具
performanceTools, // 性能分析工具
screenshotTools, // 截图工具
scriptTools, // 脚本执行工具
snapshotTools // 快照工具
];
for (const toolModule of toolModules) {
Object.values(toolModule).forEach(tool => {
server.addTool(tool);
});
}
// 5. 启动 stdio 传输层
const transport = new StdioServerTransport();
await server.connect(transport);
整个启动流程体现了几个关键设计理念:
-
延迟初始化浏览器: 注意到在 MCP Server 启动时,并没有立即启动或连接浏览器。浏览器的初始化被延迟到第一次工具调用时,这样做的好处是:
- MCP Server 启动速度更快 (几十毫秒内完成)
- 避免了不必要的资源占用 (如果用户只是查询工具列表)
- 提供了更好的容错能力 (浏览器连接失败不会导致 MCP Server 无法启动)
-
模块化的工具组织: 所有工具按照功能类别分组在不同的模块中,每个模块负责特定领域的能力。这种组织方式使得:
- 新增工具时只需要在对应模块中添加
- 工具之间相互独立,不会产生耦合
- 便于按类别进行测试和文档生成
互斥锁保证工具调用的原子性
chrome-devtools-mcp 使用了一个全局互斥锁来保证同一时间只有一个工具在执行。这是一个非常重要的设计决策,让我们理解为什么需要它:
// Mutex 的实现 (Mutex.ts)
export class Mutex {
private locked = false;
private queue: Array<() => void> = [];
async lock(): Promise<void> {
if (!this.locked) {
this.locked = true;
return;
}
// 如果已锁定,加入等待队列
return new Promise(resolve => {
this.queue.push(resolve);
});
}
unlock(): void {
if (this.queue.length > 0) {
// 如果有等待的调用,立即授予锁
const next = this.queue.shift()!;
next();
} else {
this.locked = false;
}
}
}
// 在工具执行时使用互斥锁 (main.ts)
const toolMutex = new Mutex();
server.setRequestHandler(CallToolRequestSchema, async request => {
await toolMutex.lock();
try {
// 获取上下文 (可能会初始化浏览器)
const context = await getContext();
// 执行工具逻辑
const response = new McpResponse();
const tool = findTool(request.params.name);
await tool.handler(request, response, context);
// 构建并返回响应
return response.build();
} finally {
toolMutex.unlock();
}
});
为什么需要互斥锁?
考虑这样一个场景:AI 同时发起多个工具调用:
时刻 1: click(button1) 开始执行
时刻 2: take_snapshot() 开始执行 (如果没有互斥锁)
时刻 3: click(button1) 完成
时刻 4: take_snapshot() 完成
如果没有互斥锁,take_snapshot() 可能在 click() 还在执行时就开始了,导致:
- 快照中页面处于中间状态 (按钮点击触发的动画还在进行)
- 网络请求可能不完整 (有些请求还在发送中)
- 控制台消息可能不准确 (有些消息还没有产生)
有了互斥锁,执行顺序变成:
时刻 1: click(button1) 获取锁,开始执行
时刻 2: take_snapshot() 尝试获取锁,进入等待队列
时刻 3: click(button1) 完成,释放锁
时刻 4: take_snapshot() 获取锁,开始执行 (此时页面已经稳定)
时刻 5: take_snapshot() 完成,释放锁
互斥锁的权衡:
优点:
- 保证了数据一致性和准确性
- 简化了工具实现 (不需要考虑并发问题)
- 避免了浏览器资源的竞争
缺点:
- 限制了并发性能 (所有工具调用必须串行执行)
- 如果某个工具执行时间很长,会阻塞其他工具
在当前的实现中,Google 团队认为正确性比性能更重要,因此选择了全局互斥锁。未来可能的优化方向包括:
- 读写锁: 只读操作 (如
list_network_requests) 可以并发执行 - 页面级别的锁: 不同页面的操作可以并发执行
延迟初始化:按需启动浏览器
浏览器的延迟初始化是通过 getContext() 函数实现的:
let context: McpContext;
async function getContext(): Promise<McpContext> {
const extraArgs: string[] = (args.chromeArg ?? []).map(String);
if (args.proxyServer) {
extraArgs.push(`--proxy-server=${args.proxyServer}`);
}
const devtools = args.experimentalDevtools ?? false;
// 判断连接模式
const browser =
args.browserUrl || args.wsEndpoint
? // 模式 1: 连接到已运行的浏览器
await ensureBrowserConnected({
browserURL: args.browserUrl,
wsEndpoint: args.wsEndpoint,
wsHeaders: args.wsHeaders,
devtools
})
: // 模式 2: 启动新的浏览器实例
await ensureBrowserLaunched({
headless: args.headless,
executablePath: args.executablePath,
channel: args.channel,
isolated: args.isolated,
logFile,
viewport: args.viewport,
args: extraArgs,
acceptInsecureCerts: args.acceptInsecureCerts,
devtools
});
// 如果浏览器实例变了,创建新的 Context
if (context?.browser !== browser) {
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: args.experimentalIncludeAllPages
});
}
return context;
}
这个函数的巧妙之处在于:
- 首次调用时初始化: 第一次调用工具时,
context为undefined,会触发浏览器的启动或连接 - 后续调用复用: 后续工具调用会直接返回已存在的
context,不会重复初始化 - 支持浏览器切换: 如果检测到浏览器实例变化 (比如浏览器崩溃后重启),会自动创建新的 Context
代码实现详解 (main.ts)
让我们看一个完整的工具调用流程:
// 处理工具调用请求
server.setRequestHandler(CallToolRequestSchema, async request => {
// 1. 获取互斥锁 (等待之前的工具执行完成)
await toolMutex.lock();
try {
// 2. 获取上下文 (延迟初始化浏览器)
const context = await getContext();
// 3. 找到对应的工具
const tool = allTools.find(t => t.definition.name === request.params.name);
if (!tool) {
throw new Error(`Tool not found: ${request.params.name}`);
}
// 4. 创建响应构建器
const response = new McpResponse();
// 5. 执行工具的 handler
try {
await tool.definition.handler(request, response, context);
} catch (error) {
// 捕获并格式化错误
response.appendResponseLine(`Error: ${error.message}`);
logger(`Tool ${request.params.name} failed:`, error);
}
// 6. 构建最终响应
const result = await response.build(context);
// 7. 返回结果
return {
content: [{ type: 'text', text: result }]
} as CallToolResult;
} finally {
// 8. 释放互斥锁 (允许下一个工具执行)
toolMutex.unlock();
}
});
这个流程展示了 chrome-devtools-mcp 的几个核心设计:
- 统一的错误处理: 所有工具的错误都在框架层被捕获和格式化
- 延迟计算响应: response 对象在
build()时才真正生成内容 - 自动资源管理: 互斥锁通过 finally 块确保一定会被释放
3.2 浏览器连接管理的艺术
浏览器连接管理是 chrome-devtools-mcp 的另一个核心技术点。项目支持两种连接模式,并提供了灵活的配置选项。
两种连接模式的实现
模式 1: 启动新浏览器实例
这是最常用的模式,适用于大多数自动化场景:
export async function ensureBrowserLaunched(options: McpLaunchOptions): Promise<Browser> {
if (browser?.connected) {
return browser;
}
const { channel, executablePath, headless, isolated } = options;
// 1. 确定用户数据目录
const profileDirName = channel && channel !== 'stable' ? `chrome-profile-${channel}` : 'chrome-profile';
let userDataDir = options.userDataDir;
if (!isolated && !userDataDir) {
// 默认使用缓存目录,支持多 Channel 隔离
userDataDir = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', profileDirName);
await fs.promises.mkdir(userDataDir, { recursive: true });
}
// 2. 准备启动参数
const args: LaunchOptions['args'] = [
...(options.args ?? []),
'--hide-crash-restore-bubble' // 隐藏崩溃恢复提示
];
if (headless) {
args.push('--screen-info={3840x2160}'); // 设置高分辨率屏幕
}
if (options.devtools) {
args.push('--auto-open-devtools-for-tabs'); // 自动打开 DevTools
}
// 3. 确定浏览器 Channel
let puppeteerChannel: ChromeReleaseChannel | undefined;
if (!executablePath) {
puppeteerChannel = channel && channel !== 'stable' ? (`chrome-${channel}` as ChromeReleaseChannel) : 'chrome';
}
// 4. 启动浏览器
const browser = await puppeteer.launch({
channel: puppeteerChannel,
targetFilter: makeTargetFilter(), // 过滤内部页面
executablePath,
defaultViewport: null, // 不强制设置视口大小
userDataDir,
pipe: true, // 使用 pipe 通信,比 WebSocket 更快
headless,
args,
acceptInsecureCerts: options.acceptInsecureCerts,
handleDevToolsAsPage: true // 将 DevTools 窗口当作页面处理
});
// 5. 重定向浏览器日志
if (options.logFile) {
browser.process()?.stderr?.pipe(options.logFile);
browser.process()?.stdout?.pipe(options.logFile);
}
// 6. 设置默认视口 (如果指定了)
if (options.viewport) {
const [page] = await browser.pages();
await page.setViewport(options.viewport);
}
logger('Launched Puppeteer');
return browser;
}
这个函数的亮点:
-
智能的用户数据目录管理:
- 默认使用
~/.cache/chrome-devtools-mcp/目录 - 支持多 Channel 隔离 (stable, beta, dev, canary 各自独立)
- 可以通过
isolated参数禁用持久化 (每次启动全新环境)
- 默认使用
-
目标过滤器: 通过
makeTargetFilter()过滤掉 Chrome 内部页面:
function makeTargetFilter() {
const ignoredPrefixes = new Set([
'chrome://', // Chrome 内部页面
'chrome-extension://', // 扩展页面
'chrome-untrusted://' // 不受信任的页面
]);
return function targetFilter(target: Target): boolean {
// 允许新标签页
if (target.url() === 'chrome://newtab/') {
return true;
}
// 过滤内部页面
for (const prefix of ignoredPrefixes) {
if (target.url().startsWith(prefix)) {
return false;
}
}
return true;
};
}
- pipe 通信模式: 使用
pipe: true而不是 WebSocket,性能更好,尤其是在本地环境
模式 2: 连接已运行的浏览器
这种模式适用于几种场景:
- 需要保留用户的登录状态和扩展
- 在远程服务器上运行浏览器
- 需要手动调试和观察浏览器状态
export async function ensureBrowserConnected(options: {
browserURL?: string;
wsEndpoint?: string;
wsHeaders?: Record<string, string>;
devtools: boolean;
}): Promise<Browser> {
if (browser?.connected) {
return browser;
}
const connectOptions: Parameters<typeof puppeteer.connect>[0] = {
targetFilter: makeTargetFilter(),
defaultViewport: null,
handleDevToolsAsPage: true
};
// 两种连接方式
if (options.wsEndpoint) {
// 方式 1: 直接使用 WebSocket 端点
connectOptions.browserWSEndpoint = options.wsEndpoint;
if (options.wsHeaders) {
connectOptions.headers = options.wsHeaders; // 自定义 WebSocket 头
}
} else if (options.browserURL) {
// 方式 2: 使用 HTTP URL (Puppeteer 会自动查询 WebSocket 端点)
connectOptions.browserURL = options.browserURL;
} else {
throw new Error('Either browserURL or wsEndpoint must be provided');
}
logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
browser = await puppeteer.connect(connectOptions);
logger('Connected Puppeteer');
return browser;
}
使用示例:
# 启动 Chrome 并开启远程调试
chrome --remote-debugging-port=9222
# 连接到该浏览器
chrome-devtools-mcp --browser-url=http://localhost:9222
# 或者使用 WebSocket 端点直接连接
chrome-devtools-mcp --ws-endpoint=ws://localhost:9222/devtools/browser/xxx
# 支持自定义 WebSocket 头 (用于认证或端口转发)
chrome-devtools-mcp --ws-endpoint=ws://remote-host:9222/devtools/browser/xxx \
--ws-headers='{"Authorization":"Bearer token123"}'
目标过滤器(Target Filter)
目标过滤器是一个非常实用的功能,它决定了哪些浏览上下文 (Target) 会被 McpContext 管理。
默认的过滤规则:
- ✅ 允许: 普通网页 (
http://,https://) - ✅ 允许: 新标签页 (
chrome://newtab/) - ❌ 拒绝: Chrome 内部页面 (
chrome://settings/,chrome://flags/等) - ❌ 拒绝: 扩展页面 (
chrome-extension://...) - ❌ 拒绝: 不受信任页面 (
chrome-untrusted://...)
这样设计的原因:
- 避免混淆: AI 不应该看到和操作 Chrome 的内部页面
- 安全性: 防止 AI 修改浏览器设置或访问敏感的扩展数据
- 性能: 减少需要管理的 Target 数量
用户数据目录管理
用户数据目录 (User Data Directory) 的管理体现了项目的实用性:
// 默认策略: 按 Channel 隔离
const profileDirName =
channel && channel !== 'stable'
? `chrome-profile-${channel}` // chrome-profile-beta, chrome-profile-dev 等
: 'chrome-profile';
// 目录结构:
// ~/.cache/chrome-devtools-mcp/
// ├── chrome-profile/ (stable)
// ├── chrome-profile-beta/ (beta)
// ├── chrome-profile-dev/ (dev)
// └── chrome-profile-canary/ (canary)
这种设计的好处:
- 持久化数据: Cookies、LocalStorage、IndexedDB 等数据会被保留,避免重复登录
- 多版本共存: 不同 Channel 的数据互不干扰
- 缓存加速: 缓存的资源可以复用,加快页面加载速度
- 可选的隔离模式: 通过
--isolated参数可以禁用持久化,每次启动全新环境
WebSocket 自定义头支持
这是一个高级功能,主要用于两个场景:
场景 1: 远程调试的认证
# 远程服务器上的 Chrome 要求认证
chrome-devtools-mcp --ws-endpoint=ws://remote-host:9222/devtools/browser/xxx \
--ws-headers='{"Authorization":"Bearer secret-token"}'
场景 2: SSH 端口转发
# 在本地建立 SSH 隧道
ssh -L 9222:localhost:9222 user@remote-host
# 连接到转发的端口
chrome-devtools-mcp --browser-url=http://localhost:9222
代码实现详解 (browser.ts)
让我们看一个完整的浏览器初始化流程:
// 1. 解析命令行参数
const args = parseArguments();
// 2. 确定连接模式
if (args.browserUrl || args.wsEndpoint) {
// 模式 A: 连接到已运行的浏览器
browser = await ensureBrowserConnected({
browserURL: args.browserUrl,
wsEndpoint: args.wsEndpoint,
wsHeaders: args.wsHeaders,
devtools: args.experimentalDevtools
});
} else {
// 模式 B: 启动新的浏览器实例
browser = await ensureBrowserLaunched({
headless: args.headless,
executablePath: args.executablePath,
channel: args.channel,
isolated: args.isolated,
logFile,
viewport: args.viewport,
args: args.chromeArg,
acceptInsecureCerts: args.acceptInsecureCerts,
devtools: args.experimentalDevtools
});
}
// 3. 创建 McpContext
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: args.experimentalDevtools,
experimentalIncludeAllPages: args.experimentalIncludeAllPages
});
3.3 McpContext:状态管理的核心
McpContext 类是整个架构的核心,它管理着所有的状态和资源。这是一个复杂而精妙的状态管理器,让我们深入了解它的实现。
管理的状态类型
McpContext 管理了以下几类状态:
export class McpContext implements Context {
browser: Browser;
logger: Debugger;
// 1. 页面集合管理
#pages: Page[] = [];
#pageToDevToolsPage = new Map<Page, Page>();
#selectedPage?: Page;
// 2. 快照系统
#textSnapshot: TextSnapshot | null = null;
// 3. 网络请求收集
#networkCollector: NetworkCollector;
// 4. 控制台消息收集
#consoleCollector: PageCollector<ConsoleMessage | Error>;
// 5. 性能追踪状态
#isRunningTrace = false;
// 6. 节流模拟状态
#networkConditionsMap = new WeakMap<Page, string>();
#cpuThrottlingRateMap = new WeakMap<Page, number>();
// 7. 对话框状态
#dialog?: Dialog;
// 8. DevTools 窗口检测
#devToolsWindows: Page[] = [];
// ... 更多状态
}
1. 页面集合管理
McpContext 需要管理多个标签页,并追踪当前选中的页面:
// 获取所有页面
async getAllPages(): Promise<Page[]> {
if (this.experimentalDevToolsDebugging) {
// 包含 DevTools 窗口
return this.browser.pages();
} else {
// 只返回普通页面
this.#pages = await this.browser.pages();
return this.#pages;
}
}
// 选择页面
selectPage(page: Page): void {
this.#selectedPage = page;
// 清除旧的快照 (切换页面后快照失效)
this.#textSnapshot = null;
}
// 获取当前选中的页面
getSelectedPage(): Page {
if (!this.#selectedPage) {
throw new Error('No page selected. Call select_page first.');
}
return this.#selectedPage;
}
// 根据索引获取页面
getPageByIdx(pageIdx: number): Page {
if (pageIdx < 0 || pageIdx >= this.#pages.length) {
throw new Error(`Invalid page index: ${pageIdx}`);
}
return this.#pages[pageIdx];
}
2. 快照生成与 ID 系统
快照系统是 chrome-devtools-mcp 最创新的部分之一。它基于 Accessibility Tree 而不是传统的 DOM:
#textSnapshot: TextSnapshot | null = null;
#snapshotIdCounter = 0;
async generateSnapshot(): Promise<TextSnapshot> {
const page = this.getSelectedPage();
// 1. 获取可访问性树
const root = await page.accessibility.snapshot();
if (!root) {
throw new Error('Failed to generate accessibility snapshot');
}
// 2. 分配稳定的 ID
const idToNode = new Map<string, TextSnapshotNode>();
const assignIds = (node: SerializedAXNode, prefix = ''): TextSnapshotNode => {
const id = `${prefix}${++this.#snapshotIdCounter}`;
const textNode: TextSnapshotNode = {
...node,
id,
children: [],
};
idToNode.set(id, textNode);
// 递归处理子节点
if (node.children) {
textNode.children = node.children.map((child, i) =>
assignIds(child, `${id}-`)
);
}
return textNode;
};
const rootWithIds = assignIds(root);
// 3. 检测 DevTools 中选中的元素
const selectedElementUid = await this.getSelectedElementFromDevTools(page);
// 4. 缓存快照
this.#textSnapshot = {
root: rootWithIds,
idToNode,
snapshotId: `snap-${Date.now()}`,
selectedElementUid,
};
return this.#textSnapshot;
}
// 根据 ID 获取元素
async getElementByUid(uid: string): Promise<ElementHandle> {
if (!this.#textSnapshot) {
throw new Error('No snapshot available. Call take_snapshot first.');
}
const node = this.#textSnapshot.idToNode.get(uid);
if (!node || !node.backendNodeId) {
throw new Error(`Element not found: ${uid}`);
}
const page = this.getSelectedPage();
const handle = await page.mainFrame().$(`[data-backend-node-id="${node.backendNodeId}"]`);
if (!handle) {
throw new Error(`Element handle not found for uid: ${uid}`);
}
return handle;
}
为什么使用 Accessibility Tree?
- AI 友好: 可访问性树是语义化的,包含元素的角色 (role)、名称 (name)、值 (value) 等信息,AI 更容易理解
- 结构清晰: 相比 DOM,可访问性树过滤掉了布局和样式元素,只保留了有意义的交互元素
- 性能优秀: 可访问性树比完整 DOM 小得多,生成和传输速度更快
- 标准化: 基于 ARIA 标准,不同网站的结构更一致
快照 ID 系统的设计:
每个元素都有一个全局唯一的 ID,格式为: 1-2-3-4
1: 第一层元素的索引1-2: 第一层第二个元素1-2-3: 该元素的第三个子元素1-2-3-4: 该子元素的第四个子元素
这种层级 ID 的好处:
- 稳定: 只要页面结构不变,ID 就不变
- 直观: 可以从 ID 推断出元素的层级关系
- 高效: 查找元素只需要 O(1) 的 Map 查找
3. 网络请求收集
网络请求的收集使用了 NetworkCollector,这是一个特殊的收集器:
export class NetworkCollector extends PageCollector<HTTPRequest> {
private requestToResponse = new WeakMap<HTTPRequest, HTTPResponse>();
// 监听请求
protected override getListeners(): ListenerMap<HTTPRequest> {
return {
request: request => {
this.add(request);
},
requestfinished: request => {
// 保存响应
const response = request.response();
if (response) {
this.requestToResponse.set(request, response);
}
},
requestfailed: request => {
// 标记失败的请求
this.markAsFailed(request);
}
};
}
// 获取请求的响应
getResponse(request: HTTPRequest): HTTPResponse | undefined {
return this.requestToResponse.get(request);
}
}
网络请求收集的特点:
- 跨导航保留: 当页面导航到新 URL 时,之前的请求不会丢失 (保留最近 3 次导航)
- 稳定 ID: 每个请求都有一个全局唯一的 ID (格式:
req-<navigation>-<index>) - 响应关联: 使用 WeakMap 关联请求和响应,避免内存泄漏
- 失败标记: 失败的请求会被特别标记
4. 控制台消息收集
控制台消息的收集类似:
#consoleCollector: PageCollector<ConsoleMessage | Error>;
// 在 McpContext 初始化时设置
this.#consoleCollector = new PageCollector<ConsoleMessage | Error>(browser, {
console: message => message,
pageerror: error => error,
});
5. DevTools 窗口检测
这是一个非常有趣的功能 - McpContext 可以检测打开的 DevTools 窗口,并读取其中的状态:
// 检测 DevTools 窗口
async detectDevToolsWindows(): Promise<void> {
const allPages = await this.browser.pages();
this.#devToolsWindows = allPages.filter(page => {
const url = page.url();
return url.startsWith('devtools://');
});
}
// 获取 DevTools 中选中的元素
async getSelectedElementFromDevTools(targetPage: Page): Promise<string | undefined> {
for (const devToolsPage of this.#devToolsWindows) {
// 从 DevTools 标题中提取目标页面 URL
const targetUrl = extractUrlLikeFromDevToolsTitle(
await devToolsPage.title()
);
if (urlsEqual(targetUrl, targetPage.url())) {
// 执行脚本获取选中的元素
const backendNodeId = await devToolsPage.evaluate(() => {
// 访问 DevTools 的内部 API
return window.UI.context.flavor(SDK.DOMModel.DOMNode)?.backendNodeId();
});
if (backendNodeId) {
// 在快照中找到对应的元素 ID
return this.findUidByBackendNodeId(backendNodeId);
}
}
}
return undefined;
}
这个功能实现了 DevTools UI 与快照的双向同步:
- 当你在 DevTools 中选中一个元素时
- MCP 快照中该元素会被标记为
selected - AI 可以看到你当前关注的元素
智能超时管理
McpContext 会根据当前的节流设置自动调整超时时间:
// 获取超时倍数
getTimeoutMultiplier(): number {
const page = this.getSelectedPage();
// 1. 获取网络节流倍数
const networkCondition = this.#networkConditionsMap.get(page);
const networkMultiplier = getNetworkMultiplierFromString(networkCondition);
// 2. 获取 CPU 节流倍数
const cpuThrottling = this.#cpuThrottlingRateMap.get(page) || 1;
// 3. 返回较大的倍数
return Math.max(networkMultiplier, cpuThrottling);
}
// 使用智能超时
async waitForStability(timeout?: number): Promise<void> {
const multiplier = this.getTimeoutMultiplier();
const actualTimeout = (timeout || DEFAULT_TIMEOUT) * multiplier;
await this.waitForEventsAfterAction(async () => {
// 等待网络空闲
await this.getSelectedPage().waitForNetworkIdle({
timeout: actualTimeout,
});
});
}
为什么需要智能超时?
考虑这个场景:
- 用户启用了 "Slow 3G" 网络节流 (10x 延迟)
- 默认超时是 5 秒
- 页面加载实际需要 30 秒
如果不调整超时,操作会总是失败。智能超时会自动将超时调整为 50 秒,确保操作能够完成。
节流模拟(网络和 CPU)
McpContext 支持模拟慢速网络和 CPU:
// 设置网络节流
async setNetworkConditions(
page: Page,
condition: keyof typeof PredefinedNetworkConditions | null
): Promise<void> {
if (condition) {
// 应用预定义的网络条件
await page.emulateNetworkConditions(
PredefinedNetworkConditions[condition]
);
this.#networkConditionsMap.set(page, condition);
} else {
// 清除网络节流
await page.emulateNetworkConditions(null);
this.#networkConditionsMap.delete(page);
}
}
// 设置 CPU 节流
async setCPUThrottling(page: Page, rate: number): Promise<void> {
const client = await page.createCDPSession();
if (rate > 1) {
// 启用 CPU 节流 (rate=4 表示 4 倍慢速)
await client.send('Emulation.setCPUThrottlingRate', {rate});
this.#cpuThrottlingRateMap.set(page, rate);
} else {
// 禁用 CPU 节流
await client.send('Emulation.setCPUThrottlingRate', {rate: 1});
this.#cpuThrottlingRateMap.delete(page);
}
}
代码实现详解 (McpContext.ts)
让我们看一个完整的工具调用过程中 McpContext 的使用:
// 工具: click
async function clickTool(uid: string, context: McpContext) {
// 1. 获取当前页面
const page = context.getSelectedPage();
// 2. 从快照中获取元素
const element = await context.getElementByUid(uid);
// 3. 执行点击操作,并等待后续事件
await context.waitForEventsAfterAction(async () => {
await element.click();
});
// 4. 生成新快照
const snapshot = await context.generateSnapshot();
// 5. 返回响应
return context.response().addText('Successfully clicked element').withSnapshot().withNetworkRequests();
}
waitForEventsAfterAction 的实现非常巧妙:
async waitForEventsAfterAction(action: () => Promise<void>): Promise<void> {
const page = this.getSelectedPage();
const helper = new WaitForHelper(page);
// 1. 开始监听事件
helper.startListening();
try {
// 2. 执行操作
await action();
// 3. 等待网络请求完成
await helper.waitForNetworkIdle(this.getTimeoutMultiplier());
// 4. 等待额外的稳定时间
await new Promise(resolve => setTimeout(resolve, 100));
} finally {
// 5. 停止监听
helper.stopListening();
}
}
这个函数确保:
- 操作触发的所有网络请求都已完成
- 页面已经渲染完成
- 新的快照会包含最新的页面状态
3.4 基于可访问性树的创新快照系统
可访问性树 (Accessibility Tree) 快照系统是 chrome-devtools-mcp 最具创新性的设计之一。相比传统的 DOM 快照,它为 AI 提供了更易理解、更语义化的页面表示。
为什么选择 Accessibility Tree
传统的浏览器自动化工具 (如 Selenium, Puppeteer) 通常基于 DOM 树进行元素定位和操作。但 DOM 树存在几个问题:
- 噪音太多: DOM 包含大量的布局和样式元素 (
<div>,<span>等),这些元素对交互没有实际意义 - 缺乏语义: DOM 元素的名称 (标签名) 不能表达其功能,AI 难以理解元素的作用
- 体积庞大: 一个复杂页面的 DOM 树可能包含数千个节点,传输和处理成本高
- 不统一: 不同网站的 DOM 结构差异巨大,AI 难以总结出通用模式
相比之下,可访问性树有以下优势:
优势 1: 结构化且 AI 友好
可访问性树基于 ARIA (Accessible Rich Internet Applications) 标准,每个节点都有明确的语义:
// 可访问性树节点示例
{
role: 'button', // 元素类型
name: 'Submit Form', // 用户可见的标签
value: '', // 当前值 (对输入框等)
description: 'Submit the login form', // 辅助描述
children: [] // 子节点
}
AI 可以轻松理解:
- 这是一个按钮 (
role: 'button') - 按钮上的文字是 "Submit Form" (
name) - 它的作用是提交登录表单 (
description)
优势 2: 语义丰富
可访问性树保留了元素的语义信息,而不仅仅是结构:
# DOM 树表示 (复杂且无语义)
<div class="auth-container">
<div class="form-wrapper">
<div class="input-group">
<label for="username">Username</label>
<input id="username" type="text" />
</div>
<div class="button-container">
<button class="btn btn-primary" onclick="submit()">Login</button>
</div>
</div>
</div>
# 可访问性树表示 (简洁且语义化)
- textbox "Username" (empty)
- button "Login"
AI 可以直接看到:
- 有一个用户名输入框,当前是空的
- 有一个登录按钮
优势 3: 性能优秀
可访问性树通常比 DOM 树小 90% 以上:
复杂网页示例:
- DOM 节点: 3,547 个
- 可访问性节点: 247 个
- 减少: 93%
这意味着:
- 快照生成速度更快 (毫秒级 vs 秒级)
- 传输到 AI 的数据量更小 (几 KB vs 几百 KB)
- AI 处理速度更快 (更少的 tokens)
优势 4: 标准化
基于 ARIA 标准,不同网站的可访问性树结构更加统一:
// 电商网站 A 的购买按钮
{role: 'button', name: 'Add to Cart'}
// 电商网站 B 的购买按钮
{role: 'button', name: 'Buy Now'}
// 尽管 DOM 结构完全不同,可访问性树的表示是一致的
快照 ID 系统设计
chrome-devtools-mcp 为每个可访问性树节点分配一个唯一且稳定的 ID:
// ID 分配逻辑 (McpContext.ts)
function assignIds(node: SerializedAXNode, prefix = '', counter: { value: number }): TextSnapshotNode {
// 生成层级 ID: 1, 1-1, 1-1-1, etc.
const id = prefix ? `${prefix}-${counter.value++}` : `${counter.value++}`;
const result: TextSnapshotNode = {
...node,
id,
children: []
};
// 递归处理子节点
if (node.children) {
result.children = node.children.map(child => assignIds(child, id, counter));
}
return result;
}
ID 系统的特点:
-
层级化: ID 反映了元素在树中的位置
1 # 根节点 1-1 # 第一个子节点 1-1-1 # 第一个子节点的第一个子节点 1-1-2 # 第一个子节点的第二个子节点 1-2 # 第二个子节点 -
相对稳定: 只要页面结构不变,ID 就不变
- 有利于 AI 进行多步操作 (先快照,然后引用 ID 操作)
- 有利于调试 (相同元素在不同快照中有相同的 ID 结构)
-
全局唯一: 使用计数器确保每个快照中的 ID 都是唯一的
-
易于解析: AI 可以从 ID 推断元素关系
"1-2-3 是 1-2 的子节点" "1-2 和 1-3 是兄弟节点"
节点遍历与 ID 分配
完整的快照生成流程:
// McpContext.ts
#snapshotIdCounter = 0;
async generateSnapshot(): Promise<TextSnapshot> {
const page = this.getSelectedPage();
// 1. 调用 Chrome DevTools Protocol 获取可访问性树
const axTree = await page.accessibility.snapshot();
if (!axTree) {
throw new Error('Failed to get accessibility snapshot');
}
// 2. 分配 ID 并构建索引
const idToNode = new Map<string, TextSnapshotNode>();
const assignIdsWithIndex = (node: SerializedAXNode, prefix = ''): TextSnapshotNode => {
const id = prefix ? `${prefix}-${++this.#snapshotIdCounter}` : `${++this.#snapshotIdCounter}`;
const textNode: TextSnapshotNode = {
...node,
id,
children: [],
};
// 建立 ID 到节点的映射
idToNode.set(id, textNode);
// 递归处理子节点
if (node.children) {
textNode.children = node.children.map(child =>
assignIdsWithIndex(child, id)
);
}
return textNode;
};
const rootWithIds = assignIdsWithIndex(axTree);
// 3. 检测 DevTools 中选中的元素 (如果有)
const selectedElementUid = await this.getSelectedElementFromDevTools(page);
// 4. 构建快照对象
const snapshot: TextSnapshot = {
root: rootWithIds,
idToNode,
snapshotId: `snapshot-${Date.now()}`,
selectedElementUid,
};
// 5. 缓存快照
this.#textSnapshot = snapshot;
return snapshot;
}
与 DevTools UI 的状态同步
这是一个非常创新的功能 - MCP 可以检测用户在 Chrome DevTools 中选中的元素,并在快照中标记它:
// 获取 DevTools 中选中的元素
async getSelectedElementFromDevTools(targetPage: Page): Promise<string | undefined> {
// 1. 找到对应的 DevTools 窗口
const devToolsPage = this.findDevToolsPageFor(targetPage);
if (!devToolsPage) {
return undefined;
}
try {
// 2. 在 DevTools 窗口中执行脚本,获取选中元素的 backend node ID
const backendNodeId = await devToolsPage.evaluate(() => {
// 访问 DevTools 的内部 API
// @ts-ignore - DevTools 内部 API
const selectedNode = window.UI?.context?.flavor(window.SDK?.DOMModel?.DOMNode);
return selectedNode?.backendNodeId();
});
if (!backendNodeId) {
return undefined;
}
// 3. 在快照中找到对应的元素 ID
return this.findUidByBackendNodeId(backendNodeId);
} catch (error) {
// DevTools API 可能不可用,忽略错误
return undefined;
}
}
// 根据 backend node ID 查找快照中的 UID
findUidByBackendNodeId(backendNodeId: number): string | undefined {
if (!this.#textSnapshot) {
return undefined;
}
// 遍历快照,找到匹配的节点
for (const [uid, node] of this.#textSnapshot.idToNode) {
if (node.backendNodeId === backendNodeId) {
return uid;
}
}
return undefined;
}
DevTools 同步的应用场景:
-
辅助调试: 用户在 DevTools 中选中一个元素,AI 可以看到并理解用户的意图
用户: "为什么这个按钮点不了?" [用户在 DevTools 中选中了该按钮] AI: "我看到你选中了 UID 为 1-3-5 的按钮,让我检查一下它的状态..." -
快速定位: 用户可以通过 DevTools 选中元素,然后让 AI 对其进行操作
用户: "点击我选中的这个按钮" [AI 从快照中读取 selectedElementUid,然后执行点击] -
学习用户意图: AI 可以观察用户在 DevTools 中的操作,理解用户关注的页面区域
代码实现详解 (createTextSnapshot)
格式化函数将快照转换为 Markdown 格式,便于 AI 理解:
// formatters/AccessibilityFormatter.ts
export function formatSnapshot(snapshot: TextSnapshot, verbose = false): string {
const lines: string[] = [];
// 递归格式化节点
function formatNode(node: TextSnapshotNode, indent = 0): void {
const prefix = ' '.repeat(indent);
// 构建节点描述
const parts: string[] = [];
// 1. UID (用于引用)
parts.push(`[${node.id}]`);
// 2. Role (元素类型)
if (node.role) {
parts.push(node.role);
}
// 3. Name (用户可见的标签)
if (node.name) {
parts.push(`"${node.name}"`);
}
// 4. Value (当前值)
if (node.value) {
parts.push(`value="${node.value}"`);
}
// 5. 选中标记
if (node.id === snapshot.selectedElementUid) {
parts.push('← SELECTED IN DEVTOOLS');
}
// 6. 详细信息 (verbose 模式)
if (verbose) {
if (node.description) {
parts.push(`desc="${node.description}"`);
}
if (node.disabled) {
parts.push('DISABLED');
}
if (node.focused) {
parts.push('FOCUSED');
}
}
lines.push(prefix + parts.join(' '));
// 递归处理子节点
for (const child of node.children) {
formatNode(child, indent + 1);
}
}
lines.push(`# Page Snapshot (${snapshot.snapshotId})`);
lines.push('');
formatNode(snapshot.root);
return lines.join('\n');
}
快照输出示例:
# Page Snapshot (snapshot-1702345678901)
[1] WebArea "Login Page"
[1-1] banner "Site Header"
[1-1-1] link "Home"
[1-1-2] link "About"
[1-1-3] link "Contact"
[1-2] main
[1-2-1] heading "Welcome Back"
[1-2-2] form "Login Form"
[1-2-2-1] textbox "Username" value="" ← SELECTED IN DEVTOOLS
[1-2-2-2] textbox "Password" value="••••••••" type="password"
[1-2-2-3] checkbox "Remember me" checked=false
[1-2-2-4] button "Sign In"
[1-2-3] link "Forgot password?"
[1-3] contentinfo "Footer"
[1-3-1] StaticText "© 2025 Example Inc."
AI 可以从这个快照中理解:
- 页面结构 (header, main, footer)
- 表单元素及其状态 (用户名为空,密码已填写,未勾选记住我)
- 用户当前选中的元素 (用户名输入框)
- 所有可交互元素的 UID (用于后续操作)
3.5 事件收集器模式的精妙设计
事件收集器 (Event Collector) 是 chrome-devtools-mcp 用于管理网络请求和控制台消息的核心机制。它解决了一个关键问题:如何在页面导航后仍然保留历史数据?
PageCollector 泛型收集器
PageCollector 是一个泛型基类,可以收集任何类型的页面事件:
// PageCollector.ts
export class PageCollector<T> {
#browser: Browser;
#listenersInitializer: (collector: (item: T) => void) => ListenerMap<PageEvents>;
#listeners = new WeakMap<Page, ListenerMap>();
#maxNavigationSaved = 3; // 最多保留 3 次导航的历史
// 存储结构: Page -> [最新导航的数组, 上次导航的数组, 上上次导航的数组]
protected storage = new WeakMap<Page, Array<Array<WithSymbolId<T>>>>();
constructor(browser: Browser, listeners: (collector: (item: T) => void) => ListenerMap<PageEvents>) {
this.#browser = browser;
this.#listenersInitializer = listeners;
}
async init() {
const pages = await this.#browser.pages();
for (const page of pages) {
this.#initializePage(page);
}
// 监听新页面创建
this.#browser.on('targetcreated', async target => {
const page = await target.page();
if (page) {
this.#initializePage(page);
}
});
// 清理销毁的页面
this.#browser.on('targetdestroyed', async target => {
const page = await target.page();
if (page) {
this.#cleanupPageDestroyed(page);
}
});
}
// 初始化页面的事件监听
#initializePage(page: Page) {
if (this.storage.has(page)) {
return; // 已经初始化过
}
// 创建 ID 生成器
const idGenerator = createIdGenerator();
// 创建存储结构: [[当前导航的数组]]
const storedLists: Array<Array<WithSymbolId<T>>> = [[]];
this.storage.set(page, storedLists);
// 创建事件监听器
const listeners = this.#listenersInitializer(value => {
const withId = value as WithSymbolId<T>;
withId[stableIdSymbol] = idGenerator();
// 添加到当前导航的数组
const navigations = this.storage.get(page) ?? [[]];
navigations[0].push(withId);
});
// 监听导航事件
page.on('framenavigated', frame => {
if (frame === page.mainFrame()) {
// 主框架导航,创建新的数组
const navigations = this.storage.get(page) ?? [[]];
navigations.unshift([]); // 在前面插入新数组
// 限制保留的导航数量
if (navigations.length > this.#maxNavigationSaved) {
navigations.pop(); // 删除最旧的导航
}
}
});
// 注册事件监听器
for (const [event, handler] of Object.entries(listeners)) {
page.on(event as keyof PageEvents, handler as any);
}
this.#listeners.set(page, listeners);
}
// 获取所有收集的项目
getAll(page: Page): Array<WithSymbolId<T>> {
const navigations = this.storage.get(page) ?? [[]];
// 合并所有导航的数组 (新的在前)
return navigations.flat();
}
// 根据 ID 查找项目
findById(page: Page, id: number): WithSymbolId<T> | undefined {
const all = this.getAll(page);
return all.find(item => item[stableIdSymbol] === id);
}
}
关键设计点解析:
-
泛型设计:
PageCollector<T>可以收集任何类型的事件,代码复用性强 -
WeakMap 存储: 使用
WeakMap而不是Map,当页面被销毁时,相关数据会自动被垃圾回收,避免内存泄漏 -
多层数组结构:
Array<Array<T>>表示多次导航的历史[ [req1, req2, req3], // 当前导航的请求 [req4, req5], // 上次导航的请求 [req6, req7, req8] // 上上次导航的请求 ]; -
导航感知: 监听
framenavigated事件,当主框架导航时自动创建新数组
跨导航保留历史数据
这是收集器最重要的特性。考虑这个场景:
1. 用户访问 pageA.com
2. 发出请求: req1, req2, req3
3. 用户点击链接,导航到 pageB.com
4. 发出请求: req4, req5
5. AI 调用 list_network_requests
传统方案的问题:
- 导航后,req1, req2, req3 的数据会丢失
- AI 无法分析完整的用户行为
chrome-devtools-mcp 的解决方案:
- 保留最近 3 次导航的历史
- AI 可以看到所有 5 个请求
- 每个请求都标记了所属的导航索引
稳定 ID 生成机制
每个收集的项目都有一个稳定的 ID:
// ID 生成器
function createIdGenerator() {
let i = 1;
return () => {
if (i === Number.MAX_SAFE_INTEGER) {
i = 0; // 循环重置
}
return i++;
};
}
// ID 存储使用 Symbol (避免与对象自身属性冲突)
export const stableIdSymbol = Symbol('stableIdSymbol');
type WithSymbolId<T> = T & {
[stableIdSymbol]?: number;
};
// 在收集时分配 ID
const idGenerator = createIdGenerator();
const withId = request as WithSymbolId<HTTPRequest>;
withId[stableIdSymbol] = idGenerator();
为什么使用 Symbol?
- 避免冲突:
HTTPRequest对象可能已经有id属性,使用 Symbol 确保不会覆盖 - 隐藏实现: Symbol 属性不会在 JSON 序列化中出现,不会污染输出
- 类型安全: TypeScript 可以正确推断 Symbol 属性的类型
NetworkCollector 特殊处理
网络请求收集器继承自 PageCollector,并添加了一些特殊逻辑:
// PageCollector.ts
export class NetworkCollector extends PageCollector<HTTPRequest> {
#requestToResponse = new WeakMap<HTTPRequest, HTTPResponse | null>();
#failedRequests = new WeakSet<HTTPRequest>();
constructor(browser: Browser) {
super(browser, collector => ({
// 请求开始
request: request => {
collector(request);
},
// 请求完成
requestfinished: request => {
const response = request.response();
this.#requestToResponse.set(request, response);
},
// 请求失败
requestfailed: request => {
this.#failedRequests.add(request);
this.#requestToResponse.set(request, null);
}
}));
}
// 获取请求的响应
getResponse(request: HTTPRequest): HTTPResponse | null | undefined {
return this.#requestToResponse.get(request);
}
// 检查请求是否失败
isFailed(request: HTTPRequest): boolean {
return this.#failedRequests.has(request);
}
// 获取请求的状态
getStatus(request: HTTPRequest): 'pending' | 'success' | 'failed' {
if (this.#failedRequests.has(request)) {
return 'failed';
}
if (this.#requestToResponse.has(request)) {
return 'success';
}
return 'pending';
}
}
NetworkCollector 的特殊之处:
-
三态管理: 区分 pending (进行中)、success (成功)、failed (失败) 三种状态
-
响应关联: 使用
WeakMap关联请求和响应- 为什么是 WeakMap? 因为
HTTPRequest对象可能很大,WeakMap 允许垃圾回收 - 为什么是
null?null表示请求失败了 (没有响应),undefined表示还没完成
- 为什么是 WeakMap? 因为
-
失败标记: 使用
WeakSet标记失败的请求- 比
Map<HTTPRequest, boolean>更轻量 - 自动垃圾回收
- 比
WeakMap 避免内存泄漏
这是一个关键的工程决策。考虑这个场景:
// ❌ 不好的做法: 使用 Map
class BadNetworkCollector {
private requestToResponse = new Map<HTTPRequest, HTTPResponse>();
// 问题: 即使请求已经不再使用,Map 仍然持有引用,导致内存泄漏
}
// ✅ 好的做法: 使用 WeakMap
class GoodNetworkCollector {
private requestToResponse = new WeakMap<HTTPRequest, HTTPResponse>();
// 好处: 当 HTTPRequest 对象不再被其他地方引用时,
// WeakMap 中的条目会自动被垃圾回收
}
实际影响:
场景: 用户浏览 100 个页面,每个页面 50 个请求
使用 Map:
- 内存占用: ~500MB (5000 个请求对象)
- 问题: 内存持续增长,最终可能导致崩溃
使用 WeakMap:
- 内存占用: ~50MB (只有当前和最近几次导航的请求)
- 好处: 旧请求会自动被垃圾回收
代码实现详解 (PageCollector.ts)
让我们看一个完整的使用示例:
// 创建网络收集器
const networkCollector = new NetworkCollector(browser);
await networkCollector.init();
// ... 用户浏览页面,发出请求 ...
// 获取所有请求
const allRequests = networkCollector.getAll(page);
// 格式化输出
for (const request of allRequests) {
const id = request[stableIdSymbol];
const status = networkCollector.getStatus(request);
const response = networkCollector.getResponse(request);
console.log(`[${id}] ${request.method()} ${request.url()}`);
console.log(` Status: ${status}`);
if (response) {
console.log(` Response: ${response.status()} ${response.statusText()}`);
}
}
输出示例:
[1] GET https://example.com/
Status: success
Response: 200 OK
[2] GET https://example.com/style.css
Status: success
Response: 200 OK
[3] POST https://example.com/api/login
Status: failed
[4] GET https://next-page.com/
Status: success
Response: 200 OK
(未完待续,接下来是 3.6 响应构建器的延迟执行策略 和第四部分 26 个工具的功能解析...)
3.6 响应构建器的延迟执行策略
构建器模式的应用
McpResponse 类使用构建器模式,提供了流畅的 API 来组装工具响应:
// McpResponse 的核心结构
export class McpResponse {
#responseLines: string[] = [];
#includeSnapshot = false;
#includeNetworkRequests = false;
#includeConsoleMessages = false;
#includePages = false;
// 链式调用: 添加文本行
appendResponseLine(line: string): this {
this.#responseLines.push(line);
return this;
}
// 链式调用: 包含快照
includeSnapshot(options?: SnapshotOptions): this {
this.#includeSnapshot = true;
return this;
}
// 链式调用: 包含网络请求
includeNetworkRequests(filter?: RequestFilter): this {
this.#includeNetworkRequests = true;
return this;
}
// 延迟计算: 构建最终响应
async build(context: McpContext): Promise<string> {
const sections: string[] = [];
// 1. 文本响应
if (this.#responseLines.length > 0) {
sections.push(this.#responseLines.join('\n'));
}
// 2. 页面列表 (如果需要)
if (this.#includePages) {
sections.push(await formatPages(context));
}
// 3. 快照 (如果需要)
if (this.#includeSnapshot) {
const snapshot = await context.generateSnapshot();
sections.push(formatSnapshot(snapshot));
}
// 4. 网络请求 (如果需要)
if (this.#includeNetworkRequests) {
const requests = context.getNetworkRequests();
sections.push(formatNetworkRequests(requests));
}
// 5. 控制台消息 (如果需要)
if (this.#includeConsoleMessages) {
const messages = context.getConsoleMessages();
sections.push(formatConsoleMessages(messages));
}
return sections.join('\n\n---\n\n');
}
}
延迟计算的优势
- 性能优化: 快照和网络请求数据只有在真正需要时才生成
- 灵活性: 工具可以先决定要包含什么,最后统一构建
- 一致性: 所有数据在同一时刻生成,保证状态一致
多格式内容支持
响应构建器支持多种内容类型:
- 纯文本
- Markdown 格式的快照
- 表格化的网络请求列表
- JSON 格式的详细数据
Markdown 格式化
所有输出都使用 Markdown 格式,便于 AI 理解:
# Tool Execution Result
Successfully clicked button "Submit"
---
## Page Snapshot
[1] WebArea "Login Page"
[1-1] button "Submit" ← clicked
---
## Network Requests (Page 1/2)
1. [req-1-1] POST https://api.example.com/login
Status: 200 OK
Time: 234ms
Size: 1.2KB
四、26 个工具的功能解析
chrome-devtools-mcp 提供了 26 个 MCP 工具,覆盖了浏览器自动化的各个方面。这些工具被组织在 6 大功能类别中。
4.1 输入自动化(8 个工具)
1. click - 智能点击工具
点击页面元素,支持单击和双击:
// 工具定义
defineTool({
name: 'click',
description: 'Click an element on the page',
schema: {
uid: z.string().describe('Element UID from snapshot'),
dblClick: z.boolean().optional()
},
handler: async (request, response, context) => {
const element = await context.getElementByUid(request.params.uid);
await context.waitForEventsAfterAction(async () => {
await element.click({ count: request.params.dblClick ? 2 : 1 });
});
response.appendResponseLine('Successfully clicked element');
response.includeSnapshot();
}
});
特点:
- 自动等待元素可见和可点击
- 支持双击 (
dblClick: true) - 自动等待点击后的网络请求完成
- 返回新的页面快照
使用场景:
- 点击按钮提交表单
- 点击链接导航
- 触发下拉菜单
2. fill / fill_form - 表单填充工具
填充输入框或选择下拉选项:
// fill: 填充单个输入框
defineTool({
name: 'fill',
schema: {
uid: z.string(),
value: z.string()
},
handler: async (request, response, context) => {
const element = await context.getElementByUid(request.params.uid);
await element.fill(request.params.value);
response.includeSnapshot();
}
});
// fill_form: 批量填充表单
defineTool({
name: 'fill_form',
schema: {
fields: z.array(
z.object({
uid: z.string(),
value: z.string()
})
)
},
handler: async (request, response, context) => {
for (const field of request.params.fields) {
const element = await context.getElementByUid(field.uid);
await element.fill(field.value);
}
response.appendResponseLine(`Filled ${request.params.fields.length} fields`);
response.includeSnapshot();
}
});
特点:
- 自动识别输入框类型 (text, password, email 等)
- 支持
<select>下拉框选择 - 支持复选框 (
checkbox) 和单选框 (radio) fill_form可以一次填充整个表单
3. drag - 拖拽操作工具
拖拽元素到目标位置:
defineTool({
name: 'drag',
schema: {
sourceUid: z.string(),
targetUid: z.string()
},
handler: async (request, response, context) => {
const source = await context.getElementByUid(request.params.sourceUid);
const target = await context.getElementByUid(request.params.targetUid);
await source.drag(target);
response.appendResponseLine('Successfully dragged element');
response.includeSnapshot();
}
});
使用场景:
- 拖拽文件到上传区域
- 拖拽排序列表项
- 拖拽调整窗口大小
4. hover - 悬停触发工具
鼠标悬停在元素上,触发悬停效果:
5. press_key - 键盘操作工具
模拟键盘输入,支持组合键:
defineTool({
name: 'press_key',
schema: {
key: z.string().describe('Key name like "Enter", "Ctrl+C"'),
repeat: z.number().optional()
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
for (let i = 0; i < (request.params.repeat || 1); i++) {
await page.keyboard.press(request.params.key);
}
response.appendResponseLine(`Pressed ${request.params.key}`);
response.includeSnapshot();
}
});
支持的按键:
- 普通键:
"a","1","Enter","Escape" - 组合键:
"Ctrl+C","Cmd+V","Shift+Tab" - 特殊键:
"ArrowUp","PageDown","F5"
6. upload_file - 文件上传工具
上传文件到文件选择器:
7. handle_dialog - 对话框处理工具
处理 alert, confirm, prompt 对话框:
defineTool({
name: 'handle_dialog',
schema: {
accept: z.boolean(),
promptText: z.string().optional()
},
handler: async (request, response, context) => {
const dialog = context.getDialog();
if (request.params.accept) {
await dialog.accept(request.params.promptText);
} else {
await dialog.dismiss();
}
response.appendResponseLine('Dialog handled');
}
});
4.2 导航自动化(6 个工具)
1. navigate_page - 页面导航工具
导航到指定 URL 或前进/后退:
defineTool({
name: 'navigate_page',
schema: {
type: z.enum(['url', 'back', 'forward', 'reload']),
url: z.string().optional(),
timeout: z.number().optional()
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
switch (request.params.type) {
case 'url':
await page.goto(request.params.url, { timeout: request.params.timeout });
break;
case 'back':
await page.goBack();
break;
case 'forward':
await page.goForward();
break;
case 'reload':
await page.reload();
break;
}
response.appendResponseLine(`Navigated: ${request.params.type}`);
response.includeSnapshot();
}
});
2. new_page / close_page - 页面生命周期管理
创建新标签页或关闭标签页
3. list_pages / select_page - 页面管理工具
列出所有标签页并切换当前页面:
// list_pages: 列出所有打开的页面
defineTool({
name: 'list_pages',
handler: async (request, response, context) => {
response.setIncludePages(true);
}
});
// select_page: 选择要操作的页面
defineTool({
name: 'select_page',
schema: {
pageIdx: z.number()
},
handler: async (request, response, context) => {
const page = context.getPageByIdx(request.params.pageIdx);
await page.bringToFront();
context.selectPage(page);
response.setIncludePages(true);
}
});
4. wait_for - 条件等待工具
等待指定文本出现在页面上
4.3 性能分析(3 个工具)
1. performance_start_trace - 开始追踪
开始记录性能追踪数据:
defineTool({
name: 'performance_start_trace',
schema: {
reload: z.boolean(),
autoStop: z.boolean()
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
// 启动追踪
await page.tracing.start({
categories: [
'devtools.timeline',
'disabled-by-default-devtools.timeline',
'v8.execute'
// ... 更多类别
]
});
if (request.params.reload) {
await page.reload();
}
if (request.params.autoStop) {
await new Promise(resolve => setTimeout(resolve, 5000));
await stopAndAnalyzeTrace(context);
}
response.appendResponseLine('Performance trace started');
}
});
2. performance_stop_trace - 停止并分析
停止追踪并分析性能数据,提取 Core Web Vitals:
async function stopAndAnalyzeTrace(context: McpContext) {
const page = context.getSelectedPage();
const traceBuffer = await page.tracing.stop();
// 使用 DevTools Frontend 的 TraceEngine 解析
const traceResult = await parseRawTraceBuffer(traceBuffer);
// 提取性能指标
const summary = getTraceSummary(traceResult);
return {
lcp: summary.largestContentfulPaint, // 最大内容绘制
fcp: summary.firstContentfulPaint, // 首次内容绘制
tbt: summary.totalBlockingTime, // 总阻塞时间
cls: summary.cumulativeLayoutShift, // 累积布局偏移
longTasks: summary.longTasks // 长任务列表
};
}
3. performance_analyze_insight - 深度洞察
分析特定性能指标并提供优化建议
4.4 网络调试(2 个工具)
1. list_network_requests - 请求列表与过滤
defineTool({
name: 'list_network_requests',
schema: {
resourceType: z.enum(['document', 'stylesheet', 'script', 'fetch', 'xhr', 'image']).optional(),
page: z.number().optional(),
perPage: z.number().optional()
},
handler: async (request, response, context) => {
const allRequests = context.getNetworkRequests();
// 过滤
let filtered = allRequests;
if (request.params.resourceType) {
filtered = filtered.filter(r => r.resourceType() === request.params.resourceType);
}
// 分页
const page = request.params.page || 1;
const perPage = request.params.perPage || 20;
const start = (page - 1) * perPage;
const end = start + perPage;
const paginated = filtered.slice(start, end);
response.appendResponseLine(`Network Requests (${start + 1}-${end} of ${filtered.length})`);
response.includeNetworkRequests(paginated);
}
});
2. get_network_request - 请求详情与响应体
获取特定请求的详细信息,包括响应体
4.5 调试工具(5 个工具)
1. take_snapshot - 页面快照
生成可访问性树快照
2. take_screenshot - 截图
截取页面或元素的截图:
defineTool({
name: 'take_screenshot',
schema: {
uid: z.string().optional(),
fullPage: z.boolean().optional(),
type: z.enum(['png', 'jpeg']).optional()
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
let screenshot: Buffer;
if (request.params.uid) {
// 截取特定元素
const element = await context.getElementByUid(request.params.uid);
screenshot = await element.screenshot({
type: request.params.type || 'png'
});
} else {
// 截取整个页面
screenshot = await page.screenshot({
fullPage: request.params.fullPage || false,
type: request.params.type || 'png'
});
}
// 保存或返回 base64
const base64 = screenshot.toString('base64');
response.addImage(base64, request.params.type || 'png');
}
});
3. evaluate_script - 脚本执行
在页面上下文中执行 JavaScript:
defineTool({
name: 'evaluate_script',
schema: {
script: z.string()
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
const result = await page.evaluate(request.params.script);
response.appendResponseLine(`Result: ${JSON.stringify(result, null, 2)}`);
}
});
4. list_console_messages / get_console_message - 控制台
列出控制台消息或获取特定消息详情
4.6 模拟(2 个工具)
1. emulate - 网络与 CPU 节流
模拟慢速网络或低性能 CPU:
defineTool({
name: 'emulate',
schema: {
networkConditions: z.enum(['Fast 4G', 'Slow 4G', 'Fast 3G', 'Slow 3G']).optional(),
cpuThrottling: z.number().optional() // 4 表示 4 倍慢速
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
if (request.params.networkConditions) {
await context.setNetworkConditions(page, request.params.networkConditions);
}
if (request.params.cpuThrottling) {
await context.setCPUThrottling(page, request.params.cpuThrottling);
}
response.appendResponseLine('Emulation settings applied');
}
});
2. resize_page - 视口调整
调整页面视口大小,模拟不同设备:
defineTool({
name: 'resize_page',
schema: {
width: z.number(),
height: z.number(),
deviceScaleFactor: z.number().optional()
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
await page.setViewport({
width: request.params.width,
height: request.params.height,
deviceScaleFactor: request.params.deviceScaleFactor || 1
});
response.appendResponseLine(`Viewport resized to ${request.params.width}x${request.params.height}`);
}
});
常用设备预设:
- iPhone 12: 390x844, deviceScaleFactor=3
- iPad: 768x1024, deviceScaleFactor=2
- Desktop HD: 1920x1080, deviceScaleFactor=1
总结
本文详细剖析了 chrome-devtools-mcp 项目的核心技术实现:
第三部分 - 核心技术实现:
- 3.1 MCP 服务器的生命周期管理
- 3.2 浏览器连接管理的艺术
- 3.3 McpContext 状态管理核心
- 3.4 基于可访问性树的创新快照系统
- 3.5 事件收集器模式的精妙设计
- 3.6 响应构建器的延迟执行策略
第四部分 - 26 个工具解析:
- 4.1 输入自动化 (8 个工具)
- 4.2 导航自动化 (6 个工具)
- 4.3 性能分析 (3 个工具)
- 4.4 网络调试 (2 个工具)
- 4.5 调试工具 (5 个工具)
- 4.6 模拟 (2 个工具)
这 26 个工具构成了一个完整的浏览器自动化生态系统,让 AI 能够像人类开发者一样操作浏览器、分析性能、调试问题。
让 AI 驾驭浏览器:Chrome DevTools MCP 实战指南(中),会分享第五~八部分的内容,敬请期待吧。 有问题可以私信讨论。