基于MCP协议与Playwright的AI自动化测试实践指南
1. 项目概述:当自动化测试遇上AI副驾驶
最近在搞自动化测试的朋友,估计都绕不开两个词:Playwright和Copilot。前者是微软开源的现代化端到端测试框架,后者是GitHub推出的AI编程助手。单独用它们,一个能帮你稳定地模拟用户操作,一个能帮你快速生成代码片段。但你想过没有,如果把这两者结合起来,用Copilot来辅助编写Playwright测试脚本,甚至通过某种协议让它们深度对话,会是什么体验?这就是“Playwright MCP入门实战”要探讨的核心。
简单来说,这个项目就是探索如何利用MCP(Model Context Protocol,模型上下文协议)这座桥梁,将Playwright自动化测试的能力“喂”给Copilot这类AI助手。让AI不仅能理解你的测试需求,还能直接调用Playwright来执行操作、获取页面状态,从而实现更智能、更“自动化”的自动化测试。它解决的痛点是:编写和维护大量重复、易变的UI测试脚本耗时耗力,而AI可以成为你的“测试脚本生成器”和“智能调试伙伴”。无论你是刚接触Playwright的新手,还是苦于测试脚本编写效率的资深QA,这个集成方案都能带来全新的工作流体验。
2. 核心思路与架构设计:理解MCP如何连接AI与测试
2.1 为什么是Playwright + MCP + Copilot?
在深入实操前,我们先拆解一下这个组合的合理性。Playwright之所以成为现代Web自动化测试的首选,是因为它支持多浏览器(Chromium, Firefox, WebKit)、无头/有头模式、自动等待、强大的选择器和网络拦截能力,对单页应用(SPA)和复杂交互的支持尤其出色。它的API设计现代,社区活跃。
而GitHub Copilot作为一个AI编程助手,其核心能力是基于上下文(你正在编写的代码、注释)来预测和生成代码。但它的“知识”截止于其训练数据,对于实时运行环境、动态页面状态、具体的测试执行结果,它是“盲”的。
MCP协议的出现,就是为了解决这个问题。你可以把它想象成AI模型的“外挂感官”和“可调用工具库”。一个MCP Server(服务器)对外暴露一系列“工具”(Tools)和“资源”(Resources),而一个MCP Client(客户端,如集成了MCP的Copilot、Claude Desktop等)可以发现并调用这些工具。在这个场景下,我们可以构建一个Playwright MCP Server,它提供的工具可能是:“打开浏览器导航到某URL”、“在页面中点击某个元素”、“获取当前页面标题”、“执行一段自定义的Playwright脚本并返回结果”。
于是,工作流就变成了:你在Copilot的聊天框中输入“帮我对登录页面做个测试,用户名是test,密码是123456”,Copilot(作为MCP Client)识别出你的意图,发现并调用了你本地的Playwright MCP Server提供的“执行登录测试”工具。Server收到指令后,默默启动浏览器,执行Playwright脚本完成登录操作,并将“登录成功”或“错误提示信息”等结果返回给Copilot,Copilot再组织成自然语言告诉你。整个过程,AI从“代码建议者”变成了“测试执行指挥官”。
2.2 技术栈选型与项目结构规划
要实现这个构想,我们需要明确几个部分:
- Playwright MCP Server:这是核心。我们需要一个常驻进程,它内置Playwright,并按照MCP协议规范暴露接口。考虑到Playwright官方支持Node.js、Python、.NET和Java,而MCP的参考实现和生态目前更偏向Node.js/Python,我们选择Node.js作为Server的开发语言。这样可以直接使用Playwright的Node.js API,并且利用Node.js的异步事件驱动模型高效处理AI的并发请求。
- MCP Client (AI 端):我们需要一个能连接MCP Server的AI客户端。理想的选择是Claude Desktop或Cursor IDE,因为它们已经原生支持配置自定义MCP Server。GitHub Copilot Chat在VSCode中目前对自定义MCP的支持还在演进中,但通过一些配置也能实现。本指南会以 Claude Desktop 为例,因为它对MCP的支持最直接、最稳定。
- 通信协议:MCP协议目前主要支持两种传输方式:stdio(标准输入输出)和SSE(Server-Sent Events)。对于本地集成,stdio方式最简单,我们的Server作为一个子进程被Client启动,通过stdin/stdout进行JSON-RPC通信。
一个典型的项目目录结构会是这样:
playwright-mcp-guide/ ├── server/ # MCP Server 核心代码 │ ├── index.js # Server入口文件 │ ├── tools/ # 定义各类工具 │ │ ├── browser.js # 浏览器操作工具(打开、关闭、截图) │ │ ├── navigation.js # 页面导航工具 │ │ └── actions.js # 元素交互工具(点击、输入) │ └── package.json ├── client-config/ # 客户端配置文件 │ └── claude-desktop-mcp.json ├── examples/ # 示例脚本和用例 │ └── login-test.js └── README.md注意:在构建任何与AI和自动化工具集成的项目时,务必牢记安全边界。我们的Playwright MCP Server应该设计为仅在本地运行,并且暴露的工具需进行必要的参数校验和权限控制,避免执行任意危险命令或访问敏感文件。切勿将此类Server不加保护地暴露在公网。
3. 手把手搭建Playwright MCP Server
3.1 初始化项目与环境准备
首先,确保你的系统已安装Node.js(建议18以上版本)和npm。然后,我们创建Server项目。
mkdir playwright-mcp-server cd playwright-mcp-server npm init -y接下来,安装核心依赖。我们需要@modelcontextprotocol/sdk来快速构建符合MCP协议的Server,同时安装playwright。
npm install @modelcontextprotocol/sdk playwright安装Playwright的浏览器内核(Chromium, Firefox, WebKit)。这一步可能会耗时较长,因为它需要下载浏览器二进制文件。
npx playwright install3.2 构建第一个MCP工具:浏览器导航
现在,我们来创建Server的入口文件index.js。MCP SDK的使用模式是定义一个工具列表,然后启动Server。
// server/index.js const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { playwrightTools } = require('./tools/browser.js'); // 我们即将创建的工具集 async function main() { // 1. 创建MCP Server实例,给它起个名字 const server = new Server( { name: 'playwright-mcp-server', version: '0.1.0', }, { capabilities: { tools: {}, // 声明我们支持工具 }, } ); // 2. 注册我们定义的工具 server.setRequestHandler('tools/list', async () => ({ tools: playwrightTools, })); // 3. 处理工具调用请求 server.setRequestHandler('tools/call', async (request) => { const tool = playwrightTools.find(t => t.name === request.params.name); if (!tool) { throw new Error(`Tool ${request.params.name} not found`); } // 在这里,我们将请求分发给具体的工具处理函数 // 例如,如果调用的是 `navigate_to_url`,我们就执行导航逻辑 return await handleToolCall(request.params.name, request.params.arguments); }); // 4. 使用stdio传输层启动服务器 const transport = new StdioServerTransport(); await server.connect(transport); console.error('Playwright MCP Server running on stdio...'); } // 工具调用分发处理函数(简化示例,实际需要更完善的分发逻辑) async function handleToolCall(toolName, args) { if (toolName === 'navigate_to_url') { const { url } = args; // 这里调用真正的Playwright导航逻辑 const result = await navigateToUrl(url); return { content: [ { type: 'text', text: `成功导航至: ${result.url}, 页面标题: "${result.title}"`, }, ], }; } // ... 处理其他工具 throw new Error(`Tool handler for ${toolName} not implemented`); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });上面的代码是骨架,关键在playwrightTools和handleToolCall。现在我们来创建第一个具体的工具browser.js。
// server/tools/browser.js const { chromium } = require('playwright'); // 全局维护一个浏览器实例,避免频繁启动关闭 let browser = null; let page = null; /** * 定义工具列表,每个工具需要符合MCP Tool的Schema */ const playwrightTools = [ { name: 'navigate_to_url', description: '使用浏览器打开指定的URL', inputSchema: { type: 'object', properties: { url: { type: 'string', description: '要访问的完整网址,例如 https://example.com', }, headless: { type: 'boolean', description: '是否以无头模式运行(不显示浏览器界面),默认为true', default: true, }, }, required: ['url'], }, }, { name: 'get_page_title', description: '获取当前活动页面的标题', inputSchema: { type: 'object', properties: {}, }, }, // 后续可以继续添加 click_element, fill_form, take_screenshot 等工具 ]; /** * 实际处理导航的工具函数 */ async function navigateToUrl(url, headless = true) { try { // 如果浏览器未启动,则启动一个 if (!browser) { browser = await chromium.launch({ headless }); const context = await browser.newContext(); page = await context.newPage(); } // 导航到目标URL,并等待网络基本空闲 const response = await page.goto(url, { waitUntil: 'domcontentloaded' }); const title = await page.title(); return { success: true, url: page.url(), title, status: response?.status() }; } catch (error) { return { success: false, error: error.message }; } } /** * 获取当前页面标题 */ async function getPageTitle() { if (!page) { return { success: false, error: '没有活动的页面,请先使用 navigate_to_url 打开一个网页。' }; } try { const title = await page.title(); return { success: true, title }; } catch (error) { return { success: false, error: error.message }; } } // 导出工具定义和处理函数 module.exports = { playwrightTools, navigateToUrl, getPageTitle, };接下来,我们需要完善index.js中的handleToolCall函数,使其能正确路由到这些工具函数。
// 在 index.js 中更新 handleToolCall 函数 const { navigateToUrl, getPageTitle } = require('./tools/browser.js'); async function handleToolCall(toolName, args) { switch (toolName) { case 'navigate_to_url': const { url, headless = true } = args; const navResult = await navigateToUrl(url, headless); if (navResult.success) { return { content: [{ type: 'text', text: `✅ 导航成功!\n当前URL: ${navResult.url}\n页面标题: "${navResult.title}"\nHTTP状态码: ${navResult.status}`, }], }; } else { return { content: [{ type: 'text', text: `❌ 导航失败: ${navResult.error}`, }], isError: true, }; } case 'get_page_title': const titleResult = await getPageTitle(); if (titleResult.success) { return { content: [{ type: 'text', text: `当前页面标题是: "${titleResult.title}"`, }], }; } else { return { content: [{ type: 'text', text: `❌ 获取标题失败: ${titleResult.error}`, }], isError: true, }; } default: throw new Error(`Tool ${toolName} not implemented`); } }3.3 完善工具集:元素交互与截图
仅有导航和获取标题还不够,一个实用的测试Server需要能模拟用户操作。我们来添加点击和输入工具。
首先,在tools/目录下创建actions.js。
// server/tools/actions.js const { page } = require('./browser.js'); // 复用 browser.js 中管理的 page 实例 const actionTools = [ { name: 'click_element', description: '点击页面上的某个元素', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS选择器或Playwright定位器(如 text=按钮文字)', }, timeout: { type: 'number', description: '等待元素出现的超时时间(毫秒),默认为30000', default: 30000, }, }, required: ['selector'], }, }, { name: 'fill_form', description: '向表单输入框填充文本', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: '输入框的CSS选择器或Playwright定位器', }, text: { type: 'string', description: '要输入的文本内容', }, timeout: { type: 'number', description: '等待输入框出现的超时时间(毫秒),默认为30000', default: 30000, }, }, required: ['selector', 'text'], }, }, { name: 'take_screenshot', description: '对当前页面进行截图', inputSchema: { type: 'object', properties: { fullPage: { type: 'boolean', description: '是否截取整个可滚动页面,默认为false(仅截取视口)', default: false, }, path: { type: 'string', description: '截图保存路径(可选),如不提供则返回base64编码', }, }, required: [], }, }, ]; async function clickElement(selector, timeout = 30000) { if (!page) { throw new Error('没有活动的页面。请先导航到一个网页。'); } try { await page.click(selector, { timeout }); return { success: true, message: `已点击元素: ${selector}` }; } catch (error) { // 错误处理可以更细致,比如区分“未找到元素”和“元素不可点击” return { success: false, error: `点击失败 (${selector}): ${error.message}` }; } } async function fillForm(selector, text, timeout = 30000) { if (!page) { throw new Error('没有活动的页面。请先导航到一个网页。'); } try { await page.fill(selector, text, { timeout }); return { success: true, message: `已在 ${selector} 中输入: ${text}` }; } catch (error) { return { success: false, error: `输入失败 (${selector}): ${error.message}` }; } } async function takeScreenshot(fullPage = false, path = null) { if (!page) { throw new Error('没有活动的页面。无法截图。'); } try { let screenshotBuffer; if (path) { screenshotBuffer = await page.screenshot({ path, fullPage }); return { success: true, message: `截图已保存至: ${path}`, path }; } else { screenshotBuffer = await page.screenshot({ fullPage }); const base64Image = screenshotBuffer.toString('base64'); return { success: true, message: '截图成功', image: `data:image/png;base64,${base64Image}` }; } } catch (error) { return { success: false, error: `截图失败: ${error.message}` }; } } module.exports = { actionTools, clickElement, fillForm, takeScreenshot, };然后,我们需要将actions.js中的工具合并到主工具列表,并更新handleToolCall函数。同时,为了在actions.js中能访问到page实例,我们需要稍微重构一下browser.js,将实例管理单独提取。
// server/tools/browser.js (重构后) const { chromium } = require('playwright'); let browser = null; let context = null; let page = null; function getPage() { if (!page) { throw new Error('Page not initialized. Call navigateToUrl first.'); } return page; } async function ensureBrowser(headless = true) { if (!browser) { browser = await chromium.launch({ headless }); context = await browser.newContext(); page = await context.newPage(); } return { browser, context, page }; } async function navigateToUrl(url, headless = true) { try { await ensureBrowser(headless); const response = await page.goto(url, { waitUntil: 'domcontentloaded' }); const title = await page.title(); return { success: true, url: page.url(), title, status: response?.status() }; } catch (error) { return { success: false, error: error.message }; } } // ... getPageTitle 等函数改为使用 getPage() async function getPageTitle() { try { const currentPage = getPage(); const title = await currentPage.title(); return { success: true, title }; } catch (error) { return { success: false, error: error.message }; } } // 导出实例获取方法,供 actions.js 使用 module.exports = { playwrightTools: [ /* 工具定义移到 index.js 统一管理 */ ], navigateToUrl, getPageTitle, getPage, // 新增导出 };在index.js中,我们统一导入所有工具定义和处理函数。
// server/index.js (更新部分) const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { playwrightTools: browserTools } = require('./tools/browser.js'); const { actionTools } = require('./tools/actions.js'); const { navigateToUrl, getPageTitle } = require('./tools/browser.js'); const { clickElement, fillForm, takeScreenshot } = require('./tools/actions.js'); // 合并所有工具 const allTools = [...browserTools, ...actionTools]; async function main() { const server = new Server(/* ... */); server.setRequestHandler('tools/list', async () => ({ tools: allTools, // 使用合并后的工具列表 })); // ... 其余不变 } async function handleToolCall(toolName, args) { // ... 在 switch 中添加新的 case case 'click_element': const { selector: clickSelector, timeout: clickTimeout } = args; const clickResult = await clickElement(clickSelector, clickTimeout); // ... 返回结果处理 break; case 'fill_form': const { selector: fillSelector, text, timeout: fillTimeout } = args; const fillResult = await fillForm(fillSelector, text, fillTimeout); // ... 返回结果处理 break; case 'take_screenshot': const { fullPage, path } = args; const screenshotResult = await takeScreenshot(fullPage, path); // ... 返回结果处理,如果是base64图片,可以以特定格式返回供Client渲染 break; }至此,一个具备基础浏览器操作能力的Playwright MCP Server就搭建完成了。你可以通过运行node index.js来启动它,它会等待通过stdio接收JSON-RPC请求。
4. 配置AI客户端连接MCP Server
Server准备好了,我们需要一个能理解MCP协议的Client来调用它。这里以Claude Desktop为例。
4.1 配置Claude Desktop
Claude Desktop允许通过配置文件添加自定义的MCP Server。配置文件通常位于:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
如果文件不存在,就创建一个。配置内容如下:
{ "mcpServers": { "playwright": { "command": "node", "args": [ "/ABSOLUTE/PATH/TO/YOUR/playwright-mcp-server/server/index.js" ], "env": { "NODE_ENV": "development" } } } }关键点:
command: 启动Server的命令,这里是node。args: 命令的参数,第一个是入口文件的绝对路径。请务必替换成你本地项目的实际路径。env: 可选,设置环境变量。
保存配置文件后,完全重启Claude Desktop应用。重启后,Claude应该就能发现并连接上你的Playwright MCP Server了。
4.2 在Copilot Chat (VSCode) 中配置(进阶)
截至撰写本文时,VSCode中的GitHub Copilot Chat对自定义MCP Server的支持尚在测试阶段,可能需要使用预览版扩展或特定设置。一种可行的方法是通过Continue.dev或Cursor IDE(内置了基于MCP的扩展框架)来实现类似集成。配置逻辑类似,都是在IDE的MCP设置中指定Server的启动命令和参数。
实操心得:在配置路径时,使用绝对路径是最稳妥的。相对路径可能会因为工作目录不同而导致启动失败。另外,确保你的Node.js和Playwright环境在系统PATH中可访问。如果启动失败,可以尝试在终端中手动运行配置的命令行,查看具体的错误输出,这比在AI客户端里看模糊的错误信息要高效得多。
5. 实战:与AI协作编写并执行自动化测试
现在,激动人心的时刻到了。打开Claude Desktop,你可以开始和AI对话,让它指挥Playwright干活了。
5.1 基础指令测试
你可以直接给Claude下达自然语言指令:
- “请打开百度首页。”
- “现在点击一下搜索框。”
- “在搜索框里输入‘Playwright自动化测试’。”
- “按下回车键进行搜索。”
- “截个图给我看看结果。”
Claude在背后会将你的指令转化为对相应MCP工具的调用。例如,对于“打开百度首页”,它可能会生成类似这样的内部调用:
{ "method": "tools/call", "params": { "name": "navigate_to_url", "arguments": { "url": "https://www.baidu.com", "headless": false // 如果你想看到浏览器界面 } } }Server执行后,将结果(成功或失败信息)返回,Claude再组织语言告诉你。你可以要求它以非无头模式运行,亲眼看到浏览器在自动操作,这对调试和演示非常有帮助。
5.2 编写并执行一个完整的测试用例
更强大的用法是,让AI协助你编写一个完整的测试脚本,然后通过MCP Server执行。例如,你可以对Claude说:
“帮我写一个Playwright测试脚本,测试GitHub的登录功能。步骤是:1. 打开github.com/login。2. 找到用户名输入框并输入‘testuser’。3. 找到密码输入框并输入‘testpass’。4. 点击登录按钮。5. 检查页面是否出现了错误提示信息(通常包含‘Incorrect username or password’)。把脚本保存为github_login_test.js。”
Claude可能会生成一个Node.js脚本。然后,你可以进一步说:“现在,请用我们的Playwright MCP Server来运行这个脚本,并告诉我结果。”
为了实现这个,我们需要在Server端增加一个更强大的工具:execute_playwright_script。这个工具接收一段Playwright脚本代码(字符串),在Server端动态执行。
// server/tools/execution.js const { chromium } = require('playwright'); const vm = require('vm'); // 使用Node.js的vm模块在沙盒中执行代码,更安全 const executionTools = [ { name: 'execute_playwright_script', description: '执行一段Playwright测试脚本代码', inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'Playwright测试脚本的JavaScript代码字符串', }, headless: { type: 'boolean', default: true, }, }, required: ['code'], }, }, ]; async function executePlaywrightScript(code, headless = true) { let browser = null; try { // 在一个相对隔离的上下文中执行代码,并提供必要的Playwright模块 const context = vm.createContext({ require, console, setTimeout, clearTimeout, setInterval, clearInterval, Buffer, URL, URLSearchParams, // 注入 playwright 对象 playwright: { chromium, firefox: require('playwright').firefox, webkit: require('playwright').webkit }, // 注入一个快捷启动函数 async runTest() { browser = await playwright.chromium.launch({ headless }); const page = await browser.newPage(); return { browser, page }; } }); // 包装用户代码,确保最后关闭浏览器 const wrappedCode = ` (async () => { const { browser, page } = await runTest(); let result = { success: false, message: 'Script executed without explicit result.' }; try { ${code} result = { success: true, message: '脚本执行完成。' }; } catch (error) { result = { success: false, error: error.message, stack: error.stack }; } finally { await browser.close(); } return result; })() `; const scriptResult = await vm.runInContext(wrappedCode, context); return scriptResult; } catch (error) { // 确保发生顶级错误时也关闭浏览器 if (browser) { await browser.close().catch(() => {}); } return { success: false, error: `脚本执行失败: ${error.message}` }; } } module.exports = { executionTools, executePlaywrightScript, };将这个工具集成到主Server后,AI就可以将生成的测试脚本代码直接发送给Server执行了。这实现了从“自然语言描述测试用例”到“AI生成代码”再到“自动执行并反馈结果”的完整闭环。
5.3 调试与状态管理
在实际对话中,你可能会进行一系列操作。Server维护了浏览器实例(browser,page)的状态。这意味着你的操作是有上下文的。你可以问:“我刚才在哪个页面?”(对应get_page_title),或者“现在页面上有多少个按钮?”(这需要新增一个count_elements工具)。
这种状态保持的对话模式,使得AI能够像一个真正的测试伙伴一样,和你进行多轮交互,共同完成一个复杂的测试流程。
注意事项:动态执行任意代码(
execute_playwright_script)是极其危险的操作。绝对不要将这样的Server暴露给不受信任的AI模型或网络环境。这里仅为演示技术可能性,在生产环境中,必须对输入的代码进行严格的白名单校验、沙盒隔离,或者仅允许执行预定义好的测试脚本模板。
6. 常见问题与排查技巧实录
在实际搭建和使用的过程中,你肯定会遇到各种问题。下面是我踩过的一些坑和解决方案。
6.1 Server启动失败或连接超时
- 症状:Claude Desktop提示无法连接MCP Server,或者连接超时。
- 排查步骤:
- 检查配置文件路径:确保Claude配置文件中
args里的Node脚本路径是绝对路径,并且正确无误。 - 手动测试Server:在终端中,切换到Server目录,直接运行配置的命令,例如
node /path/to/index.js。观察是否有错误输出。常见的错误包括:- 模块未找到:
Error: Cannot find module '@modelcontextprotocol/sdk'。这说明依赖没有安装好,在Server目录下运行npm install。 - Playwright浏览器未安装:
Error: Executable doesn't exist at ...。运行npx playwright install。
- 模块未找到:
- 检查Node版本:确保Node.js版本符合要求(>=18)。使用
node --version确认。 - 查看Claude日志:Claude Desktop通常有应用日志。在macOS上,可以在终端用
log stream --predicate 'sender == "Claude"'查看实时日志,寻找MCP相关的错误信息。 - 重启Claude:每次修改MCP配置后,必须完全退出并重启Claude Desktop,否则配置不会生效。
- 检查配置文件路径:确保Claude配置文件中
6.2 工具调用无响应或返回错误
- 症状:AI可以列出工具,但调用时长时间无反应,或返回模糊的错误。
- 排查步骤:
- 在Server代码中添加详细日志:在
handleToolCall函数的开始和结束,以及每个工具函数内部,添加console.error(‘[LOG] …’)语句。这些日志会输出到Claude启动Server的子进程标准错误流,对于调试至关重要。 - 验证参数格式:AI(尤其是早期版本的Claude)在将自然语言转化为工具参数时可能出错。确保你的工具定义(
inputSchema)足够清晰,属性描述(description)能明确指导AI如何填充参数。例如,对于选择器,可以描述为“CSS选择器,如#login-button,或文本选择器,如text=登录”。 - 处理异步超时:Playwright操作(如
page.goto,page.click)可能有网络或元素加载超时。在工具函数中合理设置timeout参数,并在返回给Client的错误信息中明确提示超时原因。 - 浏览器上下文问题:如果多个工具调用间页面被意外关闭或导航,后续工具会失败。考虑在Server中增加更健壮的状态检查,或在工具调用开始时尝试恢复页面上下文。
- 在Server代码中添加详细日志:在
6.3 AI无法正确理解或调用工具
- 症状:AI似乎“忘记”了可用的工具,或者调用了一个不存在的工具。
- 排查步骤:
- 检查工具列表:在Claude中,你可以直接问:“你现在可以使用哪些MCP工具?” 一个配置正确的Claude应该能列出你在Server中定义的所有工具。如果列表为空或不全,说明Server连接或
tools/list接口有问题。 - 工具命名和描述:工具的名称(
name)应简洁明了,如navigate_to_url。描述(description)应尽可能详细、无歧义,说明工具的作用、输入参数的意义。好的描述是AI正确使用工具的关键。 - MCP协议版本:确保你使用的
@modelcontextprotocol/sdk版本与Claude Desktop兼容。有时版本不匹配会导致通信问题。查看SDK和Client的文档,使用稳定的版本组合。
- 检查工具列表:在Claude中,你可以直接问:“你现在可以使用哪些MCP工具?” 一个配置正确的Claude应该能列出你在Server中定义的所有工具。如果列表为空或不全,说明Server连接或
6.4 性能与资源管理
- 症状:长时间对话后,系统内存占用变高,或者浏览器实例僵死。
- 优化建议:
- 实现会话隔离:目前的简单实现使用全局单例的
browser和page。在多人使用或长时间对话场景下,这会导致状态混乱。一个更专业的实现是为每个Claude对话会话(或每个工具调用)创建独立的浏览器上下文(browserContext),并在会话结束时清理。 - 增加资源清理工具:提供显式的
close_browser工具,让AI可以在完成一系列测试后主动释放资源。同时,在Server端设置超时,长时间无活动的浏览器实例自动关闭。 - 使用Playwright的复用连接:Playwright支持连接到远程的、已运行的浏览器实例(如
playwright connect)。可以考虑将浏览器作为独立服务运行,MCP Server作为客户端去连接,实现更好的资源管理和横向扩展。
- 实现会话隔离:目前的简单实现使用全局单例的
6.5 安全边界与生产化思考
- 风险:如前所述,允许AI通过MCP执行浏览器自动化操作,甚至动态执行代码,存在巨大安全风险。
- 加固措施:
- 网络隔离:确保MCP Server只监听本地回环地址(localhost),绝不暴露到公网。
- 命令/参数限制:对工具参数进行严格校验和过滤。例如,
navigate_to_url工具可以限制只能访问特定的域名白名单。 - 禁用危险工具:类似
execute_playwright_script这样的动态代码执行工具,在非完全受控的环境下应禁用。 - 身份验证:如果未来MCP支持,可以为Server添加简单的API密钥认证,确保只有受信的Client可以连接。
- 审计日志:记录所有工具调用请求和结果,便于事后审查和问题追踪。
搭建和调试这个过程,最深的体会是,MCP协议为AI应用打开了一扇通往真实世界操作的大门,而Playwright提供了一个极其强大和稳定的操作执行环境。这个组合的潜力远不止于自动化测试,它可以用于数据抓取、日常办公流程自动化、网站监控等众多场景。关键在于,我们作为开发者,需要设计好安全、可靠、易用的“工具”,并清晰地教会AI何时以及如何使用它们。这不仅仅是技术集成,更是一种新的人机协作模式的设计。