让 AI 驾驭浏览器:Chrome DevTools MCP 实战指南(上)

185 阅读23分钟

深入解析 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 访问数据库、文件系统或浏览器,通常有两种做法:

  1. 函数调用(Function Calling):在每次对话时,将所有可用的工具描述传递给 LLM,让它选择调用哪个工具。这种方式的问题是,每次都要传递工具描述会消耗大量的 token,而且工具之间的状态管理非常困难。
  2. 定制化集成:为每个 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 的设计遵循几个核心理念:

  1. 协议优于实现:MCP 只定义通信协议,不限制具体的实现方式。你可以用 TypeScript、Python、Rust 或任何其他语言实现 MCP Server。

  2. 简单至上:MCP 的通信机制默认使用 stdio(标准输入/输出),这是最简单、最可靠的进程间通信方式。不需要配置网络端口、不需要处理认证,只需要启动一个子进程。

  3. 类型安全:MCP 使用 JSON Schema 定义工具的参数和返回值,确保类型安全和自动验证。

  4. 可扩展性: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?

这个问题的答案在于 设计目标的不同

  1. 传统 API 是为人类开发者设计的

    • API 文档需要人类阅读和理解
    • 参数命名和结构是为人类直觉优化的
    • 错误处理依赖人类的判断和重试逻辑
  2. 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,进行浏览器自动化、性能分析和调试

这个项目的核心价值在于:

  1. 降低浏览器自动化的门槛

    • 用户不需要编写 Puppeteer 或 Playwright 脚本,只需要用自然语言告诉 AI:"打开这个网页,点击登录按钮,然后截图"
    • AI 会自动选择合适的 MCP 工具(navigate_page、click、take_screenshot),处理等待时机、元素定位等细节
  2. 让性能分析变得智能

    • 传统方式:手动启动 Performance 录制 → 操作页面 → 停止录制 → 分析 Trace 文件 → 寻找瓶颈
    • MCP 方式:告诉 AI "分析这个页面的加载性能",AI 会自动完成录制、分析,并用人类语言解释发现的问题
  3. 实现跨工具的工作流自动化

    • 可以将浏览器操作与其他 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 工具,覆盖了浏览器自动化的各个方面:

  1. 输入自动化(8 个工具)

    • click - 点击元素
    • fill / fill_form - 表单填充
    • drag - 拖拽操作
    • hover - 悬停
    • press_key - 键盘输入
    • upload_file - 文件上传
    • handle_dialog - 对话框处理
  2. 导航自动化(6 个工具)

    • navigate_page - 页面导航
    • new_page / close_page - 页面生命周期管理
    • list_pages / select_page - 多页面管理
    • wait_for - 条件等待
  3. 性能分析(3 个工具)

    • performance_start_trace - 开始性能追踪
    • performance_stop_trace - 停止追踪并分析
    • performance_analyze_insight - 深度性能洞察
  4. 网络调试(2 个工具)

    • list_network_requests - 列出网络请求
    • get_network_request - 获取请求详情
  5. 调试工具(5 个工具)

    • take_snapshot - 页面结构快照
    • take_screenshot - 截图
    • evaluate_script - 执行 JavaScript
    • list_console_messages / get_console_message - 控制台消息
  6. 模拟(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 调试接口                                        │
└─────────────────────────────────────────────────────────────┘

这个架构的精妙之处在于:

  1. 上层不依赖下层的具体实现:工具层不需要知道响应是如何构建的,响应层不需要知道浏览器是如何连接的
  2. 每一层都有明确的职责:工具层负责业务逻辑,响应层负责格式化,上下文层负责状态管理,浏览器层负责底层通信
  3. 易于测试和维护:每一层都可以独立测试,修改一层不会影响其他层

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);

整个启动流程体现了几个关键设计理念:

  1. 延迟初始化浏览器: 注意到在 MCP Server 启动时,并没有立即启动或连接浏览器。浏览器的初始化被延迟到第一次工具调用时,这样做的好处是:

    • MCP Server 启动速度更快 (几十毫秒内完成)
    • 避免了不必要的资源占用 (如果用户只是查询工具列表)
    • 提供了更好的容错能力 (浏览器连接失败不会导致 MCP Server 无法启动)
  2. 模块化的工具组织: 所有工具按照功能类别分组在不同的模块中,每个模块负责特定领域的能力。这种组织方式使得:

    • 新增工具时只需要在对应模块中添加
    • 工具之间相互独立,不会产生耦合
    • 便于按类别进行测试和文档生成

互斥锁保证工具调用的原子性

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;
}

这个函数的巧妙之处在于:

  1. 首次调用时初始化: 第一次调用工具时,contextundefined,会触发浏览器的启动或连接
  2. 后续调用复用: 后续工具调用会直接返回已存在的 context,不会重复初始化
  3. 支持浏览器切换: 如果检测到浏览器实例变化 (比如浏览器崩溃后重启),会自动创建新的 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;
}

这个函数的亮点:

  1. 智能的用户数据目录管理:

    • 默认使用 ~/.cache/chrome-devtools-mcp/ 目录
    • 支持多 Channel 隔离 (stable, beta, dev, canary 各自独立)
    • 可以通过 isolated 参数禁用持久化 (每次启动全新环境)
  2. 目标过滤器: 通过 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;
  };
}
  1. 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://...)

这样设计的原因:

  1. 避免混淆: AI 不应该看到和操作 Chrome 的内部页面
  2. 安全性: 防止 AI 修改浏览器设置或访问敏感的扩展数据
  3. 性能: 减少需要管理的 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)

这种设计的好处:

  1. 持久化数据: Cookies、LocalStorage、IndexedDB 等数据会被保留,避免重复登录
  2. 多版本共存: 不同 Channel 的数据互不干扰
  3. 缓存加速: 缓存的资源可以复用,加快页面加载速度
  4. 可选的隔离模式: 通过 --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?

  1. AI 友好: 可访问性树是语义化的,包含元素的角色 (role)、名称 (name)、值 (value) 等信息,AI 更容易理解
  2. 结构清晰: 相比 DOM,可访问性树过滤掉了布局和样式元素,只保留了有意义的交互元素
  3. 性能优秀: 可访问性树比完整 DOM 小得多,生成和传输速度更快
  4. 标准化: 基于 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);
  }
}

网络请求收集的特点:

  1. 跨导航保留: 当页面导航到新 URL 时,之前的请求不会丢失 (保留最近 3 次导航)
  2. 稳定 ID: 每个请求都有一个全局唯一的 ID (格式: req-<navigation>-<index>)
  3. 响应关联: 使用 WeakMap 关联请求和响应,避免内存泄漏
  4. 失败标记: 失败的请求会被特别标记

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 树存在几个问题:

  1. 噪音太多: DOM 包含大量的布局和样式元素 (<div>, <span> 等),这些元素对交互没有实际意义
  2. 缺乏语义: DOM 元素的名称 (标签名) 不能表达其功能,AI 难以理解元素的作用
  3. 体积庞大: 一个复杂页面的 DOM 树可能包含数千个节点,传输和处理成本高
  4. 不统一: 不同网站的 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 系统的特点:

  1. 层级化: ID 反映了元素在树中的位置

    1           # 根节点
    1-1         # 第一个子节点
    1-1-1       # 第一个子节点的第一个子节点
    1-1-2       # 第一个子节点的第二个子节点
    1-2         # 第二个子节点
    
  2. 相对稳定: 只要页面结构不变,ID 就不变

    • 有利于 AI 进行多步操作 (先快照,然后引用 ID 操作)
    • 有利于调试 (相同元素在不同快照中有相同的 ID 结构)
  3. 全局唯一: 使用计数器确保每个快照中的 ID 都是唯一的

  4. 易于解析: 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 同步的应用场景:

  1. 辅助调试: 用户在 DevTools 中选中一个元素,AI 可以看到并理解用户的意图

    用户: "为什么这个按钮点不了?"
    [用户在 DevTools 中选中了该按钮]
    AI: "我看到你选中了 UID 为 1-3-5 的按钮,让我检查一下它的状态..."
    
  2. 快速定位: 用户可以通过 DevTools 选中元素,然后让 AI 对其进行操作

    用户: "点击我选中的这个按钮"
    [AI 从快照中读取 selectedElementUid,然后执行点击]
    
  3. 学习用户意图: 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);
  }
}

关键设计点解析:

  1. 泛型设计: PageCollector<T> 可以收集任何类型的事件,代码复用性强

  2. WeakMap 存储: 使用 WeakMap 而不是 Map,当页面被销毁时,相关数据会自动被垃圾回收,避免内存泄漏

  3. 多层数组结构: Array<Array<T>> 表示多次导航的历史

    [
      [req1, req2, req3], // 当前导航的请求
      [req4, req5], // 上次导航的请求
      [req6, req7, req8] // 上上次导航的请求
    ];
    
  4. 导航感知: 监听 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?

  1. 避免冲突: HTTPRequest 对象可能已经有 id 属性,使用 Symbol 确保不会覆盖
  2. 隐藏实现: Symbol 属性不会在 JSON 序列化中出现,不会污染输出
  3. 类型安全: 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 的特殊之处:

  1. 三态管理: 区分 pending (进行中)、success (成功)、failed (失败) 三种状态

  2. 响应关联: 使用 WeakMap 关联请求和响应

    • 为什么是 WeakMap? 因为 HTTPRequest 对象可能很大,WeakMap 允许垃圾回收
    • 为什么是 null? null 表示请求失败了 (没有响应),undefined 表示还没完成
  3. 失败标记: 使用 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');
  }
}

延迟计算的优势

  1. 性能优化: 快照和网络请求数据只有在真正需要时才生成
  2. 灵活性: 工具可以先决定要包含什么,最后统一构建
  3. 一致性: 所有数据在同一时刻生成,保证状态一致

多格式内容支持

响应构建器支持多种内容类型:

  • 纯文本
  • 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 实战指南(中),会分享第五~八部分的内容,敬请期待吧。 有问题可以私信讨论。