1. WebMCP 到底是什么,为什么 Vue 项目要现在开始学
WebMCP(Web Model Context Protocol)是 W3C Web Machine Learning 社区组正在推进的一份提案规范,它做的事情可以用一句话概括:让网页把自己内部的 JavaScript 函数,以结构化「工具」的形式暴露给浏览器里的 AI 代理,让 AI 直接调用你的业务逻辑,而不是靠截图识别和模拟点击去猜你的界面。它适合谁?适合正在做 Vue、React 或者任意前端框架,并且希望自己的网站能被 AI 代理「看懂并操作」的开发者。核心检索词就是 WebMCP、W3C 标准、Vue 集成、TypeScript 类型安全、Zod 入参校验,这几个词基本覆盖了本文要讲的全部内容。
我先把传统方案的问题摆出来,你就能理解为什么需要它。现在 AI 代理要操作一个网页,常见做法有三种:截图后交给视觉模型解析、用 Playwright 之类的工具模拟点击、直接解析 DOM 抓数据。这三种方式都有一个共同的软肋——它们依赖的是「界面长什么样」,而不是「功能是什么」。你改一个按钮的 class,或者把搜索框从顶部挪到侧边栏,代理就可能失效。视觉模型还贵,复杂交互还容易点错。
WebMCP 换了个思路。它不关心你的 UI 怎么排布,而是让你主动声明:我这个页面有一个叫search_products的工具,它接收keyword和limit两个参数,返回产品列表。AI 代理拿到这份结构化描述后,直接调用这个函数,拿到 JSON 结果。整个过程在浏览器内执行,天然继承当前用户的登录态和权限,不需要后端做任何改造,也不需要把接口密钥暴露出去。
这里要区分一个容易混淆的点:WebMCP 和 MCP 不是一回事。MCP 是后端服务器向 AI 暴露能力的协议,通常涉及 OAuth 2.1 认证,跑在服务端;WebMCP 是浏览器原生 API,通过navigator.modelContext访问,跑在页面里,继承用户会话。简单说,你要暴露企业数据库或本地文件系统,用 MCP;你要让网站本身被浏览器里的 AI 代理操作,用 WebMCP。两者定位不同,不冲突。
对 Vue + TypeScript 项目来说,WebMCP 的吸引力在于类型安全。工具定义里的inputSchema如果手写 JSON Schema,很容易和实际执行函数的参数类型对不上。而 Zod 可以在 TypeScript 里定义一次 schema,既做运行时校验,又能推导出静态类型,还能转成 JSON Schema 给 AI 看。这条链路打通之后,你写工具就像写普通的组合式函数一样自然。
下面我会按「环境准备 → 可复制配置 → 验证调用 → 排错」的顺序,把整条链路走一遍。你跟着做,最后能在自己的 Vue 项目里注册一个工具,并用浏览器扩展手动调用它,看到真实返回结果。
2. 环境准备与依赖安装:Vue + TypeScript + Zod 项目初始化
这一节的目标是让你有一个能跑起来的 Vue + TypeScript 项目,并且装好 WebMCP 相关的 polyfill 和 Zod。如果你已经有项目,可以跳过创建步骤,直接看依赖安装部分。
先创建项目。用 Vite 的 Vue + TypeScript 模板,这是目前最省事的方式:
npm create vite@latest my-webmcp-app -- --template vue-ts cd my-webmcp-app npm install装完之后,项目结构里会有src/main.ts、src/App.vue这些文件。TypeScript 配置默认已经开好,tsconfig.json里strict通常是 true,这对我们后面用 Zod 推导类型很友好。
接下来装三个依赖:
npm install @mcp-b/global @mcp-b/vue-webmcp zod这三个包各自的作用要分清楚。@mcp-b/global是 polyfill,它会在浏览器里挂载navigator.modelContext对象,让还不支持原生 WebMCP 的浏览器也能用。@mcp-b/vue-webmcp是 Vue 的集成层,提供组合式函数,帮你在组件挂载和卸载时自动注册、注销工具。zod负责入参校验和类型推导。
关于浏览器环境,有两条路。第一条是用 Chrome 146 以上的 Canary 版本,打开chrome://flags,搜索并启用 "Experimental Web Platform features",重启浏览器,这样能用原生 API。第二条是装 MCP-B 扩展,在 Chrome 应用商店搜索 "MCP-B" 或 "Model Context Tool Inspector",添加到 Chrome。我建议两条都准备:polyfill 保证代码在任何环境都能跑,扩展用来手动测试工具调用。扩展这条路更稳定,因为原生 API 还在 DevTrial 阶段,行为可能变。
装完依赖后,在src/main.ts里引入 polyfill,注意要放在创建应用之前:
import { createApp } from 'vue' import App from './App.vue' import '@mcp-b/global' const app = createApp(App) app.mount('#app')这一步很关键。polyfill 必须在任何组件调用navigator.modelContext之前完成挂载,否则你会遇到Cannot read properties of undefined (reading 'registerTool')这类报错。放在main.ts顶部是最稳妥的位置。
如果你不想用 npm 包,也可以在index.html里用 script 标签引入 IIFE 版本:
<script src="https://unpkg.com/@mcp-b/global@latest/dist/index.iife.js"></script>但这种方式没有类型提示,在 TypeScript 项目里不推荐。npm 包自带类型声明,写代码时能自动补全registerTool的参数结构,体验好很多。
环境准备好之后,先跑一下npm run dev,确认页面能正常打开,控制台没有报错。打开开发者工具,在 Console 里输入navigator.modelContext,如果返回一个对象而不是 undefined,说明 polyfill 生效了。这一步是后面所有操作的前提,别跳过。
3. 可复制配置:用 Zod 定义工具并用组合式函数注册
这一节是全文的核心,我会给你一份可以直接复制到项目里的配置,包含 Zod schema 定义、TypeScript 类型推导、Vue 组合式函数封装,以及一个完整的工具注册示例。路径和文件名都写清楚,你照着建文件就行。
先建一个目录src/composables,在里面新建useWebMCP.ts。这个文件封装工具注册逻辑,处理挂载时注册、卸载时注销:
import { onMounted, onUnmounted, ref } from 'vue' import type { z } from 'zod' export interface WebMCPTool<T extends z.ZodTypeAny> { name: string description: string schema: T readOnlyHint?: boolean execute: (input: z.infer<T>) => Promise<unknown> } export function useWebMCP<T extends z.ZodTypeAny>(tool: WebMCPTool<T>) { const registered = ref(false) onMounted(() => { if (!navigator.modelContext) { console.warn('WebMCP 不可用,请检查 polyfill 或浏览器扩展') return } navigator.modelContext.registerTool({ name: tool.name, description: tool.description, inputSchema: zodToJsonSchema(tool.schema), readOnlyHint: tool.readOnlyHint ?? false, execute: tool.execute, }) registered.value = true }) onUnmounted(() => { if (registered.value && navigator.modelContext) { navigator.modelContext.unregisterTool(tool.name) } }) return { registered } }这里有个细节要说明:navigator.modelContext.registerTool接收的inputSchema是 JSON Schema 格式,而 Zod 的 schema 对象不能直接传进去。所以需要一个转换函数zodToJsonSchema。Zod 官方有zod-to-json-schema这个包,但为了减少依赖,我这里给一个简化版的手写转换,覆盖常见的 string、number、boolean、object 类型:
import type { z } from 'zod' function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> { const def = (schema as any)._def const typeName = def.typeName if (typeName === 'ZodObject') { const shape = def.shape() const properties: Record<string, unknown> = {} const required: string[] = [] for (const [key, value] of Object.entries(shape)) { properties[key] = zodToJsonSchema(value as z.ZodTypeAny) if (!(value as any).isOptional()) { required.push(key) } } return { type: 'object', properties, required } } if (typeName === 'ZodString') { return { type: 'string', description: def.description } } if (typeName === 'ZodNumber') { return { type: 'number', description: def.description } } if (typeName === 'ZodBoolean') { return { type: 'boolean', description: def.description } } if (typeName === 'ZodOptional') { return zodToJsonSchema(def.innerType) } if (typeName === 'ZodDefault') { const inner = zodToJsonSchema(def.innerType) return { ...inner, default: def.defaultValue() } } return { type: 'object', properties: {}, required: [] } }如果你不想手写这个转换,直接npm install zod-to-json-schema,然后import { zodToJsonSchema } from 'zod-to-json-schema'也行,功能更全。我上面这个版本是为了让你看清转换逻辑,实际项目建议用成熟库。
接下来在组件里用。新建src/components/ProductSearch.vue,写一个产品搜索工具:
<script setup lang="ts"> import { ref } from 'vue' import { z } from 'zod' import { useWebMCP } from '@/composables/useWebMCP' const keyword = ref('') const results = ref<Array<{ id: string; name: string; price: number }>>([]) const loading = ref(false) const searchSchema = z.object({ keyword: z.string().min(1).describe('搜索关键词'), limit: z.number().min(1).max(50).default(10).describe('返回结果数量上限'), }) useWebMCP({ name: 'search_products', description: '根据关键词搜索产品,返回产品列表', schema: searchSchema, readOnlyHint: true, async execute(input) { loading.value = true try { const res = await fetch( `/api/products?keyword=${encodeURIComponent(input.keyword)}&limit=${input.limit}` ) const data = await res.json() results.value = data.products ?? [] return { success: true, count: results.value.length, products: results.value } } catch (error) { return { success: false, error: (error as Error).message } } finally { loading.value = false } }, }) async function handleSearch() { if (!keyword.value) return const res = await fetch(`/api/products?keyword=${encodeURIComponent(keyword.value)}&limit=10`) const data = await res.json() results.value = data.products ?? [] } </script> <template> <div class="product-search"> <h2>产品搜索</h2> <input v-model="keyword" placeholder="输入产品名称..." @keyup.enter="handleSearch" /> <button @click="handleSearch">搜索</button> <div v-if="loading">加载中...</div> <ul v-else> <li v-for="item in results" :key="item.id">{{ item.name }} - ¥{{ item.price }}</li> </ul> </div> </template>这份配置里,searchSchema用 Zod 定义了keyword和limit两个字段,limit有默认值 10。useWebMCP接收这个 schema,在组件挂载时把工具注册到navigator.modelContext,卸载时自动注销。readOnlyHint: true告诉 AI 这是一个只读操作,不会修改数据,代理可以放心调用。
如果你要注册会修改数据的工具,比如加入购物车,把readOnlyHint设为 false,并在execute里做权限检查。工具定义的三件套是 Base URL、Key、Model ID 吗?不是,那是模型接入的配置。WebMCP 工具的三件套是name、description、inputSchema,这三个字段缺一不可,AI 靠它们理解工具用途和参数格式。
4. 验证请求:用 MCP-B 扩展手动调用工具看结果
配置写完了,怎么确认工具真的注册成功、调用真的生效?这一节给你一套可操作的验证流程,从浏览器控制台到扩展面板,一步步来。
第一步,启动项目:
npm run dev打开浏览器访问本地地址,通常是http://localhost:5173。按 F12 打开开发者工具,切到 Console 标签。
第二步,在 Console 里输入:
navigator.modelContext如果返回一个对象,里面有registerTool、unregisterTool、provideContext这些方法,说明 polyfill 正常。如果返回 undefined,回到main.ts检查import '@mcp-b/global'是不是在createApp之前。
第三步,确认工具已注册。在 Console 里输入:
navigator.modelContext.getTools?.()不同版本的 polyfill 方法名可能不同,有的用listTools。如果这个方法不存在,直接看 MCP-B 扩展面板更直观。
第四步,打开 MCP-B 扩展。点击浏览器工具栏的扩展图标,在弹出的面板里切到 "Tools" 标签页。你应该能看到一个工具列表,里面有一项叫search_products,描述是「根据关键词搜索产品,返回产品列表」。点开它,能看到inputSchema的结构:
{ "type": "object", "properties": { "keyword": { "type": "string", "description": "搜索关键词" }, "limit": { "type": "number", "description": "返回结果数量上限", "default": 10 } }, "required": ["keyword"] }这份 schema 就是 Zod 转换出来的结果。AI 代理看到它,就知道调用这个工具需要传keyword,可选传limit。
第五步,手动执行工具。在扩展面板里点击search_products,参数输入框里填:
{ "keyword": "笔记本", "limit": 5 }点 "Execute" 按钮。如果一切正常,你会看到返回的 JSON:
{ "success": true, "count": 5, "products": [ { "id": "p1", "name": "轻薄笔记本", "price": 4999 }, { "id": "p2", "name": "游戏笔记本", "price": 8999 } ] }这个返回结果就是你的execute函数实际跑出来的。注意看,limit传了 5,返回的count就是 5,说明 Zod 的默认值和校验都生效了。
第六步,测试校验失败的情况。把参数改成:
{ "keyword": "", "limit": 100 }keyword是空字符串,limit超过 50。如果 Zod 校验生效,你应该看到错误提示,而不是执行函数。这一步验证的是入参校验链路,确保 AI 传了非法参数时不会直接打到你的业务逻辑。
第七步,测试只读标记。在扩展面板里看search_products的详情,readOnlyHint应该显示为 true。这个标记会影响 AI 代理的调用策略,只读工具通常不需要用户二次确认。
如果你在扩展面板里看不到工具,先刷新页面,再检查 Console 有没有报错。常见的是navigator.modelContext is undefined,回到第二步排查。还有一种情况是组件没挂载,工具注册在onMounted里,如果组件被v-if条件渲染且初始为 false,工具就不会注册。
验证通过之后,你可以试着注册第二个工具,比如get_cart,然后刷新扩展面板,确认两个工具都在列表里。这一步做完,整条链路就算打通了。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
这一节把你在接入过程中可能遇到的真实报错列出来,对照着排查。这些报错不一定都来自 WebMCP 本身,有些是模型接入或代理调用环节的,但都会影响你验证工具是否生效。
报错一:navigator.modelContext is undefined
这是最常见的。原因通常是 polyfill 没加载,或者加载顺序不对。检查main.ts里import '@mcp-b/global'是否在createApp之前。如果你用的是 script 标签,确认它在所有业务脚本之前。还有一种可能是浏览器扩展和 polyfill 冲突,先禁用扩展试试。
报错二:401 Unauthorized
这个报错通常出现在你的execute函数里调用了后端 API,而后端要求认证。WebMCP 工具继承用户会话,但如果你的 fetch 请求没带 cookie 或 token,就会 401。检查 fetch 是否设置了credentials: 'include',或者手动带上 Authorization 头。如果你是通过 TaoToken 这类平台接入模型来驱动工具调用,401 也可能是 API Key 配置问题,去控制台确认 Key 是否有效、是否过期。
报错三:local proxy failed
这个报错一般出现在你本地起了代理服务,但代理没启动或端口不对。如果你在开发环境用代理转发请求,检查代理进程是否在跑,端口是否和配置一致。WebMCP 本身不需要代理,这个报错多半来自你的网络请求层。
报错四:reading 'choices'
Cannot read properties of undefined (reading 'choices')是模型返回格式不符合预期时的典型报错。通常是你调用的模型接口返回了错误信息,而不是标准的choices数组。检查请求体里的model字段是否写对,Model ID 是否和平台文档一致。如果你用 TaoToken 接入,确认 Base URL 是https://taotoken.net/api,Model ID 从文档里复制,别手写。
报错五:OAuth相关错误
WebMCP 本身继承用户会话,不涉及 OAuth。但如果你在工具里调用了需要 OAuth 的第三方 API,就会遇到 token 过期或 scope 不足的问题。检查 OAuth token 是否还在有效期,scope 是否包含你要调用的接口。如果是 MCP 后端的 OAuth 2.1 流程,那是另一套体系,别和 WebMCP 混在一起。
报错六:工具注册了但扩展面板看不到
先刷新页面,再检查组件是否真的挂载了。如果工具注册在onMounted里,而组件被v-if控制且初始为 false,工具不会注册。把v-if改成v-show,或者把注册逻辑提到父组件。还有一种可能是工具名重复,registerTool对同名工具的处理策略因版本而异,换个名字试试。
报错七:Zod 校验不生效
检查zodToJsonSchema转换后的 schema 是否正确。在 Console 里打印navigator.modelContext里注册的工具,看inputSchema是不是你期望的结构。如果required数组为空,说明 Zod 的.optional()判断有问题,检查字段是否真的必填。
排查的时候有个通用思路:先在 Console 里确认navigator.modelContext存在,再确认工具注册成功,最后确认execute函数能独立跑通。把这三层分开验证,比一次性调试整个链路快得多。
6. 从工具注册到代理调用:把链路接到真实模型上
工具能在扩展面板里手动调用,只完成了一半。真正的场景是 AI 代理自动发现并调用你的工具。这一节讲怎么把链路接到真实模型上,以及接入时需要注意的配置。
WebMCP 的设计是浏览器原生 API,AI 代理通过navigator.modelContext发现工具。目前支持这个能力的代理还在早期,Chrome 146 的 DevTrial 和 MCP-B 扩展是主要入口。如果你要在自己的应用里驱动工具调用,通常的做法是:前端注册工具,后端或代理层通过模型决定调用哪个工具,再把调用结果返回给模型继续推理。
这里就涉及模型接入的配置。以 TaoToken 为例,你需要三样东西:Base URL、API Key、Model ID。Base URL 是https://taotoken.net/api,API Key 在控制台的 API Keys 页面创建,Model ID 从文档里选一个支持工具调用的模型。配置写进你的请求体:
{ "model": "claude-sonnet-4-5", "messages": [ { "role": "user", "content": "帮我搜索一下笔记本" } ], "tools": [ { "name": "search_products", "description": "根据关键词搜索产品,返回产品列表", "input_schema": { "type": "object", "properties": { "keyword": { "type": "string" }, "limit": { "type": "number", "default": 10 } }, "required": ["keyword"] } } ] }模型返回tool_use块后,你在前端执行对应的execute函数,把结果作为tool_result回传,模型继续生成最终回答。这条链路里,WebMCP 负责工具发现和注册,模型负责决策调用哪个工具,你的execute函数负责实际执行。
如果你用 Claude Code 做开发,可以在项目里配置settings.json,把工具调用相关的环境变量写进去。Cline 的 MCP 配置也是类似思路,在配置文件里声明工具来源。Codex 的auth.json则用来存认证信息。这三件套的核心都是:Base URL 指向服务地址,Key 做认证,Model ID 指定用哪个模型。
长期做编码和 Agent 场景的话,可以考虑用 Coding Plan 这类方案,把模型调用和工具编排统一管理。验证模型是否支持工具调用,可以在模型对话页面直接测试,发一条带 tools 定义的请求,看返回里有没有tool_use。
接入文档里有完整的参数说明和示例,遇到配置问题先查文档,比在代码里猜快。API Keys 页面可以创建和管理密钥,注意别把 Key 提交到代码仓库。
整条链路跑通之后,你的 Vue 项目就不只是给人看的页面了,它同时是一个 AI 代理可以调用的工具集。从注册工具到浏览器端调用,再到模型决策,每一层都有可验证的动作。先把search_products这个只读工具跑通,再逐步加带副作用的工具,配合readOnlyHint和权限检查,这样上线风险可控。