基于MCP协议实现Playwright自动化与手动操作的无缝协同
1. 项目概述当自动化脚本遇上你的手动操作在浏览器自动化领域我们常常面临一个尴尬的境地精心编写的Playwright脚本在后台勤勤恳恳地执行任务而你作为开发者或测试人员却想在同一时间、同一个浏览器实例里手动点点看看验证某个交互或者临时调试一个元素。结果呢要么是脚本运行完自动关闭了浏览器你想接着用还得重新打开要么是手动操作干扰了脚本导致定位失败或状态混乱。这种“自动化”与“手动”之间的壁垒不仅降低了效率也让调试和探索性测试变得异常别扭。“突破浏览器会话壁垒”这个项目正是为了解决这个痛点。它的核心是利用MCPModel Context Protocol协议构建一个Playwright扩展实现自动化脚本与人工操作在同一浏览器会话中的无缝协同。简单来说就是让Playwright脚本控制的浏览器变成一个“共享工作台”脚本可以随时将控制权“交还”给你你操作完毕后脚本又能无缝接替继续执行后续流程。这不再是简单的“录制与回放”而是一种动态的、双向的、上下文感知的协作模式。这个项目适合所有使用Playwright进行Web自动化开发、测试、数据抓取或RPA流程构建的从业者。无论你是想提升调试效率构建更灵活的人机协同工作流比如与n8n等自动化平台集成还是探索AI智能体如Claude Code与真实浏览器的交互这个扩展都能为你打开一扇新的大门。接下来我将从设计思路到代码实现完整拆解如何构建这样一个工具并分享在实际集成中踩过的坑和总结的经验。2. 核心设计思路与架构选型要实现自动化与手动的无缝协同我们不能简单粗暴地让两个进程同时向浏览器发送指令那必然会导致冲突。核心思路是状态管理与控制权移交。我们需要一个中心化的“协调者”来管理浏览器会话的状态并决定当前是自动化脚本掌权还是手动操作介入。2.1 为什么选择MCP协议作为协调层MCP协议是近年来在AI智能体开发生态中兴起的一个标准它定义了AI模型与外部工具服务器之间进行上下文交换和函数调用的方式。我们选择它基于以下几个关键考量标准化与通用性MCP提供了一套标准的请求/响应、资源发现和工具调用接口。这意味着我们的协同服务不仅可以被Playwright脚本调用未来也可以轻松集成到支持MCP的AI助手如Claude Desktop、Cursor等或任何遵循该协议的客户端中极大地扩展了应用场景。上下文管理MCP协议天然适合管理“会话”和“上下文”。我们可以将当前的浏览器页面状态、DOM快照、甚至是可操作的控件列表作为“资源”暴露给客户端。当手动操作者或AI助手需要介入时它能获得丰富的上下文信息而不是面对一个黑盒。双向通信与工具调用协议支持服务器向客户端提供“工具”函数。我们的Playwright扩展可以作为MCP服务器暴露出诸如pauseForManualControl、resumeAutomation、getCurrentPageSnapshot等工具。客户端可以是另一个脚本或人机交互界面通过调用这些工具来实现控制权的切换。生态融合趋势从网络热词可以看到MCP正在与各种开发工具VSCode、Figma、测试框架Playwright Test以及AI编码助手深度集成。基于MCP构建能使我们的项目更好地融入现代开发工作流具备前瞻性。2.2 系统架构拆解整个系统的架构可以分为三层浏览器层由Playwright驱动的Chromium、Firefox或WebKit浏览器实例。这是所有操作最终生效的场所。MCP服务器层核心扩展这是我们实现的重点。它是一个运行在Node.js环境下的服务主要职责包括封装Playwright实例启动并管理浏览器、上下文和页面。暴露MCP接口实现MCP协议规定的initialize、resources/list、tools/call等端点。状态机管理维护一个核心状态机状态包括AUTOMATING自动化进行中、MANUAL_PAUSED已暂停等待手动操作、MANUAL_IN_CONTROL手动控制中等。上下文同步在状态切换时负责捕获和恢复必要的页面状态如URL、Cookies、局部存储以减少手动操作后的恢复成本。客户端层可以是多种形态。自动化脚本客户端原有的Playwright测试脚本或自动化流程通过MCP客户端库与服务器通信发起自动化任务并在适当时机暂停。手动控制客户端可以是一个简单的命令行界面CLI一个Web控制面板或者直接是集成了MCP客户端的代码编辑器如VSCode 相关扩展。用户通过它发送“接管控制”或“恢复自动化”的指令。AI智能体客户端如Claude Code它可以读取服务器暴露的页面资源如HTML摘要并调用工具来执行一些自动化指令实现“AI指导下的半自动化”。这个架构的关键在于所有对浏览器的操作指令无论来自何方都必须通过MCP服务器这一单一入口进行路由和排队从而避免了竞争条件。注意关于“会话”的界定这里讨论的“浏览器会话壁垒”主要指的是自动化进程与手动操作进程之间的控制权隔离。对于需要保持登录状态的网站我们通常通过Playwright的browserContext来持久化cookies和localStorage这个“会话”在浏览器实例存活期间是持续的我们的MCP服务器会确保这个上下文在控制权切换时不被意外销毁。3. 核心细节解析与实操要点理解了架构我们深入核心细节。实现一个稳定可用的协同扩展以下几个环节至关重要。3.1 Playwright实例的生命周期管理服务器层必须可靠地管理Playwright实例。一个常见的错误是在每次工具调用时都启动新浏览器这无法实现会话共享。正确做法是采用单例模式管理核心对象// mcp-playwright-server.js import { chromium } from playwright; import { McpServer } from modelcontextprotocol/sdk/server/index.js; class PlaywrightSessionManager { constructor() { this.browser null; this.context null; this.page null; this.currentState IDLE; // IDLE, AUTOMATING, MANUAL_PAUSED this.controlLock null; // 用于控制权排队的锁 } async ensureBrowserLaunched() { if (!this.browser || !this.browser.isConnected()) { // 以带GUI的模式启动方便手动操作观看 this.browser await chromium.launch({ headless: false, args: [--start-maximized] // 最大化窗口体验更好 }); // 创建持久化上下文保存登录态 this.context await this.browser.newContext({ viewport: null, storageState: ./storage_state.json // 可选的持久化存储路径 }); } return { browser: this.browser, context: this.context }; } async getOrCreatePage() { const { context } await this.ensureBrowserLaunched(); if (!this.page) { this.page await context.newPage(); } // 检查页面是否已关闭 if (this.page.isClosed()) { this.page await context.newPage(); } return this.page; } }关键点解析headless: false这是实现“可手动操作”的基础。必须让浏览器窗口可见。storageState通过将上下文状态cookies, localStorage定期保存到文件并在初始化时加载可以实现在浏览器重启后仍保持登录会话这对于需要长时间协同的任务非常关键。状态检查isConnected()和isClosed()的检查是健壮性的保证能处理浏览器意外崩溃或页面被关闭的情况。3.2 MCP工具的设计与实现MCP服务器需要暴露一系列工具。以下是几个最核心的工具设计start_automation(工具): 客户端调用此工具来启动一段自动化流程。服务器收到请求后会将状态置为AUTOMATING并执行客户端发送过来的初始化脚本例如导航到某个URL并登录。pause_for_manual_control(工具): 这是协同的关键。当自动化脚本执行到需要人工介入的点例如验证一个复杂的图形验证码或确认一个非标准弹窗它调用此工具。服务器动作将状态改为MANUAL_PAUSED。这里有一个重要技巧不是简单地等待而是需要让Playwright脚本进入一个“空闲循环”或完全停止发送指令同时确保浏览器页面不被脚本的任何后续操作干扰。我们可以通过返回一个Promise该Promise只有在收到恢复信号时才被解决resolve来实现。上下文快照在暂停时可以自动调用capture_page_context工具将当前页面的URL、主要元素截图或可交互元素列表作为资源更新方便手动控制方了解现状。capture_page_context(工具/资源): 获取当前页面的关键信息。可以作为工具被调用也可以作为动态资源如resource://page/snapshot供客户端随时读取。// 示例获取页面结构化信息 async capturePageContext() { const page await this.getOrCreatePage(); const url page.url(); const title await page.title(); // 获取所有可交互元素的简要信息非完整DOM减轻负担 const interactiveElements await page.evaluate(() { const elements []; [button, a, input, select, textarea].forEach(selector { document.querySelectorAll(selector).forEach(el { elements.push({ tag: el.tagName, text: el.innerText?.substring(0, 50) || el.value || , id: el.id, classes: el.className }); }); }); return elements.slice(0, 50); // 限制数量 }); return { url, title, interactiveElements }; }resume_automation(工具): 手动操作完成后客户端调用此工具。服务器将状态改回AUTOMATING并释放之前pause_for_manual_control中阻塞的Promise让自动化脚本继续执行。execute_script(工具): 为了灵活性可以暴露一个通用的脚本执行工具。手动控制方或AI客户端可以通过它向当前页面注入一段JavaScript代码来执行操作。但必须极其小心安全问题仅应在可信环境中使用。3.3 控制权移交与状态同步的难点这是项目中最容易出bug的地方。主要难点和解决方案如下难点一竞态条件。手动操作者在点击同时自动化脚本的定时器触发了一个操作。解决方案引入一个简单的锁机制controlLock。在状态为MANUAL_PAUSED或MANUAL_IN_CONTROL时所有来自自动化脚本客户端的工具调用除了resume_automation都应被排队或直接拒绝。在服务器内部用一个标志位或Promise锁来实现。难点二页面状态漂移。手动操作可能改变了页面URL、打开了新标签页、或跳转了页面导致自动化脚本恢复后找不到之前的元素。解决方案状态恢复在pause_for_manual_control时记录当前页面的主要句柄page对象。在resume_automation时检查并尝试将焦点切换回该页面。如果页面已关闭则需要有恢复策略如重新导航到原URL。健壮的选择器自动化脚本应尽量使用基于属性>mkdir playwright-mcp-bridge cd playwright-mcp-bridge npm init -y npm install playwright modelcontextprotocol/sdkplaywright是浏览器自动化的核心。modelcontextprotocol/sdk是官方提供的MCP协议SDK它封装了协议通信的细节让我们专注于工具的实现。此外我们还需要安装Playwright的浏览器内核。npx playwright install chromium这里选择Chromium是因为其兼容性和性能最好。在实际生产中你可能需要根据测试需求安装firefox或webkit。4.2 构建MCP服务器骨架创建一个server.js文件作为我们服务器的入口。// server.js import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { PlaywrightSessionManager } from ./playwright-session.js; // 初始化MCP服务器和会话管理器 const server new McpServer({ name: playwright-mcp-bridge, version: 0.1.0, }); const sessionManager new PlaywrightSessionManager(); // 工具定义将在下一步添加 // server.setToolHandler(...) // 启动服务器使用标准输入输出传输便于通过CLI或进程调用 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Playwright MCP Server running on stdio...); } main().catch((error) { console.error(Server fatal error:, error); process.exit(1); });这个服务器使用StdioServerTransport意味着它通过标准输入/输出与客户端通信。这是一种非常通用的方式可以被任何能启动子进程并与之通信的客户端调用如另一个Node.js脚本、Python程序或集成开发环境。4.3 实现关键工具暂停与恢复让我们实现最核心的pause_for_manual_control和resume_automation工具。这需要跨请求保持状态我们使用一个Map来存储不同会话的暂停锁。// playwright-session.js (部分关键代码) export class PlaywrightSessionManager { constructor() { // ... 其他属性 this.pauseResolvers new Map(); // key: sessionId, value: { resolve, reject } } async pauseForManualControl(sessionId default) { if (this.currentState AUTOMATING) { this.currentState MANUAL_PAUSED; console.log([${sessionId}] Automation paused for manual control.); // 返回一个Promise其控制权交给外部 return new Promise((resolve, reject) { this.pauseResolvers.set(sessionId, { resolve, reject }); // 可选在这里自动触发一次上下文快照 // this.capturePageContext().then(snapshot { /* 更新资源 */ }); }); } else { throw new Error(Cannot pause in current state: ${this.currentState}); } } async resumeAutomation(sessionId default) { if (this.currentState MANUAL_PAUSED) { const resolver this.pauseResolvers.get(sessionId); if (resolver) { this.currentState AUTOMATING; console.log([${sessionId}] Resuming automation.); resolver.resolve(); // 解决之前创建的Promise让暂停的自动化脚本继续执行 this.pauseResolvers.delete(sessionId); } else { throw new Error(No pause found for session: ${sessionId}); } } else { throw new Error(Cannot resume in current state: ${this.currentState}); } } }在server.js中注册这两个工具// server.js (续) server.setToolHandler( pause_for_manual_control, { description: Pause the current automation script and yield control to manual operation., }, async (params, extra) { const sessionId extra.sessionId || default; // 这里直接调用返回的Promise会被MCP SDK正确处理 await sessionManager.pauseForManualControl(sessionId); return { content: [{ type: text, text: Session ${sessionId} is now paused. Take manual control., }], }; } ); server.setToolHandler( resume_automation, { description: Resume the automation script after manual control is finished., }, async (params, extra) { const sessionId extra.sessionId || default; await sessionManager.resumeAutomation(sessionId); return { content: [{ type: text, text: Session ${sessionId} automation resumed., }], }; } );4.4 编写一个协同演示脚本现在我们编写一个客户端自动化脚本它使用MCP客户端与我们的服务器通信。首先安装MCP客户端SDK并创建一个客户端脚本demo_automation.js。// demo_automation.js import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; async function runDemo() { // 1. 创建MCP客户端并连接到我们的服务器进程 const client new Client( { name: playwright-demo-client, version: 0.1.0 }, { capabilities: {} } ); // 注意这里假设服务器进程已在运行。实际部署时可能需要动态启动子进程。 const transport new StdioClientTransport({ command: node, args: [./server.js] }); await client.connect(transport); console.log(Connected to Playwright MCP Server.); // 2. 启动浏览器并导航到示例页面通过调用服务器工具 const startResult await client.request({ method: tools/call, params: { name: start_automation, arguments: { url: https://example.com } } }); console.log(Automation started:, startResult); // 模拟一段自动化操作 console.log(Performing automated login...); await new Promise(resolve setTimeout(resolve, 2000)); // 模拟操作耗时 // 3. 到达需要人工介入的节点暂停自动化 console.log( Pausing automation for manual CAPTCHA verification ); const pauseResult await client.request({ method: tools/call, params: { name: pause_for_manual_control } }); console.log(Pause acknowledged:, pauseResult); // 这行调用会阻塞直到手动操作方调用 resume_automation // 在实际的Playwright脚本中你可能需要将这部分逻辑封装到一个函数中 // 该函数调用MCP工具并等待而不是像这里一样线性执行。 // 为了演示我们假设有一个 waitForResume 的机制。 // 此处简化我们直接等待一段时间模拟手动操作期。 console.log(【此时用户可以看到浏览器窗口并手动操作。我们等待10秒模拟】); await new Promise(resolve setTimeout(resolve, 10000)); // 4. 在真实的协同中恢复信号由另一个客户端发出。 // 此处为演示我们假设手动操作已完成自动调用恢复。 console.log( Manual operation complete. Resuming automation ); const resumeResult await client.request({ method: tools/call, params: { name: resume_automation } }); console.log(Resume acknowledged:, resumeResult); // 5. 自动化继续执行 console.log(Automation continuing... finishing up.); await new Promise(resolve setTimeout(resolve, 1000)); await client.close(); console.log(Demo finished.); } runDemo().catch(console.error);这个演示脚本清晰地展示了流程启动自动化 - 执行任务 - 在特定点主动暂停并让出控制权 - 模拟手动操作期- 恢复自动化 - 继续执行。4.5 构建一个简单的手动控制台为了完整闭环我们需要一个方式让“人”来发送恢复信号。可以是一个简单的Node.js CLI脚本manual_controller.js// manual_controller.js import readline from readline; import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const rl readline.createInterface({ input: process.stdin, output: process.stdout }); async function setupController() { const client new Client( { name: manual-controller, version: 0.1.0 }, { capabilities: {} } ); const transport new StdioClientTransport({ command: node, args: [./server.js] }); await client.connect(transport); console.log(Manual Controller Connected. Type resume to resume automation, or exit to quit.); rl.on(line, async (input) { if (input.trim() resume) { try { const result await client.request({ method: tools/call, params: { name: resume_automation } }); console.log(Resume command sent successfully.); } catch (error) { console.error(Failed to send resume:, error); } } else if (input.trim() exit) { rl.close(); await client.close(); process.exit(0); } else { console.log(Unknown command. Use resume or exit.); } }); } setupController().catch(console.error);运行这个控制器你就可以在自动化脚本暂停后在命令行输入resume来让它继续了。在实际应用中这个控制器可以发展成一个带有按钮的Web界面或者集成到IDE的侧边栏中。5. 常见问题与排查技巧实录在实际开发和集成过程中我遇到了不少问题。这里记录下最典型的几个及其解决方案。5.1 连接与传输问题问题MCP客户端连接服务器失败报错ECONNREFUSED或传输错误。排查确保服务器已启动MCP服务器需要先于客户端启动。检查server.js进程是否在运行。检查传输配置StdioTransport要求客户端启动服务器子进程。确保客户端脚本中command和args路径正确。在复杂环境下考虑使用StdioServerTransport和StdioClientTransport的stdio配置选项显式指定stdin,stdout,stderr流。协议版本兼容检查modelcontextprotocol/sdk的版本在服务器和客户端是否一致。版本差异可能导致握手失败。实操心得在开发阶段可以先用一个简单的“echo”服务器测试MCP连接是否通畅排除Playwright本身的影响。另外务必在服务器和客户端的代码中添加详细的错误日志和事件监听MCP SDK提供了onerror等事件能帮助快速定位通信层问题。5.2 浏览器状态同步异常问题手动操作后恢复自动化脚本找不到元素或者页面状态不对。排查页面句柄失效手动操作可能打开了新标签页或弹窗导致原来的page对象引用不再是活动页面。在resumeAutomation方法中加入页面焦点恢复逻辑async resumeAutomation(sessionId) { // ... 状态检查 const pages this.context.pages(); // 尝试找到之前记录的页面或激活第一个页面 let activePage this.page; if (activePage !activePage.isClosed()) { await activePage.bringToFront(); } else if (pages.length 0) { activePage pages[0]; await activePage.bringToFront(); this.page activePage; // 更新引用 } else { throw new Error(No browser page available after manual control.); } // ... 解决Promise }选择器健壮性自动化脚本中避免使用绝对XPath。优先使用>// 在自动化脚本中 await page.waitForSelector(button:has-text(Submit), { state: visible, timeout: 10000 });实操心得在pause_for_manual_control工具被调用时自动对当前页面进行一个“现场保全”。除了截图还可以记录当前URL和主要框架的content哈希。恢复时先进行比对如果差异过大可以抛出一个警告或触发一个特殊的恢复流程比如导航回原URL并重新执行暂停前的最后几步。5.3 性能与资源泄漏问题长时间运行后内存占用越来越高或者浏览器变得卡顿。排查与优化页面管理确保不会无限制地创建新页面而不关闭旧页面。在自动化脚本中及时page.close()不再需要的页面。在会话管理器中定期清理context.pages()中已关闭的页面引用。资源清理在服务器关闭或会话结束时确保正确关闭浏览器await browser.close()。使用try...catch...finally块来保证资源释放。快照优化capture_page_context工具如果返回完整DOM或大图会消耗大量内存和网络带宽。只返回必要的信息比如交互元素的摘要列表或者将截图保存为文件后返回文件路径。实操心得为你的MCP服务器实现一个health_check工具返回当前打开的页面数、内存使用概览等信息。这对于监控长期运行的服务非常有用。可以考虑引入playwright-core而不是完整的playwright包如果你只需要Chromium的话可以减小依赖体积。5.4 与现有测试框架如Playwright Test集成问题如何在Playwright Test的测试用例中嵌入协同暂停解决方案Playwright Test本身不支持直接调用外部MCP服务。但你可以通过环境变量或配置文件来注入控制逻辑。在playwright.config.ts中设置一个globalSetup和globalTeardown来启动和停止你的MCP服务器。在测试文件中编写一个自定义的test.step或使用test.setTimeout在特定条件下例如遇到验证码调用一个辅助函数这个函数负责通过MCP客户端发送暂停请求。更优雅的方式是创建一个Playwright Test的自定义Fixture。这个Fixture封装了页面对象和MCP客户端提供类似await fixture.pauseForManualCheck()的方法。// 示例一个简单的自定义Fixture思路 import { test as base, Page } from playwright/test; import { McpClient } from ./mcp-client; export type TestFixtures { smartPage: Page { pauseForManual: (reason: string) Promisevoid }; }; export const test base.extendTestFixtures({ smartPage: async ({ page }, use) { const client new McpClient(); // 你的MCP客户端封装 await client.connect(); const enhancedPage Object.assign(page, { pauseForManual: async (reason: string) { console.log(Pausing test for: ${reason}); await client.callTool(pause_for_manual_control, { reason }); // 这里会阻塞直到手动恢复 } }); await use(enhancedPage); await client.close(); }, });然后在测试中就可以await smartPage.pauseForManual(Please verify the CAPTCHA);。将Playwright MCP扩展集成到n8n或类似的工作流自动化平台中其核心思路是将其作为一个自定义节点Custom Node。n8n支持通过HTTP请求、命令行调用或直接引入JS模块来集成外部功能。你可以将MCP服务器的工具调用封装成n8n能识别的操作这样就能在图形化工作流中直接插入“暂停等待人工确认”或“获取页面数据”的节点极大地丰富了自动化流程的灵活性和处理复杂场景的能力。

相关新闻