FEATURED · 精选文章

基于Chrome DevTools MCP协议构建AI驱动的浏览器自动化测试工具

发布时间 / 2026/8/12 17:21:13
来源 / 创域科博编辑部
栏目 / 资讯中心
基于Chrome DevTools MCP协议构建AI驱动的浏览器自动化测试工具 1. 项目概述当AI遇见浏览器开发者工具最近在折腾AI驱动的自动化测试我发现了一个非常有意思的“连接器”——Chrome DevTools MCP。简单来说它就像给AI Agent装上了一双能直接“看”和“操作”浏览器内部的眼睛和手。传统的自动化测试无论是用Selenium还是Playwright我们都需要写大量的脚本去模拟点击、输入、断言。但现在我们可以让AI比如Claude、Cursor里的Codex或者通过Spring AI集成的模型直接理解我们的自然语言指令然后通过这个MCP协议去操控浏览器完成测试任务。这不仅仅是“自动化”更是“智能化”的测试探索。这个项目的核心就是研究如何利用Chrome DevTools ProtocolCDP构建一个MCPModel Context Protocol服务器。MCP是Anthropic提出的一种协议旨在为大模型提供一个标准化的方式来访问外部工具和数据。而CDP是Chrome浏览器暴露给外部的调试协议能让我们以编程方式控制浏览器标签页、检查DOM、执行JavaScript、监听网络请求等等。将两者结合意味着我们可以创建一个“翻译官”把AI的自然语言指令如“检查登录按钮是否存在并点击”翻译成CDP能理解的命令并执行。这对于快速构建原型、探索性测试、或者处理那些UI结构复杂、传统脚本难以维护的场景潜力巨大。无论你是测试开发工程师还是对AI应用落地方向感兴趣的开发者这个组合都值得深入把玩一下。2. 核心思路与技术选型解析2.1 为什么是MCP CDP而不是直接调用Playwright首先得搞清楚我们为什么要绕这个弯。Playwright、Selenium这些成熟的自动化框架已经封装得很好API友好社区强大。直接让AI调用它们的库不行吗当然可以但这存在几个问题上下文与状态管理复杂AI在生成代码时需要理解整个测试脚本的上下文如页面对象、变量状态。一旦执行出错或页面状态变化AI很难进行实时调整和恢复。而MCP Server可以维护一个持久的浏览器会话状态AI只需发送指令由Server负责执行和状态保持。安全性直接让AI模型生成并执行任意代码即使是测试代码存在安全风险。MCP Server作为一个中间层可以严格限制AI能执行的操作范围比如只允许特定的CDP命令防止其执行危险的系统命令。工具标准化与复用MCP正在成为一种事实上的标准越来越多的AI平台如Claude Desktop、Cursor原生支持MCP。构建一个CDP的MCP Server就等于为你所有的AI工作流添加了一个强大的浏览器操控能力这个能力可以在不同的AI工具间复用。实时交互与调试通过MCP我们可以实现更接近“对话式”的测试。你可以对AI说“刚才点击后页面好像卡住了看看网络面板里有没有失败的请求” AI通过MCP查询CDP的网络数据然后给你分析结果。这种动态的、基于上下文的交互是静态脚本难以实现的。因此选择MCPCDP目标不是替代Playwright而是开辟一条AI-Native的测试交互路径特别适合快速验证想法、辅助编写复杂测试用例、或者进行智能的探索性测试。2.2 技术栈的权衡与确定要实现这个想法我们需要搭建几个部分MCP Server核心这是我们需要自己实现的部分。它需要实现MCP协议处理来自AI客户端的请求如tools/call。内部集成一个CDP客户端我们称之为CDP驱动层用于连接和控制真实的Chrome或Chromium浏览器。将AI的意图“翻译”成一系列CDP命令序列。我选择使用Node.js来实现。原因有三一是CDP官方有完善的Node.js客户端库chrome-remote-interface二是Node.js事件驱动、非阻塞I/O的特性很适合处理MCP的异步请求和CDP的事件流三是生态丰富便于快速开发。CDP驱动层选用chrome-remote-interface这个库。它是对CDP WebSocket接口的封装让我们能用更优雅的Promise风格API来调用诸如Page.navigate、DOM.getDocument、Runtime.evaluate等方法。比直接操作WebSocket要省心得多。AI客户端/平台这是发出指令的一端。可以是Claude Desktop安装后配置本地MCP Server即可。Cursor最新版本支持配置MCP Server让它的AI助手Codex获得新能力。任何实现了MCP Client的AI应用或自己写的客户端脚本。浏览器实例需要启动一个支持远程调试的Chrome/Chromium。通常通过--remote-debugging-port9222这样的命令行参数启动。整个架构的流程是AI客户端将用户指令自然语言通过MCP协议发送给我们自建的ServerServer解析指令通过CDP驱动层向浏览器发送命令浏览器执行后返回结果Server再将结果格式化成MCP要求的格式返回给AI客户端呈现给用户。3. 构建Chrome DevTools MCP Server详解3.1 项目初始化与核心依赖安装首先我们创建一个新的Node.js项目并安装核心依赖。mkdir chrome-devtools-mcp-server cd chrome-devtools-mcp-server npm init -y npm install modelcontextprotocol/sdk chrome-remote-interface ws npm install --save-dev typescript types/node ts-node这里解释一下几个包的作用modelcontextprotocol/sdkAnthropic官方提供的MCP Server SDK它帮我们处理了协议层面的通信、序列化、工具定义等繁琐工作让我们专注于业务逻辑。chrome-remote-interface前面提到的CDP客户端库是我们与浏览器对话的桥梁。wsWebSocket库MCP协议基于SSE或WebSocketSDK内部可能需要。因为我们使用TypeScript以获得更好的类型提示和开发体验所以需要安装TypeScript相关依赖并初始化配置。npx tsc --init在生成的tsconfig.json中确保设置target: ES2020,module: commonjs,outDir: ./dist等。3.2 MCP Server骨架与工具定义MCP Server的核心是向AI客户端声明自己有哪些“工具”Tools。每个工具对应一个AI可以调用的功能。我们先定义几个最基础、最关键的工具。创建一个src/server.ts文件import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema } from modelcontextprotocol/sdk/types.js; import CDP from chrome-remote-interface; // 1. 创建Server实例 const server new Server( { name: chrome-devtools-mcp-server, version: 0.1.0, }, { capabilities: { tools: {}, // 声明我们支持工具 }, } ); // 2. 全局变量用于保存CDP客户端实例和浏览器目标 let cdpClient: CDP.Api | null null; let target: CDP.Target.TargetInfo | null null; // 3. 定义工具列表 server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: navigate_to_url, description: Navigate the current browser tab to a specified URL. This is the starting point for most web interactions., inputSchema: { type: object, properties: { url: { type: string, description: The full URL to navigate to (e.g., https://example.com)., }, }, required: [url], }, }, { name: get_page_content, description: Get the outer HTML of the entire document or a specific element by CSS selector. Useful for understanding the current page structure., inputSchema: { type: object, properties: { selector: { type: string, description: Optional CSS selector. If provided, returns the outer HTML of that element. If omitted, returns the entire document HTML., }, }, required: [], }, }, { name: click_element, description: Simulate a mouse click on an element identified by a CSS selector. Will scroll the element into view if needed., inputSchema: { type: object, properties: { selector: { type: string, description: CSS selector to uniquely identify the target element., }, waitForNavigation: { type: boolean, description: Whether to wait for page navigation after clicking (e.g., for links). Defaults to false., default: false, }, }, required: [selector], }, }, { name: type_text, description: Focus on an input element (by selector) and type the provided text into it., inputSchema: { type: object, properties: { selector: { type: string, description: CSS selector for the input, textarea, or contenteditable element., }, text: { type: string, description: The text to type into the element., }, clear: { type: boolean, description: Whether to clear the existing content before typing. Defaults to true., default: true, }, }, required: [selector, text], }, }, { name: execute_javascript, description: Execute arbitrary JavaScript in the context of the current page and return the result. Use with caution., inputSchema: { type: object, properties: { expression: { type: string, description: The JavaScript expression or function to execute. Return statements are handled automatically., }, }, required: [expression], }, }, ], }; });注意工具的描述description至关重要。AI模型如Claude主要依靠这些描述来理解何时以及如何使用该工具。描述应清晰、具体说明工具的用途、输入参数的意义和典型使用场景。好的描述能极大提升AI调用的准确性。3.3 CDP连接管理与工具实现逻辑定义了工具接下来要实现连接浏览器和处理工具调用的逻辑。我们需要一个函数来建立CDP连接这个连接应该在Server启动时或第一次需要时建立。// 4. 连接到Chrome DevTools Protocol async function connectToBrowser() { if (cdpClient) { return cdpClient; } try { // 查找可用的浏览器目标。默认假设Chrome在localhost:9222运行 const targets await CDP.List(); // 通常我们选择第一个可用的页面目标type: page target targets.find(t t.type page) || targets[0]; if (!target) { throw new Error(No available browser target found. Make sure Chrome is running with --remote-debugging-port9222); } // 连接到该目标 cdpClient await CDP({ target }); // 启用必要的Domain以便调用其方法 const { Page, Runtime, DOM, Input } cdpClient; await Promise.all([Page.enable(), Runtime.enable(), DOM.enable(), Input.enable()]); console.error(MCP Server: Connected to browser successfully.); return cdpClient; } catch (error) { console.error(MCP Server: Failed to connect to browser:, error); cdpClient null; target null; throw error; } } // 5. 处理工具调用请求 server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; const client await connectToBrowser(); // 确保已连接 const { Page, DOM, Runtime, Input } client; try { switch (name) { case navigate_to_url: { const { url } args as { url: string }; await Page.navigate({ url }); // 等待页面加载完成的一个简单标志 await Page.loadEventFired(); return { content: [ { type: text, text: Successfully navigated to ${url}, }, ], }; } case get_page_content: { const { selector } args as { selector?: string }; if (selector) { // 获取特定元素 const { root } await DOM.getDocument({ depth: -1 }); const { nodeId } await DOM.querySelector({ nodeId: root.nodeId, selector }); if (!nodeId) { return { content: [{ type: text, text: Element with selector ${selector} not found. }] }; } const { outerHTML } await DOM.getOuterHTML({ nodeId }); return { content: [{ type: text, text: outerHTML }], }; } else { // 获取整个文档 const { root } await DOM.getDocument({ depth: -1 }); const { outerHTML } await DOM.getOuterHTML({ nodeId: root.nodeId }); return { content: [{ type: text, text: outerHTML }], }; } } case click_element: { const { selector, waitForNavigation false } args as { selector: string; waitForNavigation?: boolean }; const { root } await DOM.getDocument({ depth: -1 }); const { nodeId } await DOM.querySelector({ nodeId: root.nodeId, selector }); if (!nodeId) { throw new Error(Element with selector ${selector} not found.); } // 获取元素的边界框用于计算点击坐标 const { model } await DOM.getBoxModel({ nodeId }); if (!model) { throw new Error(Could not get box model for element ${selector}.); } const centerX model.content[0] model.width / 2; const centerY model.content[1] model.height / 2; // 使用Input Domain模拟鼠标点击 await Input.dispatchMouseEvent({ type: mousePressed, button: left, x: centerX, y: centerY, clickCount: 1, }); await Input.dispatchMouseEvent({ type: mouseReleased, button: left, x: centerX, y: centerY, clickCount: 1, }); if (waitForNavigation) { await Page.loadEventFired(); } return { content: [{ type: text, text: Successfully clicked element: ${selector} }], }; } case type_text: { const { selector, text, clear true } args as { selector: string; text: string; clear?: boolean }; const { root } await DOM.getDocument({ depth: -1 }); const { nodeId } await DOM.querySelector({ nodeId: root.nodeId, selector }); if (!nodeId) { throw new Error(Input element with selector ${selector} not found.); } // 先聚焦元素 await DOM.focus({ nodeId }); if (clear) { // 简单的清空方式选中全部内容并输入空字符然后删除 await Input.dispatchKeyEvent({ type: keyDown, key: Meta }); // Mac用MetaWindows用Control await Input.dispatchKeyEvent({ type: keyDown, key: A }); await Input.dispatchKeyEvent({ type: keyUp, key: A }); await Input.dispatchKeyEvent({ type: keyUp, key: Meta }); await Input.dispatchKeyEvent({ type: keyDown, key: Backspace }); await Input.dispatchKeyEvent({ type: keyUp, key: Backspace }); } // 模拟输入每个字符 for (const char of text) { await Input.dispatchKeyEvent({ type: keyDown, text: char }); await Input.dispatchKeyEvent({ type: keyUp, text: char }); // 可以添加微小延迟以模拟真人输入避免某些网站检测 await new Promise(resolve setTimeout(resolve, 50)); } return { content: [{ type: text, text: Typed ${text} into element: ${selector} }], }; } case execute_javascript: { const { expression } args as { expression: string }; const result await Runtime.evaluate({ expression, returnByValue: true }); if (result.exceptionDetails) { throw new Error(JS Execution Error: ${JSON.stringify(result.exceptionDetails)}); } return { content: [{ type: text, text: Result: ${JSON.stringify(result.result.value)} }], }; } default: throw new Error(Unknown tool: ${name}); } } catch (error) { console.error(MCP Server: Error executing tool ${name}:, error); return { content: [ { type: text, text: Error: ${error instanceof Error ? error.message : String(error)}, }, ], isError: true, }; } });3.4 启动服务器与配置AI客户端最后我们需要启动这个MCP Server它通常通过标准输入输出stdio与AI客户端通信。// 6. 启动Server async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP Server: Chrome DevTools MCP Server is running on stdio.); } main().catch((error) { console.error(MCP Server: Fatal error:, error); process.exit(1); });将TypeScript编译成JavaScript并运行npx tsc node dist/server.js现在Server已经在后台运行等待AI客户端的连接。接下来需要配置你的AI客户端。以Claude Desktop为例找到Claude Desktop的配置文件位置macOS通常在~/Library/Application Support/Claude/claude_desktop_config.jsonWindows在%APPDATA%\Claude\claude_desktop_config.json。在配置文件中添加MCP Server配置{ mcpServers: { chrome-devtools: { command: node, args: [/ABSOLUTE/PATH/TO/YOUR/chrome-devtools-mcp-server/dist/server.js] } } }重启Claude Desktop。现在当你和Claude聊天时它就能“看到”并使用navigate_to_url、click_element等工具了。以Cursor为例在Cursor中打开设置Cmd,或Ctrl,。找到MCP Servers配置部分。添加一个新的Server配置指定命令和路径类似于上面的配置。重启CursorCodex助手便具备了浏览器控制能力。4. 进阶功能与测试场景实战4.1 扩展更多实用工具基础的导航、点击、输入是骨架。要让这个MCP Server真正强大我们需要添加更多面向测试的“肌肉”。截图工具用于视觉验证或记录测试步骤。// 在工具列表中添加 { name: take_screenshot, description: Capture a screenshot of the current viewport or a specific element. Returns the image as a base64 string., inputSchema: { type: object, properties: { selector: { type: string, description: Optional CSS selector. If provided, captures only that element. }, fullPage: { type: boolean, description: Capture the full scrollable page. May be slow for long pages., default: false }, }, required: [], }, }实现逻辑调用Page.captureScreenshot方法。对于元素截图需要先获取其边界框然后设置clip参数。网络请求监听工具性能测试或API监控的关键。{ name: monitor_network, description: Start monitoring network requests. Returns a summary of requests (URL, status, method, type) made after this call until stopped., inputSchema: { type: object, properties: { durationMs: { type: number, description: How long to monitor for, in milliseconds. Defaults to 5000 (5 seconds)., default: 5000 }, filterUrl: { type: string, description: Optional string to filter requests by URL containing this text. }, }, required: [], }, }实现逻辑调用Network.enable()监听Network.requestWillBeSent和Network.responseReceived事件将数据收集到数组中在指定时间后返回汇总信息。获取控制台日志工具捕获JavaScript错误或console.log输出。{ name: get_console_logs, description: Retrieve browser console logs (including errors, warnings, logs) since the last call or for a specified duration., inputSchema: { type: object, properties: { clearPrevious: { type: boolean, description: Clear stored logs before starting new collection., default: true }, }, required: [], }, }实现逻辑调用Runtime.enable()并监听Runtime.consoleAPICalled和Runtime.exceptionThrown事件将日志存储在Server的内存中供查询。元素状态检查工具比获取HTML更精确地检查元素属性。{ name: inspect_element, description: Get detailed properties of an element: visibility, enabled state, computed styles, attributes, inner text., inputSchema: { type: object, properties: { selector: { type: string, description: CSS selector for the target element. }, }, required: [selector], }, }实现逻辑结合DOM.getAttributes、DOM.resolveNode后调用Runtime.callFunctionOn执行getComputedStyle以及检查offsetParent判断可见性。4.2 构建端到端测试场景示例假设我们要测试一个简单的登录流程。现在你可以直接对AI如Claude说“帮我测试一下登录功能。打开网站https://demo.testfire.net找到登录链接并点击在用户名输入框里输入admin在密码框里输入admin然后点击登录按钮。登录成功后检查页面上是否显示了‘Congratulations’这个词。”AI会理解你的意图并规划调用一系列MCP工具navigate_to_url-https://demo.testfire.netclick_element-selector: a[href*login](假设登录链接的CSS选择器)type_text-selector: #uid, text: admintype_text-selector: #passw, text: adminclick_element-selector: input[namebtnSubmit], waitForNavigation: trueexecute_javascript-expression: document.body.innerText.includes(Congratulations) ? Login success text found. : Login success text NOT found.AI会按顺序执行这些工具并将每个步骤的结果反馈给你。如果某个步骤失败比如元素没找到AI可以根据错误信息尝试其他选择器或者向你请求进一步指示。这就构成了一个动态的、可交互的测试会话。4.3 与现有测试框架的集成思路你可能会问这和我用Playwright写脚本有什么区别核心区别在于交互模式和智能程度。MCP Server可以成为你现有测试框架的“智能副驾驶”。用例生成你可以让AI分析一个用户故事User Story然后自动生成一系列MCP工具调用序列你可以将其保存为JSON或脚本这就是一个自动化测试用例的雏形。脚本修复当你的Playwright脚本因为UI变化而失败时你可以把错误信息和当前页面HTML丢给AI让它通过MCP Server去探索页面找出新的正确选择器甚至直接生成修复后的代码片段。探索性测试助手在手动探索性测试时你可以随时让AI帮你执行一些重复操作“把所有商品都加入购物车”或者检查一些不易察觉的状态“看看控制台有没有404错误”。5. 避坑指南与性能优化在实际搭建和使用过程中我踩过不少坑这里总结几个关键点5.1 连接稳定性与错误处理浏览器进程管理我们的Server假设浏览器已经以调试模式打开。在生产环境中你需要用child_process模块来自动启动和关闭浏览器进程并确保端口不被占用。记得在Server关闭时也清理浏览器进程。import { spawn } from child_process; let browserProcess: ChildProcess; async function launchBrowser() { browserProcess spawn(google-chrome, [ --remote-debugging-port9222, --no-first-run, --no-default-browser-check, --disable-sync, --disable-extensions, --disable-default-apps, --headlessnew // 无头模式适合CI环境 ]); // 等待CDP端口就绪 await new Promise(resolve setTimeout(resolve, 3000)); }CDP会话超时与重连网络波动或浏览器卡顿可能导致CDP连接断开。必须在Server层实现重连机制。可以为cdpClient添加onclose事件监听一旦断开就尝试重新连接并重置内部状态。工具调用的原子性与超时每个工具调用都应该有超时设置防止某个CDP命令如等待导航无限期挂起。可以使用Promise.race包装CDP调用。5.2 选择器的鲁棒性与AI提示AI并不总是知道最佳选择器当你对AI说“点击登录按钮”AI可能会尝试button、.btn-login、#submit等多种选择器。为了提高成功率我们可以在工具描述中给出提示或者实现一个“智能查找”工具。实现一个find_element工具这个工具接收元素描述如“登录按钮”、“搜索框”内部通过执行JavaScript综合评估元素的文本内容、aria-label、placeholder、type、className、tagName等多种属性计算出一个最可能匹配的CSS选择器或XPath返回。这比让AI盲目猜测要可靠得多。优先使用>
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻