尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

AI-Extension 实战:用 MCP 让 AI 真的「看得到、动得了」你的浏览器

发布时间:2026/9/28 19:24:40

资讯中心
01
ARTICLE

AI-Extension 实战:用 MCP 让 AI 真的「看得到、动得了」你的浏览器

AI-Extension 实战:用 MCP 让 AI 真的「看得到、动得了」你的浏览器
1. 为什么你的 AI 助手看不见浏览器里的按钮你有没有遇到过这种尴尬让 AI 帮你处理一个网页表单它洋洋洒洒写了一大段操作步骤结果你复制粘贴过去发现——它根本不知道页面上有哪些输入框、按钮叫什么名字、提交后跳到了哪里。AI 能写代码、能聊天但它对浏览器里真实发生的 DOM 结构、元素状态、页面跳转一无所知。这就是当前 AI Agent 落地时最典型的断层模型有推理能力却没有感知和操作浏览器的手段。你让它「帮我在后台系统里新建一条任务」它只能回复「请点击新建按钮」——可它连那个按钮的 UID 都拿不到。AI-Extension 这类方案要解决的就是这件事。它基于 WXT 框架构建浏览器扩展把 MCPModel Context Protocol协议塞进浏览器环境让任意网页变成可被 AI 调用的工具集合。模型通过 MCP 服务端暴露的工具先调用takeSnapshot拿到页面无障碍树快照每个可交互节点被分配唯一 UID然后精确执行click、fill、selectOption等动作操作完再刷新快照确认结果。整个过程不需要改你现有应用的任何一行代码。这篇文章面向想从零搭一个「AI 能读 DOM、能触发点击填表」的扩展骨架的开发者。我会给出manifest.json与 MCP 服务端配置的可复制骨架附本地加载扩展、验证 AI 读取页面结构与执行动作的完整步骤。适合谁前端工程师、AI 应用开发者、想给内部系统加 AI 操作能力的团队。读完你能得到一个可跑通的最小闭环而不是停留在概念层。2. 前置准备TaoToken 与 MCP 服务端配置在写扩展代码之前先把模型调用这一环打通。AI-Extension 的 MCP 服务端本身不绑定模型厂商它只负责把浏览器能力暴露成标准 MCP 工具。真正做推理和工具调用的模型需要你提供一个兼容 OpenAI 接口的端点。我实测下来用 TaoToken 的 API 接入最省事它同时支持模型对话和 Coding Plan 两种模式前者适合验证「AI 能不能读到 DOM」后者适合长期跑编码和 Agent 任务。你需要先拿到一个 API Key。访问 API Keys 管理页创建密钥https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite创建后复制 Key后面在 MCP 服务端配置里会用到。注意不要把它硬编码进前端扩展代码扩展的 content script 运行在用户浏览器里Key 泄露风险很高。正确做法是把 Key 放在本地 MCP 服务端的配置文件或环境变量里扩展只通过本地 WebSocket 与服务端通信。TaoToken 的 API 基础地址是https://taotoken.net/api这个地址不加任何 UTM 参数直接用于代码里的baseURL配置。模型对话的调试入口在这里你可以先用它验证 Key 是否可用https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite如果你打算长期跑编码类 Agent 任务比如让 AI 自动改代码、跨页面填表、批量操作后台建议看一下 Coding Plan它的额度和并发策略更适合持续调用https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite接入文档在这里里面有完整的 MCP 工具注册示例和参数说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你用的是 Claude Code 这类 Anthropic 系工具配置方式略有不同参考这个页面https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite前置准备的核心就三件事拿到 Key、确认 API 地址、选好模型对话还是 Coding Plan。接下来进入扩展骨架的搭建。3. 可复制配置manifest.json 与 MCP 服务端骨架3.1 扩展目录结构先建一个最小可跑的目录。我用 WXT 的约定式结构但为了让你不装 WXT 也能看懂这里用原生 manifest v3 的写法WXT 只是帮你打包和热更新。ai-extension-demo/ ├── manifest.json ├── background.js ├── content-script.js ├── mcp-server/ │ ├── server.js │ └── tools/ │ └── page-tools.js └── package.json3.2 manifest.json 骨架这是扩展的入口配置。关键点是permissions里要开scripting、activeTab、storagehost_permissions要覆盖你想操作的域名。content_scripts负责注入页面读取 DOM 的脚本。{ manifest_version: 3, name: AI-Extension Demo, version: 0.1.0, description: 让 AI 通过 MCP 读取页面结构并执行点击、填表动作, permissions: [ scripting, activeTab, storage, tabs ], host_permissions: [ https://*/*, http://localhost/* ], background: { service_worker: background.js, type: module }, content_scripts: [ { matches: [https://*/*], js: [content-script.js], run_at: document_idle } ], action: { default_title: AI-Extension } }注意host_permissions不要无脑写all_urls生产环境按需收窄。本地调试时http://localhost/*是给 MCP 服务端 WebSocket 用的。3.3 content-script.js读取 DOM 与执行动作content script 运行在页面隔离环境里负责两件事把页面可交互元素整理成结构化快照以及接收指令执行点击和填表。// content-script.js function takeSnapshot() { const interactiveSelectors [ button, a[href], input, select, textarea, [rolebutton], [onclick] ]; const nodes document.querySelectorAll(interactiveSelectors.join(,)); const snapshot []; let uid 0; nodes.forEach((el) { const rect el.getBoundingClientRect(); if (rect.width 0 || rect.height 0) return; const id uid-${uid}; el.setAttribute(data-ai-uid, id); snapshot.push({ uid: id, tag: el.tagName.toLowerCase(), text: (el.innerText || el.value || el.placeholder || ).trim().slice(0, 80), type: el.type || null, name: el.name || null, visible: true }); }); return { url: location.href, title: document.title, nodes: snapshot }; } function clickByUid(uid) { const el document.querySelector([data-ai-uid${uid}]); if (!el) return { ok: false, error: element not found }; el.click(); return { ok: true, uid }; } function fillByUid(uid, value) { const el document.querySelector([data-ai-uid${uid}]); if (!el) return { ok: false, error: element not found }; el.focus(); el.value value; el.dispatchEvent(new Event(input, { bubbles: true })); el.dispatchEvent(new Event(change, { bubbles: true })); return { ok: true, uid, value }; } chrome.runtime.onMessage.addListener((msg, sender, sendResponse) { if (msg.type TAKE_SNAPSHOT) { sendResponse(takeSnapshot()); } else if (msg.type CLICK) { sendResponse(clickByUid(msg.uid)); } else if (msg.type FILL) { sendResponse(fillByUid(msg.uid, msg.value)); } return true; });这段代码的核心是data-ai-uid属性。每次快照给元素打上唯一 UIDAI 后续的点击和填表都通过 UID 定位避免用文本匹配带来的歧义。fillByUid里手动派发input和change事件是因为很多前端框架React、Vue依赖这些事件同步状态直接改value不会触发框架更新。3.4 background.js桥接扩展与 MCP 服务端background service worker 负责把 content script 的能力通过 WebSocket 转发给本地 MCP 服务端。// background.js let ws null; function connectMcpServer() { ws new WebSocket(ws://localhost:8765); ws.onopen () { console.log([AI-Extension] MCP server connected); ws.send(JSON.stringify({ type: REGISTER, client: browser-extension })); }; ws.onmessage async (event) { const msg JSON.parse(event.data); const [tab] await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab) return; if (msg.type TAKE_SNAPSHOT) { const result await chrome.tabs.sendMessage(tab.id, { type: TAKE_SNAPSHOT }); ws.send(JSON.stringify({ id: msg.id, result })); } else if (msg.type CLICK) { const result await chrome.tabs.sendMessage(tab.id, { type: CLICK, uid: msg.uid }); ws.send(JSON.stringify({ id: msg.id, result })); } else if (msg.type FILL) { const result await chrome.tabs.sendMessage(tab.id, { type: FILL, uid: msg.uid, value: msg.value }); ws.send(JSON.stringify({ id: msg.id, result })); } }; ws.onclose () { console.log([AI-Extension] MCP server disconnected, retry in 3s); setTimeout(connectMcpServer, 3000); }; } connectMcpServer();3.5 MCP 服务端注册工具并调用模型服务端用 Node.js 写监听 WebSocket同时把浏览器能力注册成 MCP 工具。模型通过 TaoToken API 调用工具调用结果回传给模型。// mcp-server/server.js import { WebSocketServer } from ws; import OpenAI from openai; const openai new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY, baseURL: https://taotoken.net/api }); const wss new WebSocketServer({ port: 8765 }); let extensionSocket null; const pending new Map(); let msgId 0; function callExtension(type, payload {}) { return new Promise((resolve, reject) { if (!extensionSocket) return reject(new Error(extension not connected)); const id msg-${msgId}; pending.set(id, resolve); extensionSocket.send(JSON.stringify({ id, type, ...payload })); setTimeout(() { if (pending.has(id)) { pending.delete(id); reject(new Error(extension call timeout)); } }, 5000); }); } wss.on(connection, (socket) { extensionSocket socket; socket.on(message, (data) { const msg JSON.parse(data); if (msg.id pending.has(msg.id)) { pending.get(msg.id)(msg.result); pending.delete(msg.id); } }); }); const tools [ { type: function, function: { name: takeSnapshot, description: 获取当前页面可交互元素快照返回每个元素的 uid、标签、文本, parameters: { type: object, properties: {} } } }, { type: function, function: { name: click, description: 根据 uid 点击页面元素, parameters: { type: object, properties: { uid: { type: string, description: 元素唯一标识 } }, required: [uid] } } }, { type: function, function: { name: fill, description: 根据 uid 向输入框填入文本, parameters: { type: object, properties: { uid: { type: string }, value: { type: string } }, required: [uid, value] } } } ]; async function runAgent(userInput) { const messages [ { role: system, content: 你是浏览器操作助手。先调用 takeSnapshot 了解页面再执行 click 或 fill。 }, { role: user, content: userInput } ]; while (true) { const res await openai.chat.completions.create({ model: gpt-4o-mini, messages, tools, tool_choice: auto }); const choice res.choices[0].message; messages.push(choice); if (!choice.tool_calls || choice.tool_calls.length 0) { return choice.content; } for (const call of choice.tool_calls) { const args JSON.parse(call.function.arguments || {}); let result; if (call.function.name takeSnapshot) { result await callExtension(TAKE_SNAPSHOT); } else if (call.function.name click) { result await callExtension(CLICK, { uid: args.uid }); } else if (call.function.name fill) { result await callExtension(FILL, { uid: args.uid, value: args.value }); } messages.push({ role: tool, tool_call_id: call.id, content: JSON.stringify(result) }); } } } runAgent(帮我在当前页面找到搜索框并填入 MCP 教程).then(console.log);这段代码里baseURL指向 TaoToken 的 API 地址model可以换成你账号下支持的任意模型。工具定义里的description很关键模型靠它判断何时调用哪个工具写得越具体调用越准。4. 验证请求本地加载扩展并跑通闭环4.1 加载扩展打开 Chrome 或 Edge地址栏输入chrome://extensions右上角开启「开发者模式」点击「加载已解压的扩展程序」选择ai-extension-demo目录。加载成功后你会看到扩展卡片点开「Service Worker」可以看 background 日志。4.2 启动 MCP 服务端在mcp-server目录下安装依赖并启动npm init -y npm install ws openai export TAOTOKEN_API_KEY你的Key node server.js启动后终端会显示 WebSocket 监听在 8765。回到扩展的 Service Worker 控制台应该能看到MCP server connected。4.3 验证 AI 读取 DOM打开任意一个带搜索框的页面比如一个文档站。在 MCP 服务端终端里runAgent会自动执行。你会在日志里看到模型先调用takeSnapshot返回的 JSON 里包含页面上所有可交互元素的 uid 和文本。如果快照返回的nodes数组为空检查 content script 是否注入成功。在页面控制台输入document.querySelectorAll([data-ai-uid]).length正常应该大于 0。4.4 验证 AI 执行动作模型拿到快照后会选择一个 uid 调用fill。你观察页面搜索框应该被自动填入文本并且输入框的边框或状态有变化说明input事件生效了。接着模型可能调用click点击搜索按钮页面跳转到结果页。整个闭环跑通后你可以在runAgent里换不同的指令比如「点击登录按钮」「在表单里填入邮箱」验证 AI 是否能正确选择 uid 并执行。5. 本篇常见错排查扩展加载后 content script 不注入检查manifest.json里matches是否覆盖当前页面域名。chrome://开头的页面不允许注入换普通网页测试。WebSocket 连接失败确认 MCP 服务端已启动且端口是 8765。如果扩展和 service worker 在不同网络环境localhost可能解析异常改用127.0.0.1。模型不调用工具直接回复文字检查tools数组是否传给了chat.completions.create以及tool_choice是否为auto。有些模型对工具描述敏感把description写得更具体比如「获取当前页面所有可点击按钮和输入框的 uid」。fill 后页面状态没更新确认fillByUid里派发了input和change事件。React 受控组件必须靠这两个事件同步只改value无效。快照里元素重复或缺失takeSnapshot里过滤了rect.width 0的隐藏元素如果页面用visibility: hidden隐藏元素但仍有尺寸会被误判为可见。可以加getComputedStyle(el).visibility ! hidden判断。API 返回 401检查TAOTOKEN_API_KEY环境变量是否设置正确以及baseURL是否为https://taotoken.net/api。Key 不要有多余空格。工具调用超时callExtension里设了 5 秒超时如果页面元素多、快照生成慢可以调到 10 秒。同时确认 background 的 WebSocket 没有断开。6. 下一步把浏览器能力接进你的工作流跑通这个最小闭环后你可以做几件事让它更实用。第一把takeSnapshot的过滤逻辑按你的目标站点定制比如只抓取表单区域减少 token 消耗。第二给 MCP 服务端加一个 HTTP 接口这样 Cursor、Claude Code 这类工具可以直接通过 MCP 协议连进来不用自己写runAgent。第三把常用操作封装成 Skills比如「填报销单」「批量改商品价格」模型识别意图后自动调用对应工具链。如果你在接入过程中遇到工具注册或模型调用的问题可以对照接入文档里的示例排查https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite需要长期跑编码和 Agent 任务的话Coding Plan 的并发和额度策略比按次调用更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite我踩过的一个坑是一开始把 API Key 写进了 content script结果扩展一发布就等于公开了密钥。后来改成 Key 只存在本地 MCP 服务端扩展通过 WebSocket 通信才安全。你搭的时候直接按本文的结构来能省掉这一步返工。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。