1. 浏览器里跑通 MCP:AI-Extension 与 WXT 到底解决了什么问题
AI-Extension 是 OpenTiny 团队基于 WXT 框架开发的一款智能浏览器扩展,核心思路是把 MCP(Model Context Protocol)协议搬进浏览器,让 AI 助手不再只是“读网页”,而是能真正“点按钮、填表单、跳页面”。如果你之前用过 Cursor、Claude、Coze 这类工具,会发现它们对网页的感知基本停留在文本或截图层面,遇到动态渲染的 Vue/React 组件、需要登录态的内部系统,就很容易卡住。AI-Extension 的价值就在于:它作为浏览器扩展运行,天然复用你的 Cookie、缓存和登录态,同时通过无障碍树解析和视觉模型拿到页面结构快照,再把 click、fill、select 这些操作封装成 MCP 工具暴露给 AI。
WXT 是这套方案的工程底座。它是一个面向现代浏览器扩展的开发框架,支持 Manifest V3、多浏览器构建、热更新和 TypeScript 开箱即用。相比手写 manifest.json 和 webpack 配置,WXT 的目录约定和自动导入能省掉大量样板代码。我这次的目标很明确:用 WXT 搭一个最小扩展骨架,把 MCP 客户端接进去,然后把请求端点统一改到 TaoToken 的 API 通道,最后在本地 Chrome 里跑通一次完整的 MCP 调用链路。适合谁?适合想自己动手做浏览器侧 AI 自动化、又不想从零造轮子的前端或全栈同学。
整个链路可以拆成四层:WXT 扩展骨架负责生命周期和页面注入;MCP 客户端负责和模型侧通信;TaoToken 统一 Key/API 通道负责鉴权和模型路由;浏览器侧工具层负责把 DOM 操作注册成 MCP 工具。下面按可跟做的顺序展开,每一步都给到可复制的配置和命令。
2. 前置准备:TaoToken 统一 Key 与 API 通道配置
在写代码之前,先把模型侧的入口准备好。TaoToken 在这里扮演的是统一 Key/API 通道的角色,你不需要在扩展里硬编码多个厂商的 Key,而是通过一个端点做模型路由。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数。
第一步是拿到 API Key。进入控制台后创建密钥,建议按项目维度命名,比如wxt-mcp-extension,方便后续轮换。创建完成后你会得到一串以sk-开头的 Key,先复制到本地临时文件,后面写进.env时用。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
第二步是确认模型 ID。不同任务对模型能力要求不同,MCP 工具调用场景建议选支持 function calling 的模型。你可以在模型对话页先做一次简单验证,地址是 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,输入一句“请用 JSON 返回一个 click 工具的定义”,看返回结构是否符合预期。
第三步是理解鉴权方式。TaoToken 的 API 走标准 Bearer Token,请求头形如Authorization: Bearer sk-xxxx。在浏览器扩展里,这个 Key 不能直接写进前端代码,因为扩展包是可以被解压查看的。推荐做法是:开发阶段用.env注入,构建时通过 WXT 的import.meta.env读取;生产环境则应该走你自己的后端做代理,扩展只持有短期令牌。这一点在后面的配置片段里会体现。
如果你后续要做长期编码或 Agent 类任务,可以了解 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,它更适合持续性的开发场景。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,遇到参数疑问优先查这里。
3. WXT 项目骨架与 MCP 客户端可复制配置
先初始化 WXT 项目。确保 Node 版本在 18 以上,然后执行:
npx wxt@latest init ai-extension-mcp --template react-ts cd ai-extension-mcp npm installWXT 的目录约定是:entrypoints/放各个入口,components/放 UI 组件,public/放静态资源。默认会生成entrypoints/background.ts和entrypoints/popup/。我们要加一个 content script 用来注入页面工具层,再加一个 sidepanel 作为对话界面。
先改wxt.config.ts,声明权限和 host 权限:
import { defineConfig } from 'wxt'; export default defineConfig({ manifest: { name: 'AI-Extension MCP', permissions: ['storage', 'scripting', 'activeTab', 'sidePanel'], host_permissions: ['<all_urls>'], side_panel: { default_path: 'sidepanel.html', }, }, });接着配置环境变量。在项目根目录建.env:
VITE_TAOTOKEN_BASE_URL=https://taotoken.net/api VITE_TAOTOKEN_API_KEY=sk-你的Key VITE_MCP_MODEL_ID=你的模型ID注意.env要加进.gitignore,别提交。然后在entrypoints/background.ts里写 MCP 客户端的最小实现。这里用 fetch 直接调 TaoToken 的 chat completions 端点,把工具定义传进去:
export default defineBackground(() => { const BASE_URL = import.meta.env.VITE_TAOTOKEN_BASE_URL; const API_KEY = import.meta.env.VITE_TAOTOKEN_API_KEY; const MODEL_ID = import.meta.env.VITE_MCP_MODEL_ID; const tools = [ { type: 'function', function: { name: 'click_element', description: '点击页面上的元素', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS 选择器' }, }, required: ['selector'], }, }, }, { type: 'function', function: { name: 'fill_input', description: '向输入框填写内容', parameters: { type: 'object', properties: { selector: { type: 'string' }, value: { type: 'string' }, }, required: ['selector', 'value'], }, }, }, ]; chrome.runtime.onMessage.addListener(async (msg, _sender, sendResponse) => { if (msg.type !== 'MCP_CALL') return; const res = await fetch(`${BASE_URL}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}`, }, body: JSON.stringify({ model: MODEL_ID, messages: msg.messages, tools, tool_choice: 'auto', }), }); const data = await res.json(); sendResponse(data); return true; }); });这段代码的关键点:tools数组就是 MCP 工具在模型侧的声明,模型返回tool_calls后,由 content script 执行真实 DOM 操作。Base URL、Key、Model ID 三件套全部来自环境变量,符合统一通道的接入方式。
再写 content script,负责执行工具调用:
export default defineContentScript({ matches: ['<all_urls>'], main() { chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (msg.type !== 'EXEC_TOOL') return; const { name, args } = msg; try { if (name === 'click_element') { const el = document.querySelector(args.selector); if (!el) throw new Error('元素未找到'); (el as HTMLElement).click(); sendResponse({ ok: true }); } else if (name === 'fill_input') { const el = document.querySelector(args.selector) as HTMLInputElement; if (!el) throw new Error('输入框未找到'); el.value = args.value; el.dispatchEvent(new Event('input', { bubbles: true })); sendResponse({ ok: true }); } } catch (e) { sendResponse({ ok: false, error: (e as Error).message }); } return true; }); }, });到这里,WXT 骨架、MCP 客户端、工具执行层就齐了。接下来是加载验证。
4. 加载扩展并验证一次完整 MCP 调用链路
构建开发版本:
npm run devWXT 会输出一个.output/chrome-mv3-dev目录。打开 Chrome,地址栏输入chrome://extensions,右上角开启“开发者模式”,点“加载已解压的扩展程序”,选择.output/chrome-mv3-dev。加载成功后,扩展列表里会出现 AI-Extension MCP。
接着打开任意一个测试页面,比如一个带登录表单的本地 HTML。点扩展图标打开 sidepanel,在输入框里发一句:“帮我点击 id 为 login-btn 的按钮”。sidepanel 会把消息发给 background,background 调 TaoToken 的 chat completions,模型返回tool_calls,background 再把EXEC_TOOL消息发给 content script,content script 执行document.querySelector('#login-btn').click()。
验证成功的标志有三个:一是 sidepanel 里能看到模型返回的 tool_calls 结构;二是页面上的按钮真的被点击了;三是 background 的 Network 面板里能看到对https://taotoken.net/api/v1/chat/completions的 200 响应。如果这三步都通,说明 MCP 调用链路在浏览器侧跑通了。
如果你想更直观地看模型返回,可以在 sidepanel 里加一段渲染逻辑,把data.choices[0].message.tool_calls打印出来。实测下来,第一次调用可能会有几百毫秒延迟,属于正常范围。另外注意:content script 默认运行在隔离环境,拿不到页面 JS 内存里的变量,如果你要读 Vue/React 状态,需要把world改成MAIN,但那样会牺牲一部分安全性,按需选择。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来对照,都是我在接入过程中遇到或社区里高频出现的。
401 Unauthorized:最常见的原因是 Key 没读到或格式不对。先检查.env里VITE_TAOTOKEN_API_KEY是否以sk-开头,然后确认构建时环境变量真的被注入——WXT 只暴露VITE_前缀的变量。如果 Key 正确但仍 401,检查请求头是不是写成了Authorization: sk-xxx,正确格式是Bearer sk-xxx。另外 Key 被删除或过期也会 401,去 API Keys 页面确认状态。
local proxy failed:这个报错通常出现在你本地起了代理服务、但扩展请求没走通的情况下。排查顺序是:先确认VITE_TAOTOKEN_BASE_URL写的是https://taotoken.net/api而不是带路径的完整端点;再确认扩展的 host_permissions 包含<all_urls>;最后看 background 的 console 有没有跨域报错。如果是 Manifest V3,fetch 在 background service worker 里发起,不受页面 CORS 限制,但需要确保 service worker 没被浏览器休眠中断。
reading 'choices' of undefined:这个报错说明data.choices是 undefined,通常是响应体不是预期的 JSON 结构。可能原因有三个:一是请求打到了错误端点,比如漏了/v1;二是模型 ID 写错,服务端返回了错误对象;三是响应被拦截成了 HTML。排查方法是在 background 里先console.log(await res.text()),看原始返回。确认端点、模型 ID、Key 三件套一致后,这个问题基本就消失了。
OAuth 相关报错:如果你在扩展里接了需要 OAuth 的第三方服务,可能会遇到 token 过期或 scope 不足。注意 MCP 工具调用本身不依赖 OAuth,它走的是 TaoToken 的 Bearer Token。如果你看到 OAuth 报错,先确认是不是把两套鉴权混在一起了。扩展侧的 OAuth 应该单独走chrome.identity,和模型通道分开管理。
另外提一个容易忽略的点:如果你同时装了 Cline、CC Switch 或 Codex 这类工具,它们的auth.json或 MCP 配置可能会和扩展的配置冲突。建议把扩展的 Base URL、Key、Model ID 三件套单独放在.env里,不要和编辑器侧的配置混用。CC Switch 的配置里如果出现 MCP server 定义,记得 Base URL 指向https://taotoken.net/api,Key 用同一套,Model ID 保持一致,这样排查时变量最少。
6. 把链路固定下来:后续接入与验证入口
跑通一次之后,建议把验证步骤固化成一个小脚本或 checklist,避免每次改配置都重新摸索。我的做法是在项目里放一个scripts/verify-mcp.mjs,用 Node 直接调一次 chat completions,确认 Key 和模型 ID 可用,再去浏览器里验证工具执行。这样能把“模型侧问题”和“扩展侧问题”分开定位。
如果你要验证模型返回结构,模型对话页是最快的入口:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果你要管理或轮换 Key,去 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入参数和端点细节以文档为准:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。长期做编码或 Agent 任务的话,Coding Plan 更适合持续调用:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后说一个实操细节:WXT 的npm run dev在改动 background 后会自动重载扩展,但 content script 的改动有时需要手动刷新页面才生效。如果你发现工具执行没反应,先刷新目标页面,再看 background 的 service worker console。这个坑我踩过,排查了半小时才发现是 content script 没重新注入。把这一步写进你的验证流程,能省不少时间。