本文根据最新的 WebMCP 标准与 doc-ai 示例项目,带你一步步把普通 Vue 工程升级为 AI 驱动的智能应用。
核心变化:我们现在统一使用浏览器原生的
navigator.modelContext接口。通过调用 SDK 提供的初始化函数,低版本浏览器也能获得完全一致的 Polyfill 支持,实现 AI 工具的自动注册与路由同步。 示例工程仓库:packages/doc-ai
在 Web 端集成 MCP 时,最重要的资产是 “模型上下文 (Model Context)”。
- 标准 API:使用浏览器标准的
navigator.modelContext进行工具管理。 - 全平台 Polyfill:调用
initializeBuiltinWebMCP后,SDK 会确保navigator.modelContext在所有浏览器中均可用。 - 自动路由感知:当 AI 在对话中判定需要调用某个页面的工具时,SDK 会自动驱动路由跳转,确保工具在调用前已就绪。
| 模块 | 职责 |
|---|---|
| Model Context | 浏览器原生接口,用于注册工具。对话组件(如 TinyRemoter)会自动从中读取。 |
| Page Tool Bridge | 监听 AI 指令,负责路由跳转 (Navigator) 与工具调用之间的时序同步。 |
| WebSkills | 让 AI 获得业务知识(如产品手册、SOP)的 Markdown 文档包。 |
| WebAgent | 远程代理模块,支持手机或异地 AI 通过识别码控制当前页面工具。 |
为了保持项目的可维护性,建议采用下方的模块化结构(参考 doc-ai 项目):
src/
├── main.ts # 激活 Builtin WebMCP + 设置 Navigator
├── App.vue # 放置 TinyRemoter + 批量加载 Skills + 初始化 WebAgent
├── mcp-servers/ # 【方案B】分离式配置目录 (简单应用)
│ ├── finance/
│ │ └── tools.ts # 定义工具 Schema 与 routeConfig
│ └── useWebAgentServer.ts # 远程遥控初始化逻辑
├── skills/ # 【方案A】WebSkills 知识库 (大型应用)
│ ├── inventory/
│ │ └── SKILL.md # 库存业务引导词
│ └── sales/
│ └── SKILL.md
└── views/ # 业务页面
├── inventory/
│ └── index.vue # 【方案A】页面内按需注册 (onMounted)
└── finance/
└── index.vue # 【方案B】逻辑绑定 (registerPageTool)
在应用入口处,你需要激活内置服务器并配置导航器。
// src/main.ts
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
import { setNavigator, initializeBuiltinWebMCP } from '@opentiny/next-sdk'
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
// 1. 注册核心导航器:告诉 SDK 如何跳转页面
setNavigator(async (route) => {
const failure = await router.push(route)
if (failure) {
// 处理重复跳转:如果已经在目标页面,直接返回 true 告知 SDK
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
return true
}
throw new Error(`页面跳转失败: ${(failure as any).message}`)
}
})
// 2. 激活浏览器内置 WebMCP 服务 (含低版本浏览器 Polyfill)
initializeBuiltinWebMCP()
const app = createApp(App)
app.use(router)
app.mount('#app')对于复杂的业务系统,我们强烈建议在页面组件内部按需注册工具。
- 减少幻觉:工具只在对应的业务页面挂载时存在,大模型不会在无关页面看到干扰工具。
- 降低负载:工具列表随路由变化自动增减,保证上下文(Context)的高效。
- 配合 Skills:通过 WebSkills 引导 AI 意图。当 AI 判定用户需要执行库存操作时,它会由 Skills 指导先跳转到
/inventory,随后在该页面内自动激活对应的工具。
<!-- src/views/product-list/index.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
const modelContext = navigator.modelContext
onMounted(() => {
if (!modelContext) return
modelContext.registerTool({
name: 'get_product_detail',
description: '查询商品详情。',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: '商品 ID' }
},
required: ['id']
},
execute: async ({ id }: { id: string }) => {
return { content: [{ type: 'text', text: `商品 ${id} 的状态:销售中` }] }
}
})
})
onUnmounted(() => {
modelContext?.unregisterTool('get_product_detail')
})
</script>如果你的应用功能较少,或者不想编写繁琐的 WebSkills,可以使用一次性全量注册方案。
在独立文件中定义工具及其所属路由。这种方式下,AI 随时可见该工具,并能自动触发跳转。
// src/mcp-servers/finance/tools.ts
export default function registerFinanceTools() {
navigator.modelContext.registerTool({
name: 'finance_summary_query',
title: '查询财务数据',
description: '查询关键财务指标。',
inputSchema: {
/* ... */
},
// 💡 关键:无需 Skills,显式声明跳转目标
routeConfig: {
route: '/finance'
}
})
}在业务组件内,你只需要关注如何处理该工具的逻辑,无需再次声明或配置。
<!-- src/views/finance/index.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { registerPageTool } from '@opentiny/next-sdk'
let cleanup: (() => void) | undefined
onMounted(() => {
// 绑定具体执行逻辑,需与声明时的 route 路径对应
cleanup = registerPageTool({
route: '/finance',
handlers: {
'finance_summary_query': async ({ month }) => {
// 执行具体的财务查询逻辑...
return { content: [{ type: 'text', text: `11月净收入:¥10,000` }] }
}
}
})
})
onUnmounted(() => cleanup?.())
</script>Note
适用场景:这是增强功能。只有在你需要通过手机远程操控、或将本地工具能力暴露给远端 AI Agent 平台时才需要配置。如果仅需在当前网页中使用 AI 对话,可跳过此步。
WebAgent 可以将当前页面的 WebMCP 能力桥接到远端平台,通过一个会话 ID 即可实现跨设备(如手机控制电脑)遥控。
该文件负责建立与远程代理服务器的 WebSocket 连接。
// src/mcp-servers/useWebAgentServer.ts
import { WebMcpClient } from '@opentiny/next-sdk'
const client = new WebMcpClient()
const SESSION_ID_KEY = 'web-agent-session-id'
export const useWebAgentServer = async () => {
// 从本地存储读取,确保刷新页面后识别码保持不变
const cachedSessionId = localStorage.getItem(SESSION_ID_KEY) ?? undefined
const { sessionId } = await client.connect({
sessionId: cachedSessionId,
agent: true, // 开启代理模式
builtin: true, // 代理内置的 WebMCP 工具
url: 'https://agent.opentiny.design/api/v1/webmcp-trial/mcp'
})
if (sessionId) {
localStorage.setItem(SESSION_ID_KEY, sessionId)
}
return { sessionId }
}在应用根组件中初始化远程服务,并将获取到的 sessionId 填充到 TinyRemoter 的菜单中。
<!-- App.vue (部分逻辑) -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useWebAgentServer } from './mcp-servers/useWebAgentServer'
const menuItems = ref([])
const AGENT_ROOT = 'https://agent.opentiny.design'
onMounted(async () => {
try {
const { sessionId } = await useWebAgentServer()
if (sessionId) {
const remoteUrl = `${AGENT_ROOT}/mcp?sessionId=${sessionId}`
// 在 AI 助手的菜单中添加遥控信息
menuItems.value = [
{
action: 'remote-url',
text: '遥控器链接',
desc: remoteUrl, // 完整链接,点击可复制
active: true,
showCopyIcon: true
},
{
action: 'remote-control',
text: '识别码',
desc: sessionId.slice(-6), // 展示后 6 位作为识别码
know: true,
showCopyIcon: true
}
]
}
} catch (err) {
console.warn('[WebAgent] 远程连接初始化失败,本地对话仍可正常运行:', err)
}
})
</script>在根组件中配置面板,并加载相关的技能知识库。
<!-- src/App.vue -->
<template>
<div class="main-layout">
<router-view />
<TinyRemoter
:show="show"
:skills="skillMdModules"
:mcpServers="mcpServers"
:menuItems="menuItems"
title="智能助手"
:llmConfig="llmConfig"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { TinyRemoter } from '@opentiny/next-remoter'
import '@opentiny/next-remoter/dist/style.css'
const show = ref(true)
const mcpServers = {
'builtin-webmcp': {
type: 'builtin' as const,
// 【重要】Client 端仅连接 modelContextTesting 接口
client: navigator.modelContextTesting
}
}
const llmConfig = {
/* 模型配置... */
}
// 加载 skills 目录下所有 Markdown 文件(含子目录)
const skillMdModules = import.meta.glob('./skills/**/*', {
query: '?raw',
import: 'default',
eager: true
}) as Record<string, string>
</script>| 特性 | 原生内置 WebMCP (按需) | 分离式全量注册 (全局) |
|---|---|---|
| 推荐段位 | 中大型、复杂业务系统 | 小型、功能单一应用 |
| AI 幻觉风险 | 极低(工具随页面动态上线) | 中(工具全局常驻,上下文负载随规模增加) |
| 路由跳转依赖 | 依赖 WebSkills 指引或大模型主动跳转 | 依赖工具自身的 routeConfig 声明 |
| 实现复杂度 | 稍高(需配置 Skills 引导词) | 极低(一站式注册即可用) |
通过 initializeBuiltinWebMCP() 已经为全局环境注入了 navigator.modelContext。直接使用原生 API 可以保持代码的简洁性。
请确保在 main.ts 的 setNavigator 中捕获并处理了 NavigationFailure。