1. 为什么要在 Vue 里折腾 MCPApp 的 iframe 通信
MCPApp 是 MCP 协议在 2025 年 7 月 28 日那版规范里正式落地的官方 UI 扩展,核心思路一句话就能说清:MCP Server 的工具不再只返回一段文本,而是可以额外附带一个交互式 HTML 界面,由 Host 客户端塞进沙盒 iframe 里渲染。对用户来说,图表、仪表盘、视频播放器、多步骤表单这些富媒体内容可以直接在对话窗口里操作,不用每换一个视图就重新发一轮对话。
但真到落地环节,坑基本都集中在两件事上:一是 iframe 里的前端页面怎么和宿主 Vue 应用通信,二是这个页面里的模型调用怎么统一走一条可控的 Key/API 通道。前者靠 PostMessage 解决,后者我用 TaoToken 的 OpenAI 兼容接口来兜底,一个 Key 就能覆盖对话、富媒体生成这类请求,省得在 iframe 里再散落一堆密钥。
这篇适合已经在写 Vue、想接 MCPApp 富媒体交互的前端同学,也适合做 MCP Server 想验证 UI 资源渲染的后端同学。下面给的是能直接复制跑的骨架:Vue 侧 iframe 容器 + PostMessage 双向通信 + 鉴权配置 + 本地验证步骤,最后附上我踩过的几个典型报错。
2. TaoToken 前置:把 Key 和 API 通道准备好
MCPApp 的 iframe 页面里如果要调模型,最忌讳把 Key 硬编码进前端。我的做法是让 iframe 只负责发请求意图,真正的模型调用走宿主 Vue 应用转发,或者 iframe 内用短期票据。不管哪种,底层都统一指向 TaoToken 的 API 通道。
先拿到 Key:打开 https://taotoken.net/api-keys ,登录后创建一个 API Key,复制出来形如sk-xxxx。这个 Key 只在服务端或宿主侧使用,不要写进 iframe 的 HTML 里。
TaoToken 的接口是 OpenAI 兼容格式,Base URL 用https://taotoken.net/api,对话补全的路径就是/v1/chat/completions。你可以先用 curl 验证通道是否通:
curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "只回复两个字:通了"}] }'返回里能看到choices[0].message.content就说明通道没问题。这一步很关键,因为后面 iframe 里的富媒体请求如果失败,你要能快速区分是通信问题还是 Key/通道问题。
如果你后面要做长期编码或 Agent 类场景,可以顺带看下 Coding Plan:https://taotoken.net/coding-plan ,它更适合高频调用;只是验证模型能力的话,直接用模型对话页 https://taotoken.net/models 更快。
3. 可复制配置:Vue 侧 iframe 容器与 PostMessage 骨架
MCPApp 的通信是双通道:MCP 协议通道负责工具调用,PostMessage 通道负责 UI 交互。我们这里聚焦后者,因为 iframe 渲染和父子通信是前端最容易卡住的地方。
3.1 父窗口:Vue 组件里创建 iframe 并监听消息
先写一个 Vue 3 的组合式组件,负责挂载 iframe、发送初始化消息、接收子窗口回传。
<template> <div class="mcp-app-host"> <iframe ref="appFrame" :src="appUrl" sandbox="allow-scripts allow-same-origin" style="width: 100%; height: 480px; border: 1px solid #e5e7eb; border-radius: 8px" @load="onFrameLoad" /> <p v-if="lastMessage">子窗口最新消息:{{ lastMessage }}</p> </div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from 'vue' const appFrame = ref(null) const appUrl = ref('/mcp-app/index.html') // 你的 MCPApp 页面地址 const lastMessage = ref('') // 只接受来自我们 iframe 的消息,避免其他窗口伪造 const ALLOWED_ORIGIN = window.location.origin function onFrameLoad() { // iframe 加载完成后,父窗口主动发一次握手 appFrame.value?.contentWindow?.postMessage( { type: 'host:init', payload: { theme: 'light', locale: 'zh-CN' } }, ALLOWED_ORIGIN ) } function handleMessage(event) { if (event.origin !== ALLOWED_ORIGIN) return const { type, payload } = event.data || {} if (type === 'app:ready') { lastMessage.value = '子窗口已就绪' } if (type === 'app:request-model') { // 子窗口请求模型能力,父窗口转发到 TaoToken callModel(payload).then((result) => { appFrame.value?.contentWindow?.postMessage( { type: 'host:model-result', payload: result }, ALLOWED_ORIGIN ) }) } } async function callModel(payload) { const res = await fetch('/api/taotoken/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) return res.json() } onMounted(() => window.addEventListener('message', handleMessage)) onBeforeUnmount(() => window.removeEventListener('message', handleMessage)) </script>这里有两个细节值得强调。第一,sandbox属性我保留了allow-scripts allow-same-origin,因为 MCPApp 的 View 需要跑脚本,同时要能访问自身资源;但不要加allow-top-navigation,否则子窗口能劫持父页面。第二,handleMessage里第一行就校验event.origin,这是防伪造消息的基本功,别省。
3.2 子窗口:MCPApp 页面里的握手与请求
iframe 里的页面(也就是 MCP Server 通过ui://资源下发的 HTML)需要主动告诉父窗口自己准备好了,并在需要模型能力时发请求。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>MCPApp View</title> </head> <body> <div id="chart"></div> <script> const HOST_ORIGIN = window.location.origin // 通知父窗口:我准备好了 window.parent.postMessage({ type: 'app:ready' }, HOST_ORIGIN) // 监听父窗口的初始化与结果消息 window.addEventListener('message', (event) => { if (event.origin !== HOST_ORIGIN) return const { type, payload } = event.data || {} if (type === 'host:init') { console.log('收到宿主初始化参数', payload) // 这里可以按主题、语言渲染 UI } if (type === 'host:model-result') { renderChart(payload) } }) // 需要模型能力时,向父窗口发请求,而不是自己拿 Key 调 function requestModel(prompt) { window.parent.postMessage( { type: 'app:request-model', payload: { prompt } }, HOST_ORIGIN ) } function renderChart(data) { document.getElementById('chart').textContent = '图表数据:' + JSON.stringify(data) } // 模拟一次请求 requestModel('生成一组销售趋势数据') </script> </body> </html>这套骨架的关键在于职责分离:iframe 不碰 Key,只发意图;父窗口持有鉴权逻辑,统一走 TaoToken。这样即使 iframe 内容来自 MCP Server,也不会泄露凭证。
3.3 服务端转发:把请求打到 TaoToken
父窗口里那个/api/taotoken/chat需要你后端实现,Node 示例:
// server.js (Express) import express from 'express' const app = express() app.use(express.json()) app.post('/api/taotoken/chat', async (req, res) => { const r = await fetch('https://taotoken.net/api/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.TAOTOKEN_API_KEY}` }, body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: req.body.prompt }] }) }) const data = await r.json() res.json(data.choices?.[0]?.message?.content ?? '') }) app.listen(3000)Key 放在环境变量TAOTOKEN_API_KEY里,前端永远看不到。
4. 验证请求:本地跑通 PostMessage 往返与富媒体渲染
配置写完,别急着接真实 MCP Server,先在本地把通信链路验证通。我一般分三步。
第一步,起一个静态服务托管 iframe 页面,比如npx serve public,确认/mcp-app/index.html能单独打开。
第二步,在 Vue 页面里打开控制台,观察消息流。正常顺序是:iframeload触发父窗口发host:init,子窗口收到后打印初始化参数,同时子窗口发app:ready,父窗口把lastMessage更新为「子窗口已就绪」。如果lastMessage一直是空,说明消息没到,先查 origin 是否一致。
第三步,验证模型往返。子窗口调用requestModel后,父窗口应该发出/api/taotoken/chat请求,返回结果再通过host:model-result回传,子窗口的renderChart被触发。你可以在 Network 面板看到对taotoken.net/api/v1/chat/completions的请求,状态 200 且响应里有内容,就说明整条链路通了。
富媒体渲染的验证更直观:把renderChart换成真实的图表库(比如 ECharts),数据灌进去后 iframe 里应该出现图表。如果图表不显示但数据到了,问题在渲染层;如果数据没到,回到第二步查消息。
5. 本篇常见错排查
报错一:Blocked a frame with origin ... from accessing a cross-origin frame
这是同源策略,不是 PostMessage 的问题。检查 iframe 的src和父页面是否同源。如果 MCPApp 页面部署在不同域名,postMessage的第二个参数要写子窗口的真实 origin,不能再用window.location.origin。同时子窗口发消息时targetOrigin也要写父窗口的真实 origin。
报错二:消息发出去了但收不到
九成是event.origin校验写错。父窗口收到的是子窗口的 origin,子窗口收到的是父窗口的 origin,两边别写反。调试时可以先临时打印event.origin确认。
报错三:iframe 里请求 TaoToken 返回 401
说明 Key 没带上或带错了。检查后端转发时Authorization头是不是Bearer sk-xxx,以及环境变量有没有加载。注意不要在 iframe 前端直接调https://taotoken.net/api,那样 Key 会暴露,而且容易触发跨域。
报错四:sandbox太严导致脚本不执行
如果 iframe 白屏且控制台报脚本被阻止,检查sandbox是否漏了allow-scripts。但别为了省事直接去掉sandbox,那等于放弃隔离,MCPApp 的安全模型就废了。
报错五:MCP Server 下发的 HTML 里资源 404
MCPApp 的 UI 资源通过ui://协议注册,Host 读取后渲染。如果你本地直接拿文件路径测,相对路径的资源会找不到。确认资源路径是相对于 iframe 页面本身的,或者用绝对路径。
6. 继续往下走:把通道和文档用起来
通信骨架跑通后,下一步就是接真实的 MCP Server 工具调用。这时候建议先把 API Key 管理好,不同环境用不同 Key,方便排查和限额:https://taotoken.net/api-keys 。接入细节和参数说明看文档:https://taotoken.net/doc ,里面有 OpenAI 兼容接口的完整字段。
如果你只是想先验证某个模型在富媒体场景下的输出质量,直接去模型对话页试:https://taotoken.net/models 。而如果你要做的是长期编码、Agent 编排这类高频场景,Coding Plan 会更划算:https://taotoken.net/coding-plan 。
最后提醒一句,MCPApp 的 iframe 通信看着简单,真正难的是边界处理:origin 校验、消息类型收敛、错误回传。我建议你在handleMessage里加一个switch白名单,只处理已知的type,未知消息直接丢弃,这样后期加功能时不会因为一条脏消息把整个宿主搞崩。