应用

useMcpApp() 桥接

在 iframe 内使用 data、hostContext、sendPrompt、callTool、openLink、显示模式、模型上下文和文件下载。

useMcpApp()

唯一的客户端组合式函数,会自动导入到每个 MCP App SFC 中。它返回 iframe 与主机通信所需的一切:

const {
  initialData,        // Ref<T | null>          — 视图收到的首个有效载荷快照,此后不会更新
  data,               // Ref<T | null>          — 主机推送的 structuredContent,可由 callTool 刷新
  loading,            // Ref<boolean>           — 收到首个有效载荷或调用失败前为 true
  error,              // Ref<Error | null>      — 桥接、传输或有效载荷错误
  pending,            // Ref<boolean>           — callTool() 执行期间为 true
  hostContext,        // Ref<HostContext | null> — 主题、显示模式、区域设置等
  hostCapabilities,   // Ref<HostCapabilities | null> — 主机支持的功能
  callTool,           // (name, params?) => Promise<T | null> — 重新调用任意 MCP 工具
  sendPrompt,         // (prompt: string) => void              — 将消息推送到聊天中
  openLink,           // (url: string) => void                 — 请求主机打开 URL
  requestDisplayMode, // (mode) => Promise<DisplayMode> — 切换内联、全屏或画中画模式
  updateModelContext, // ({ content?, structuredContent? }) => Promise<void> — 为下一轮对话更新上下文
  downloadFile,       // (contents) => Promise<void> — 通过主机保存文件
} = useMcpApp<MyPayload>()

将你的有效载荷类型作为泛型传入,以便下游获得完整推断。

initialData & data

initialData 是视图收到的首个 structuredContent 的冻结快照(在 ChatGPT 中则是 window.openai.toolOutput)。此后它不会改变;可以用它保存 ID 或查询参数等初始值,因为后续 callTool 结果可能会覆盖 data 中的值。

主机推送工具结果后,data 会被填充;之后可由 callTool 或主机推送的 tool-result 更新。loading 初始为 true,收到首个有效载荷后变为 false。如果主机返回错误结果或取消调用,loading 也会变为 false,并由 error 保存原因。callTool() 正在刷新时请使用 pending:

<script setup lang="ts">
const { initialData, data, callTool } = useMcpApp<{ listId: string }>()

async function refresh() {
  const result = await callTool('list_todos', { listId: initialData.value!.listId })
  // `data` 现在保存最新的工具结果;`initialData` 仍然包含 `{ listId }`
}
</script>
<template>
  <section v-if="loading" class="skeleton" />
  <section v-else-if="data" class="content">
    {{ data.swatches.length }} 个色板,来自 {{ data.base }}
  </section>
</template>

hostContext

主机会在 ui/initialize 握手期间向 iframe 传递上下文对象,并在其内容变化时发送 ui/notifications/host-context-changed,例如用户切换主题或应用进入全屏时。hostContext 会合并每次更新,因此始终保持最新。你可以用它来适配深色模式、全屏或固定的 iframe 尺寸:

type DisplayMode = 'inline' | 'fullscreen' | 'pip'

interface HostContext {
  theme?: 'light' | 'dark'
  displayMode?: DisplayMode
  availableDisplayModes?: DisplayMode[]
  containerDimensions?: { width?: number, height?: number, maxWidth?: number, maxHeight?: number }
  locale?: string
  timeZone?: string
  platform?: 'web' | 'desktop' | 'mobile'
}
<script setup lang="ts">
const { hostContext } = useMcpApp()
const isDark = computed(() => hostContext.value?.theme === 'dark')
const isFullscreen = computed(() => hostContext.value?.displayMode === 'fullscreen')
</script>

<template>
  <main :data-theme="isDark ? 'dark' : 'light'" :data-mode="isFullscreen ? 'fullscreen' : 'inline'">
    …
  </main>
</template>
hostContext 在第一次绘制时为 null,会在握手后填充(通常 <50 ms)。请始终在模板中使用回退值。

hostCapabilities

这是主机在握手期间声明的能力;握手完成前为 null。主机支持某项功能时,对应键就会存在(例如 openLinks、downloadFile、updateModelContext、message、serverTools 等)。在提供主机可能不支持的操作前,请先检查此对象。例如,Claude on iOS 不支持 downloadFile:

<script setup lang="ts">
const { hostCapabilities } = useMcpApp()
const canDownload = computed(() => !!hostCapabilities.value?.downloadFile)
</script>

sendPrompt(prompt) — 后续跟进

将消息发送到聊天中,就像用户亲自输入了一样。然后 LLM 会像处理任何其他请求一样路由它——包括调用另一个 MCP App:

<button @click="sendPrompt(`Use ${swatch.name} (${swatch.hex}) as the brand colour.`)">
  使用此颜色
</button>

主机会像用户输入了一样接收该提示。LLM 可能会回复、调用另一个工具,或者响应时打开另一个 MCP App——应用到应用的工作流 就由这个原语自然实现。

后续跟进是尽力而为。实现了 ui/message 的主机可以顺畅转发提示。ChatGPT 会确认请求,但并不总是将下一个工具以内联方式重新渲染(这是上游限制)。

callTool(name, params) — 原地刷新

从 iframe 中重新调用任意 MCP 工具。结果会自动替换 data:

<script setup lang="ts">
const { data, pending, callTool } = useMcpApp<PalettePayload>()

async function refresh(base: string) {
  await callTool('color-picker', { base })
}
</script>

可将其用于筛选、分页、刷新按钮——任何无需完整聊天往返即可更改查询的操作。

openLink(url)

沙盒 iframe 不能打开窗口。openLink 会请求主机替你完成此操作(例如在新的浏览器标签页中打开预订确认页面):

<button @click="openLink(`https://example.com/colors/${swatch.hex.slice(1)}`)">
  在网页中查看
</button>
如果你还需要从 iframe 中 fetch() 该目标,请将目标主机添加到 csp.connectDomains。

requestDisplayMode(mode)

请求主机以 inline、fullscreen 或画中画(pip)模式显示应用。该方法会以主机实际设置的模式完成解析;结果可能与请求的模式不同(claude.ai 会将 pip 请求按 inline 处理),并会更新 hostContext.displayMode。如果 hostContext.availableDisplayModes 不包含所请求的模式,就不会发送请求,而是以当前模式解析。在 ChatGPT 中,请求会通过 window.openai.requestDisplayMode 发出。

<script setup lang="ts">
const { hostContext, requestDisplayMode } = useMcpApp()
const isFullscreen = computed(() => hostContext.value?.displayMode === 'fullscreen')
</script>

<template>
  <button @click="requestDisplayMode(isFullscreen ? 'inline' : 'fullscreen')">
    {{ isFullscreen ? '收起' : '展开' }}
  </button>
</template>

updateModelContext({ content, structuredContent })

无需发送消息,即可告诉模型用户正在查看的内容。每次调用都会替换此应用之前发送的上下文;主机会在模型的下一轮对话中加入该上下文。模型会将这段文本作为上下文读取,因此不要传入网页抓取内容或其他用户输入等不可信内容。如果主机没有通过 hostCapabilities.updateModelContext 声明此能力,调用会被拒绝。

const { updateModelContext } = useMcpApp()

await updateModelContext({
  content: [{ type: 'text', text: `The user selected ${swatch.name} (${swatch.hex}).` }],
  structuredContent: { selected: swatch.hex },
})
Claude 会确认这次更新,但不会自动将它加入提示词;模型会在需要时通过 read_widget_context 工具读取。

downloadFile(contents)

沙盒 iframe 无法直接触发下载。downloadFile 会将要保存的 MCP 资源交给主机处理,资源可以是内嵌内容(type: 'resource',包含 text 或 base64 blob),也可以是链接(type: 'resource_link',由主机获取)。主机会根据 URI 的最后一段确定文件名,通常还会要求用户确认。

const { downloadFile } = useMcpApp()

await downloadFile([{
  type: 'resource',
  resource: { uri: 'file:///palette.json', mimeType: 'application/json', text: JSON.stringify(palette) },
}])

如果主机不支持 hostCapabilities.downloadFile,调用会被拒绝;如果用户取消或主机拒绝下载,错误中会包含 cancelled: true。resource_link 必须使用 http 或 https;其他协议会在请求主机前以 code: -32602 拒绝。主机不支持此能力时,可改用 openLink。

错误

requestDisplayMode、updateModelContext 和 downloadFile 被拒绝时会返回 McpAppRequestError:code 和 data 包含主机返回的 JSON-RPC 错误,cancelled 表示下载被拒绝。主机缺少相应能力时,updateModelContext 和 downloadFile 会以 code: -32601 拒绝;如果主机不支持请求的显示模式,requestDisplayMode 则会以当前模式解析。当应用在主机环境之外运行,或 ui/initialize 握手未完成时,这三个方法都会直接拒绝,不会发送请求。

import type { McpAppRequestError } from '@nuxtjs/mcp-toolkit/app'

try {
  await downloadFile([file])
}
catch (error) {
  if (!(error as McpAppRequestError).cancelled) showError()
}

useToolCall()

callTool 会将结果写入 data。如果要调用工具但不修改 data,例如调用仅供应用使用、用于获取下载文件内容的工具,请导入 useToolCall:

import { useToolCall } from '@nuxtjs/mcp-toolkit/app'

const png = useToolCall<{ filename: string, png: string }>('get_icon_png')

const result = await png.call({ iconId }) // `png.pending`, `png.error`, `png.result` track the call

callTool 和 useToolCall 都会等待 ui/initialize 握手完成后再发送 tools/call,因此可以在组件挂载时安全地调用工具。