基于MCP协议与Playwright构建零代码AI自动化测试框架
1. 项目概述当AI遇上浏览器自动化最近在搞自动化测试的朋友估计都听过一个词叫“零代码”。听起来挺玄乎但说白了就是让不懂编程的人也能玩转自动化。今天要聊的这个“TraePlaywright MCP”组合就是冲着这个目标来的。它试图解决一个很实际的痛点测试同学或者产品、运营想验证一个网页流程但不想或者没时间写一行代码能不能快速搞出一个能跑起来的自动化脚本Trae你可以把它理解为一个“AI工作台”或者“智能体平台”。它本身不直接干具体的活比如操作浏览器或者分析数据但它擅长“指挥”和“协调”。它通过一种叫做MCPModel Context Protocol的协议去调用各种专门的“技能”Skills。这些技能才是真正干活的专家。Playwright则是目前最火的浏览器自动化框架之一由微软出品支持Chromium、Firefox和WebKit三大内核写起自动化脚本来又快又稳。那么“TraePlaywright MCP”的核心思路就清晰了在Trae这个平台上接入一个具备Playwright操作能力的MCP服务。然后你只需要用自然语言告诉Trae你想干什么比如“帮我去电商网站首页搜索‘手机’把前三个商品的名字和价格记下来”Trae就会理解你的意图通过MCP协议指挥Playwright技能去浏览器里执行这一系列操作并返回结果。整个过程你确实没写代码但一个功能完整的自动化测试流程已经跑通了。这玩意儿适合谁呢首先是测试工程师尤其是那些业务变化快、需要频繁回归核心场景的团队可以用它快速生成冒烟测试脚本。其次是产品经理和运营他们可以用这个工具来自动化一些数据采集、竞品页面监控或者简单的功能走查。当然对开发同学来说这也是一个不错的快速原型验证工具。2. 核心思路与架构拆解MCP如何连接AI与自动化要理解这个方案为什么能工作得先拆解它的三层架构交互层、协调层和执行层。这就像一家餐厅你是顾客交互层服务员是Trae协调层后厨的厨师是Playwright技能执行层。2.1 MCP协议万能的服务连接器MCP即模型上下文协议是这套方案的技术基石。它不是某个具体的软件而是一套通信标准。你可以把它想象成USB协议或者蓝牙协议。你的电脑Trae通过USB协议可以连接键盘、鼠标、U盘等任何符合标准的设备MCP Server。在这个项目里Trae是MCP Client客户端而我们将要搭建的、封装了Playwright能力的服务就是一个MCP Server服务器。这个Server向Client“宣告”自己有哪些能力比如“我能打开浏览器”、“我能点击页面元素”、“我能提取文本”。当Trae收到用户“点击登录按钮”的指令时它就知道该调用这个Server的“点击”能力并把“登录按钮”的描述信息传过去。MCP协议的核心优势在于解耦和标准化。Trae不需要知道Playwright的具体API怎么写它只认MCP的标准调用方式。同样一个写好了的Playwright MCP Server不仅可以给Trae用未来任何支持MCP协议的AI工作台或智能体平台都能直接调用它。这极大地提高了工具的可复用性。2.2 Trae的角色意图理解与任务编排Trae在这里扮演大脑和指挥官的角色。它的核心工作有两部分意图理解将用户模糊的自然语言指令转化为结构化的、可操作的任务步骤。比如用户说“看看新闻网站的头条”Trae需要理解这可能需要“打开某网址”、“等待页面加载”、“找到头条新闻的区域”、“提取该区域的文本”。任务编排根据分解后的任务步骤按顺序调用对应的MCP Server能力。它知道先调用“导航”去打开网页再调用“等待元素”确保内容加载最后调用“获取文本”拿到结果。目前市面上除了Trae也有其他平台支持MCP比如Claude for Desktop、Cursor等。选择Trae可能是因为它在设计上更偏向于一个集成的智能体开发与运行环境对MCP的支持比较原生和友好。但原理是相通的。2.3 Playwright技能可靠的自动化执行者执行层就是我们的Playwright MCP Server。它不是一个现成的、开箱即用的软件而是需要我们基于Playwright库和MCP Server SDK去开发的一个服务。这个服务内部封装了所有浏览器自动化逻辑。它的工作流程是收到Trae发来的标准化指令如{“action”: “click”, “selector”: “button#submit”}后将其“翻译”成Playwright的Python或JavaScript代码例如page.click(‘button#submit’)然后在后台启动或复用一個浏览器实例来执行这段代码最后将执行结果成功/失败、获取到的文本等包装成MCP标准格式返回给Trae。注意这里有一个关键点即元素定位。Playwright支持多种定位方式CSS选择器、XPath、文本内容等。在零代码场景下不可能让用户去写选择器。因此这个MCP Server需要具备一定的“元素推断”能力。例如当Trae传来“点击登录按钮”的指令时Server可能需要结合页面上下文智能地使用如page.get_by_role(“button”, name“登录”)或page.get_by_text(“登录”)这类更语义化的定位方式这比硬编码的CSS选择器更健壮。3. 从零搭建Playwright MCP Server实战理论讲完了我们动手搭一个最简单的Playwright MCP Server。这里以Python为例因为它生态丰富Playwright的Python API也非常清晰。3.1 环境准备与依赖安装首先确保你的系统有Python 3.8。然后我们创建项目目录并安装核心依赖。# 创建项目目录 mkdir playwright-mcp-server cd playwright-mcp-server # 创建虚拟环境推荐 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate # 安装核心库 pip install playwright mcp # 安装Playwright所需的浏览器内核 playwright install chromium这里解释一下几个包playwright: Python语言下操作浏览器的核心库。mcp: 官方提供的MCP协议Python SDK它提供了构建Server和Client的工具类让我们不用从零实现协议通信。playwright install chromium: 安装Chromium浏览器二进制文件。Playwright的特点是自带浏览器环境一致性强避免了“在我机器上能跑”的问题。3.2 构建一个最小化的MCP Server我们来创建一个server.py文件实现几个最基础的能力启动浏览器、打开网页、截图、获取页面标题。import asyncio from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import playwright.async_api from pydantic import BaseModel from typing import Any # 定义MCP Server app Server(playwright-mcp-server) # 定义工具Tool的输入参数模型 class NavigateParams(BaseModel): url: str class ScreenshotParams(BaseModel): selector: str “body” # 默认截整个页面 class GetTextParams(BaseModel): selector: str # 声明Server提供的工具 app.list_tools() async def handle_list_tools(): return [ { “name”: “navigate_to_url”, “description”: “在浏览器中导航到指定的URL”, “inputSchema”: { “type”: “object”, “properties”: { “url”: {“type”: “string”, “description”: “要访问的完整网址如 https://example.com} }, “required”: [“url”] } }, { “name”: “take_screenshot”, “description”: “对页面或指定元素进行截图返回图片的Base64编码”, “inputSchema”: { “type”: “object”, “properties”: { “selector”: {“type”: “string”, “description”: “CSS选择器默认为‘body’} }, “required”: [] } }, { “name”: “get_element_text”, “description”: “获取指定页面元素的文本内容”, “inputSchema”: { “type”: “object”, “properties”: { “selector”: {“type”: “string”, “description”: “CSS选择器”} }, “required”: [“selector”] } } ] # 全局浏览器和页面实例简单示例生产环境需管理生命周期 _browser None _page None async def get_or_create_page(): 获取或创建浏览器页面实例 global _browser, _page if _browser is None: _browser await playwright.async_api.async_playwright().start() if _page is None: browser_context await _browser.launch(headlessTrue) # 无头模式 _page await browser_context.new_page() return _page # 实现导航工具 app.call_tool() async def handle_call_tool(name: str, arguments: dict[str, Any]) - list[dict]: page await get_or_create_page() if name “navigate_to_url”: params NavigateParams(**arguments) await page.goto(params.url, wait_until“networkidle”) # 等待网络空闲 return [{ “type”: “text”, “text”: f“已成功导航至 {params.url}当前页面标题{await page.title()}” }] elif name “take_screenshot”: params ScreenshotParams(**arguments) element page.locator(params.selector).first if params.selector ! “body” else page screenshot_bytes await element.screenshot() import base64 screenshot_b64 base64.b64encode(screenshot_bytes).decode(‘utf-8’) return [{ “type”: “text”, “text”: f“截图成功选择器{params.selector}” }, { “type”: “image”, “data”: screenshot_b64, “mimeType”: “image/png” }] elif name “get_element_text”: params GetTextParams(**arguments) text_content await page.locator(params.selector).first.text_content() return [{ “type”: “text”, “text”: f“元素 ‘{params.selector}’ 的文本内容为{text_content}” }] else: raise ValueError(f“未知工具{name}”) # 资源清理可选 app.on_exit() async def handle_exit(): global _browser if _browser: await _browser.close() # 启动Server async def main(): async with app.run_stdio() as (read_stream, write_stream): # 这里使用标准输入输出流通信方便Trae通过子进程调用 await app._run(read_stream, write_stream, InitializationOptions()) if __name__ “__main__”: asyncio.run(main())这个Server虽然简单但五脏俱全。它通过app.list_tools()声明了三个工具并通过app.call_tool()实现了对应的功能。它使用标准输入输出stdio作为通信方式这是MCP Server最常见的一种运行模式方便被主进程调用。3.3 在Trae中配置与连接MCP Server假设你已经安装了Trae如Trae Work或Trae IDE。配置MCP Server通常需要一个配置文件。在Trae的配置目录或通过其GUI设置中添加如下配置// 例如在Trae的 mcp-servers.json 配置文件中 { “mcpServers”: { “playwright-automation”: { “command”: “python”, “args”: [ “/你的绝对路径/playwright-mcp-server/server.py” ], “env”: { “PYTHONPATH”: “/你的绝对路径/playwright-mcp-server” } } } }配置完成后重启Trae。理论上Trae应该能自动发现并连接这个Server。你可以在Trae的聊天界面中尝试输入“使用playwright工具打开百度首页并截图”。Trae会识别出可用的工具并组合调用navigate_to_url和take_screenshot。实操心得一环境隔离与路径问题这是第一个容易踩坑的地方。Trae在调用你的Python脚本时可能是在一个全新的子进程环境中可能找不到你虚拟环境中的依赖。有几种解决方案在配置的args中直接指向虚拟环境内的Python解释器“args”: [“venv/bin/python”, “server.py”]。使用绝对路径并且确保Playwright的浏览器也已安装在系统路径或该虚拟环境中。将依赖打包成Docker镜像用Docker命令来运行Server这是最彻底的环境隔离方案。4. 实现智能元素定位与健壮性处理上面的例子还很简单尤其是元素定位仍然要求用户输入CSS选择器这离“零代码”还有距离。真正的智能自动化需要Server能理解更模糊的指令。4.1 增强工具基于描述的智能点击与输入我们需要设计更“聪明”的工具。例如一个smart_click工具它接收“按钮文本”或“元素附近文本”作为描述而不是选择器。class SmartClickParams(BaseModel): description: str # 如“登录按钮”、“搜索框旁边的提交按钮” app.call_tool() async def handle_call_tool(name: str, arguments: dict[str, Any]) - list[dict]: # ... 其他工具处理 ... if name “smart_click”: params SmartClickParams(**arguments) page await get_or_create_page() # 策略1优先通过角色和名称定位ARIA标准 button_locator page.get_by_role(“button”, nameparams.description, exactTrue) if await button_locator.count() 0: await button_locator.first.click() return [{“type”: “text”, “text”: f“已通过角色定位点击‘{params.description}’按钮”}] # 策略2通过文本内容定位 text_locator page.get_by_text(params.description, exactTrue) if await text_locator.count() 0: await text_locator.first.click() return [{“type”: “text”, “text”: f“已通过文本定位点击‘{params.description}’”}] # 策略3通过Placeholder定位输入框 input_locator page.get_by_placeholder(params.description) if await input_locator.count() 0: await input_locator.first.click() return [{“type”: “text”, “text”: f“已点击Placeholder为‘{params.description}’的输入框”}] # 策略4组合定位文本角色 # 可以尝试更复杂的组合比如先找包含该文本的div再在其子元素中找button # 如果都找不到 return [{“type”: “text”, “text”: f“未在页面上找到描述为‘{params.description}’的可点击元素”}]同时我们还需要一个smart_fill工具来处理输入。class SmartFillParams(BaseModel): field_description: str # 字段描述如“用户名”、“密码” value: str # 要输入的值 # 在 handle_call_tool 中添加 elif name “smart_fill”: params SmartFillParams(**arguments) page await get_or_create_page() # 尝试多种方式定位输入框 locators_to_try [ page.get_by_placeholder(params.field_description), page.get_by_label(params.field_description), # 通过label标签 page.get_by_role(“textbox”, nameparams.field_description), ] for locator in locators_to_try: if await locator.count() 0: await locator.first.fill(params.value) return [{“type”: “text”, “text”: f“已在‘{params.field_description}’字段输入内容”}] return [{“type”: “text”, “text”: f“未找到描述为‘{params.field_description}’的输入字段”}]4.2 引入等待与重试机制网页加载有快有慢元素出现有时延。健壮的自动化必须包含等待。from playwright.async_api import TimeoutError as PlaywrightTimeoutError async def smart_click_with_retry(page, description, retries3, delay1.0): 带重试的智能点击 for attempt in range(retries): try: # ... 上述定位逻辑 ... # 在点击前可以增加一个显式等待确保元素可交互 await button_locator.first.wait_for(state“visible”, timeout5000) await button_locator.first.click() return True, f“第{attempt1}次尝试点击成功” except PlaywrightTimeoutError: await asyncio.sleep(delay) # 等待后重试 continue except Exception as e: return False, f“点击时发生错误{str(e)}” return False, f“尝试{retries}次后仍未找到或无法点击元素‘{description}’”在工具调用中使用这个增强函数来代替直接的点击操作。4.3 组合工具实现完整流程现在我们可以声明一个更高级的“流程工具”它接收一个目标描述内部按顺序调用多个基础工具。class LoginFlowParams(BaseModel): url: str username: str password: str # 在工具列表中声明 { “name”: “execute_login_flow”, “description”: “执行一个标准的登录流程需要提供网址、用户名和密码”, “inputSchema”: { “type”: “object”, “properties”: { “url”: {“type”: “string”}, “username”: {“type”: “string”}, “password”: {“type”: “string”} }, “required”: [“url”, “username”, “password”] } } # 在工具调用中实现 elif name “execute_login_flow”: params LoginFlowParams(**arguments) page await get_or_create_page() results [] # 1. 导航 await page.goto(params.url, wait_until“networkidle”) results.append({“type”: “text”, “text”: f“导航到 {params.url}”}) # 2. 输入用户名 success, msg await smart_fill_with_retry(page, “用户名”, params.username) results.append({“type”: “text”, “text”: msg}) if not success: return results # 中途失败则返回 # 3. 输入密码 success, msg await smart_fill_with_retry(page, “密码”, params.password) results.append({“type”: “text”, “text”: msg}) if not success: return results # 4. 点击登录按钮 success, msg await smart_click_with_retry(page, “登录”) results.append({“type”: “text”, “text”: msg}) # 5. 可选等待登录后页面跳转并验证登录成功如检查是否存在“退出”按钮 try: await page.wait_for_url(“**/dashboard**”, timeout10000) # 假设登录后跳转到dashboard results.append({“type”: “text”, “text”: “登录成功已跳转到仪表盘。”}) except PlaywrightTimeoutError: results.append({“type”: “text”, “text”: “登录操作已完成但未检测到预期的页面跳转。”}) return results这样用户在Trae中只需要说“执行登录流程网址是xxx用户名是yyy密码是zzz”就能完成整个自动化操作。这才是“零代码”的体验。5. 高级特性与工程化考量一个可用于实际项目的Playwright MCP Server还需要考虑更多。5.1 会话隔离与状态管理上面的示例使用了全局的_browser和_page。这意味着所有用户的请求都共享同一个浏览器页面这会导致严重的状态混乱和数据泄露。在生产环境中必须为每个会话或每个用户请求创建独立的浏览器上下文Context。from typing import Dict import uuid # 使用字典管理会话 _sessions: Dict[str, dict] {} class SessionParams(BaseModel): session_id: str None # 客户端可传入已有的session_id否则创建新的 async def get_or_create_session(session_id: str None): 获取或创建一个独立的浏览器会话 if session_id and session_id in _sessions: return _sessions[session_id] new_id session_id or str(uuid.uuid4()) playwright_instance await playwright.async_api.async_playwright().start() browser await playwright_instance.launch(headlessTrue) context await browser.new_context() # 每个会话独立的上下文 page await context.new_page() session { “id”: new_id, “browser”: browser, “context”: context, “page”: page, “playwright”: playwright_instance } _sessions[new_id] session return session # 在每个工具调用开始时获取或创建会话 app.call_tool() async def handle_call_tool(name: str, arguments: dict[str, Any]) - list[dict]: # 从参数中提取或生成session_id session_params SessionParams(**arguments) session await get_or_create_session(session_params.session_id) page session[“page”] # 后续所有操作都基于这个独立的page # ... # 在返回结果中可以附上本次使用的session_id供后续调用使用 return [{ “type”: “text”, “text”: f“操作成功。会话ID: {session[‘id’]}”, “session_id”: session[‘id’] # 可以放在metadata中 }] # 需要增加一个清理会话的工具 app.list_tools() async def handle_list_tools(): tools [...] tools.append({ “name”: “close_session”, “description”: “关闭指定的浏览器会话释放资源”, “inputSchema”: { “type”: “object”, “properties”: { “session_id”: {“type”: “string”} }, “required”: [“session_id”] } }) return tools5.2 结果验证与断言自动化测试的核心是验证。我们需要提供断言工具。class AssertTextParams(BaseModel): session_id: str selector: str expected_text: str is_exact: bool True elif name “assert_text”: params AssertTextParams(**arguments) session _sessions.get(params.session_id) if not session: return [{“type”: “text”, “text”: f“会话 {params.session_id} 不存在”}] page session[“page”] actual_text await page.locator(params.selector).first.text_content() if params.is_exact: is_pass (actual_text.strip() params.expected_text.strip()) else: is_pass (params.expected_text.strip() in actual_text.strip()) if is_pass: return [{“type”: “text”, “text”: f“断言通过元素‘{params.selector}’的文本符合预期‘{params.expected_text}’。”}] else: return [{“type”: “text”, “text”: f“断言失败元素‘{params.selector}’的文本为‘{actual_text}’预期为‘{params.expected_text}’。”}]5.3 性能优化与资源回收浏览器实例是重量级资源。需要实现超时自动回收和主动清理机制。import asyncio from datetime import datetime, timedelta # 在会话信息中记录最后活动时间 session[“last_activity”] datetime.now() # 可以启动一个后台任务定期检查并清理闲置过久的会话 async def cleanup_idle_sessions(timeout_minutes30): while True: await asyncio.sleep(300) # 每5分钟检查一次 now datetime.now() to_delete [] for sid, sess in _sessions.items(): if now - sess[“last_activity”] timedelta(minutestimeout_minutes): to_delete.append(sid) for sid in to_delete: await _sessions[sid][“browser”].close() await _sessions[sid][“playwright”].stop() del _sessions[sid] print(f“已清理闲置会话{sid}”)6. 常见问题与排查技巧实录在实际搭建和运行过程中你会遇到各种各样的问题。下面是我踩过的一些坑和解决办法。6.1 Trae无法发现或连接MCP Server这是最常见的问题表现为Trae的聊天界面里根本看不到你定义的工具。检查点1配置语法与路径仔细检查Trae的MCP Server配置文件如mcp-servers.json。确保command和args的路径是绝对路径。相对路径在Trae启动的子进程环境中很可能失效。特别是Python脚本的路径和虚拟环境路径。检查点2Server启动日志修改你的server.py在开头和main()函数里加上打印语句比如print(“Playwright MCP Server starting...”, filesys.stderr)。然后尝试在终端手动用配置中的命令运行它看是否能正常启动是否有报错如缺少依赖。Trae通常会捕获Server的标准错误输出查看Trae的日志窗口能找到线索。检查点3MCP协议版本确保你使用的mcpPython SDK版本与Trae兼容。有时版本不匹配会导致握手失败。可以尝试使用较稳定的版本例如pip install mcp0.5.*。检查点4Trae的重新加载修改配置后必须完全重启Trae。有时仅仅是重启插件或刷新界面是不够的。6.2 工具调用成功但浏览器无反应Trae显示调用了工具并返回了成功信息但实际浏览器没有任何动作。检查点1Headless模式在开发阶段不要使用无头模式。将launch(headlessTrue)改为launch(headlessFalse)。这样浏览器窗口会弹出来你能直观地看到页面是否加载、点击是否生效。这能立刻区分是通信问题还是Playwright操作问题。检查点2页面加载状态Playwright的page.goto()默认的等待策略可能不够。对于现代单页应用SPA使用wait_until”networkidle”或wait_until”commit”可能更合适。甚至可以在goto之后用page.wait_for_selector(“某个关键元素”)来确保页面真正就绪。检查点3元素定位失败这是自动化脚本的头号杀手。打开浏览器的开发者工具F12使用元素选择器仔细检查你代码中使用的选择器或定位策略如get_by_text在当前页面状态下是否唯一匹配目标元素。页面结构可能动态变化或者存在iframe。实操心得二优先使用语义化定位器Playwright推荐的page.get_by_role(),page.get_by_text(),page.get_by_label(),page.get_by_placeholder()等API比写死的CSS选择器如div.button:nth-child(3)健壮得多。它们更贴近用户视角“提交按钮”而不是开发者视角#submit-btn。在构建智能MCP工具时应优先尝试这些语义化定位方式。6.3 会话管理与内存泄漏长时间运行后Server内存占用越来越高甚至崩溃。根源每个会话都创建了独立的Browser和Playwright实例但调用结束后没有正确关闭。解决方案必须实现会话关闭工具如上文所述提供close_session工具并在Trae的流程结束时显式调用。设置超时自动回收如上文cleanup_idle_sessions后台任务。使用Browser Context对于轻度任务可以为每个请求创建新的Context而不是新的Browser。Context更轻量共享同一个Browser进程。context await browser.new_context(); page await context.new_page()。任务结束后关闭Context即可await context.close()。6.4 异步编程陷阱Playwright API和MCP SDK都是异步的async/await。在编写工具函数时一个常见的错误是混用同步代码或者在错误的地方await。确保入口点是异步的我们的main()函数和handle_call_tool都用了async。避免阻塞操作不要在异步函数内部执行长时间的同步计算如处理大文件。如果必须使用asyncio.to_thread将其放到线程池中执行。正确处理异常用try...except包裹可能失败的Playwright操作如点击、等待并返回清晰的错误信息给Trae而不是让整个Server崩溃。6.5 安全与隐私考量密码等敏感信息在Trae的对话中直接传递密码存在泄露风险。在实际应用中应考虑通过Trae的密钥管理功能或环境变量来传递而不是写在对话里。MCP Server本身也应避免在日志中打印敏感参数。任意URL导航你的Server如果允许导航到任意URL则可能被滥用访问恶意网站。可以根据业务需要在Server端设置URL白名单。资源限制防止恶意用户创建大量会话耗尽系统资源。可以限制单个IP的并发会话数或设置每个会话的最大操作次数。7. 扩展思路不止于测试这个“TraePlaywright MCP”的框架其潜力远不止于自动化测试。任何需要通过浏览器与网页交互的重复性工作都可以尝试用它来简化。数据抓取与监控配置一个定时任务让Trae每天上午10点执行“打开某数据面板截图并提取关键指标通过邮件发送给我”的流程。竞品分析编写一个流程自动打开几个竞品网站抓取首页产品列表、价格、活动信息并整理成表格。内容生成与发布结合AI生成文章然后通过Playwright MCP自动登录到你的博客后台创建新文章并发布。工作流自动化对于内部那些没有API的老旧Web系统可以用它来模拟人工操作实现数据录入、报表下载等。要实现这些关键在于设计好“领域特定”的高级MCP工具。例如对于数据抓取可以设计一个scrape_table工具它接收一个URL和一个表格描述返回结构化的JSON数据。这样Trae只需要组合导航和抓取工具就能完成复杂任务。我个人在实际操作中的体会是最大的挑战不在于技术实现而在于如何将模糊的人类指令精准地分解和映射为一系列可靠的浏览器操作。这需要你的MCP Server具备一定的“领域知识”比如知道电商网站的“加入购物车”按钮通常是什么样子知道登录流程的常见步骤。这可以通过预定义更多的“流程工具”和更精细的“智能定位”策略来不断完善。开始时可以从最确定、最高频的场景做起让它先在一个小范围内可靠地工作再逐步扩展其能力边界。

相关新闻