ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

浏览器操作录制生成 MCP Server:给大模型装一双能操作网页的手

浏览器操作录制生成 MCP Server:给大模型装一双能操作网页的手 最近我折腾了一个小工具名字暂时叫 WebRecorder2MCP功能一句话概括你在浏览器里正常操作一遍比如打开页面、输入关键词、点击搜索、翻到第二页我把这些动作全部录下来自动生成一个可以被大模型直接调用的 MCP Server。以后大模型在想完成同类任务的时候只需要发一个 tool call就能自动复现你刚才录的那套操作等于给模型装了一双能操作网页的手。这个想法不稀奇很多团队在做浏览器自动化和 MCP 的结合。但做得顺手的方案其实不多——要么要求你懂 Playwright 脚本怎么写要么要求你熟悉 MCP 协议细节更麻烦的是大部分现成方案把“浏览器遥控器”直接交给模型模型自由发挥的结果往往是一通乱点。我想要的体验很朴素业务人员完全不懂代码录一遍操作就得到一个标准、安全、可被大模型调用的 MCP 工具。这篇文章就把我这套“录制 → 转换 → MCP Server”的完整实现思路和踩坑过程记录下来。1. 整体设计为什么必须走“录制 → 转换 → MCP Server”这条路1.1 解决的真实痛点大模型本身不能直接操作网页。虽然现在很多模型支持截图理解和多模态但真要完成“填表单 → 点查询 → 读取结果 → 点击下一页”这种多步操作还是得靠外部工具。MCPModel Context Protocol解决的核心问题就是给大模型挂工具的标准化方式。它是一套基于 JSON-RPC 的软件协议而不是硬件协议。类比一下USB-C 是硬件接口标准让不同设备能插同一个口子MCP 是软件接口标准让不同大模型能调用同一个工具集。问题在于大多数业务人员不懂怎么写 MCP Server。就算懂代码的人为每个网页流程单独封装一个 tool 也很烦。每次都要起项目、定义输入输出、写 handler、处理 Playwright 重放逻辑流程重复度太高。录制方案的价值就在这里操作步骤你来做协议代码和重放逻辑我生成。录完一套动作不仅自己能复用还能把这个能力开放给任何支持 MCP 的大模型客户端这才是它跟普通自动化脚本的本质区别。1.2 对比 Playwright MCP 和 Browser Use MCP差异化在哪现在社区里被高频拿来对比的两个方案是 Playwright MCP 和 Browser Use。前者是微软官方出的把 Playwright 的核心能力暴露成通用浏览器操作工具模型可以说“打开某个网址”“点击某个按钮”“截图看看当前页面”后者更像一个 Agent 框架让模型自主规划浏览器操作。这两个方案有一个共同问题粒度太粗模型有自由发挥空间很容易在关键节点上操作失误而且缺少“业务语义”。我做的工具定位完全不同录制好的动作被打包成一个业务级 tool。比如录了一个“查询库存”的流程对外暴露的 tool 就是run_stock_query模型只能选择调用或者不调用不能在调用过程中插入“顺便点一下其他按钮”这种自由操作。可以这么理解通用浏览器 MCP 是给模型一个遥控器我这个工具是给模型一个已经录好的宏按一下就跑完整套动作。这样既保留了大模型的调度能力又锁死了执行路径重放成功率要高得多。1.3 三个中间产物的设计整个工具能跑通核心是定义好三个中间产物缺一不可。第一个是录制阶段产生的动作列表。一条动作最少包含动作类型click/input/scroll/wait、目标选择器、补充参数输入值、滚动偏移、等待时间等。第二个是录制结束后导出的静态 JSON 文件这个文件必须能脱离录制器独立重放也就是说只要给它一个 Playwright 运行时它就能原样执行。第三个是 MCP Server 注册时的 tool definition 和 handler。这三个产物解耦之后录制端只要能产出 JSON转换端只要能消费 JSON就能自由组合。我在实现里还额外加了一个 manifest 文件用来描述每个工具的名称、用途和参数 schema这样大模型看到工具列表的时候能清楚地知道“什么场景该调用哪个流程”。2. 核心细节解析与实操要点2.1 录制模块不只是记录鼠标键盘录制听起来简单浏览器里的坑却不少。直接监听 click 和 keydown 远远不够因为现代页面大多是 React、Vue 动态渲染你录下来的坐标位置页面一刷新就全变了。我实现的录制器是在页面里注入一段脚本捕获 PointerDown、PointerUp、Input、Change、Scroll、KeyDown 等事件然后通过document.elementFromPoint(x, y)找到真实元素再生成一个稳定的选择器。对于滚动操作不要傻傻地录滚动条位置要记录“哪个容器滚动了多少”同时记录滚动容器本身的选择器因为页面尺寸和布局随时会变。输入操作更要注意。很多框架对输入事件的触发方式很敏感有的组件监听input有的监听change有的还需要失焦事件才确认。重放的时候我统一用 Playwright 的pressSequentially来模拟逐字输入而不是直接设置 value。这样更接近真人操作也能触发前端框架的响应式更新。录制的时候还有一个细节如果遇到网络请求导致的加载等待不能只记一个固定 sleep更好的做法是把“等待某个元素出现”也作为一种动作类型记录下来。2.2 生成稳定选择器录制链路里最影响成功率的一环选择器稳定性直接决定重放成败。最简单的做法是记录 DOM 索引路径比如body div:nth-child(2) button:nth-child(3)但这种路径在页面局部更新后极其脆弱。我生成选择器时按优先级走优先用开发者显式提供的>mkdir web-recorder-mcp cd web-recorder-mcp npm init -y npm install modelcontextprotocol/sdk playwright zod npm install -D typescript tsx types/node npx playwright install chromium这里解释一下为什么要装 zod。MCP SDK 支持用 Zod schema 来定义工具参数Zod 会帮你在运行时做校验同时自动生成 MCP 需要的 JSON Schema 描述。少写很多手写 schema 的样板代码。Playwright 负责录制和重放两端的能力浏览器我用的是 Chromium主要是录制和重放行为一致性最好。3.2 实现一个轻量录制器录制器的实现思路是在页面里注入一段脚本。这里给出一个简化版本核心是监听关键事件并生成动作记录。为了控制篇幅我省略了选择器生成的具体实现但逻辑闭环是完整的// recorder.ts import { chromium, Page } from playwright; type Action { type: click | input | scroll | wait; selector?: string; value?: string; waitForSelector?: string; }; const injectRecorder (page: Page) { return page.evaluate(() { const actions: any[] []; const recordClick (e: MouseEvent) { const el document.elementFromPoint(e.clientX, e.clientY); if (!el) return; const selector generateStableSelector(el); // 核心选择器逻辑 actions.push({ type: click, selector }); }; const recordInput (e: Event) { const el e.target as HTMLInputElement; if (!el || !el.value) return; const selector generateStableSelector(el); actions.push({ type: input, selector, value: el.value }); }; document.addEventListener(pointerdown, recordClick); document.addEventListener(input, recordInput); window.__recordedActions actions; }); }; export async function startRecording(url: string) { const browser await chromium.launch({ headless: false }); const page await browser.newPage(); await page.goto(url); await injectRecorder(page); return { browser, page }; } export function stopRecording(page: Page): Action[] { return page.evaluate(() window.__recordedActions || []); }实际操作时你可以录制一个完整的搜索流程打开目标站点、输入关键词、点击搜索按钮、等待结果加载、点击下一页。录制结束后拿到的 JSON 就是第二小节里说到的中间产物。这个 JSON 是干净的纯数据不携带任何浏览器状态所以它可以被提交到 Git、写入数据库或者直接作为配置文件传给转换器。3.3 将录制 JSON 转换为 MCP Server拿到了 JSON 动作列表接下来要做的是把它变成一个 MCP Server。核心逻辑是读取动作 JSON启动一个 Playwright 浏览器逐条执行动作最后把关键结果返回给模型。下面这段代码展示的是 MCP Server 的主干部分// mcp-server.ts import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { chromium } from playwright; import { z } from zod; import recordedActions from ./recorded-flow.json; const server new McpServer({ name: web-recorder-mcp, version: 1.0.0, }); async function executeActions(actions: Action[], input: Recordstring, string) { const browser await chromium.launch({ headless: true }); const page await browser.newPage(); let result ; for (const action of actions) { switch (action.type) { case click: await page.locator(action.selector!).click(); break; case input: { const value action.value!.replace(/\{\{(\w)\}\}/g, (_, k) input[k] ?? ); await page.locator(action.selector!).pressSequentially(value, { delay: 30 }); break; } case scroll: await page.locator(action.selector!).evaluate((el, top) { el.scrollTop top; }, action.value); break; case wait: await page.waitForTimeout(action.value ? Number(action.value) : 1000); break; } } // 取结果通常录制的最后一步是读取某个区域的文本 const finalStep actions.find((a) a.selector?.includes(result)); if (finalStep) { result await page.locator(finalStep.selector!).innerText(); } await browser.close(); return result; } server.tool( run_recorded_flow, 按录制流程打开页面、搜索关键词并返回结果区域文本, { keyword: z.string().describe(搜索关键词) }, async ({ keyword }) { const result await executeActions(recordedActions, { keyword }); return { content: [{ type: text, text: result }] }; } ); const transport new StdioServerTransport(); await server.connect(transport);这段代码里有一个细节值得说明录制 JSON 里的输入值不一定是字面值我支持用{{keyword}}这种模板语法占位。录制的时候你随便输入一个“测试关键词”转换的时候真实值由大模型通过 tool 参数传入。这样录制的流程就变成了一个可复用的模板而不是写死的一次性脚本。3.4 在大模型客户端里验证 tool 调用Server 写好之后本地先用 MCP 客户端 SDK 做一个冒烟测试不需要急着接大模型。下面这段代码列出并调用刚才注册的工具// smoke-test.ts import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const transport new StdioClientTransport({ command: npx, args: [tsx, mcp-server.ts], }); const client new Client({ name: smoke, version: 1.0.0 }); await client.connect(transport); const tools await client.listTools(); console.log(tools.tools.map((t) t.name)); const result await client.callTool({ name: run_recorded_flow, arguments: { keyword: MCP 协议 }, }); console.log(result);跑通这里整个链路已经通了。接下来要做的就是把这个 Server 注册到支持 MCP 的客户端配置里比如 Claude Desktop 的claude_desktop_config.json或者任何可以配置 MCP Server 的 IDE。配置方式和普通 MCP Server 一模一样没人能看出这个 Server 底层是“录制出来的”。模型只需要看 tool 名和描述就知道什么时候该调它。4. 常见问题与排查技巧实录4.1 元素选择器在重放时失效这是最常见的翻车点。录制时明明定位得很好一重放就找不到元素。原因通常有两种一是页面里存在动态生成的随机 ID 或 class每次加载都不一样二是列表页局部刷新元素顺序发生了改变。我的排查顺序是先打开浏览器 DevTools在重放失败时把页面的 DOM 导出来对比录制时的选择器是否还存在。如果是随机 ID去检查选择器生成逻辑优先用>
返回列表