编写与 defineMcpApp
快速开始
一个完整的应用——schema、服务器处理器、UI——全部写在一个文件里:
<script setup lang="ts">
import { z } from 'zod'
interface PalettePayload {
base: string
swatches: { name: string, hex: string }[]
}
defineMcpApp({
description: '选择一种颜色并预览一个 5 色调色板。',
inputSchema: {
base: z.string().describe('用于锚定调色板的十六进制颜色,例如 #2563eb'),
},
handler: async ({ base }): Promise<{ structuredContent: PalettePayload }> => {
const swatches = await $fetch<{ name: string, hex: string }[]>('/api/palette', {
query: { base },
})
return { structuredContent: { base, swatches } }
},
})
const { data, loading, sendPrompt } = useMcpApp<PalettePayload>()
</script>
<template>
<main class="picker">
<p v-if="loading">
正在混合颜色…
</p>
<ul v-else-if="data" class="swatches">
<li v-for="s in data.swatches" :key="s.hex">
<button
type="button"
:style="{ background: s.hex }"
@click="sendPrompt(`将 ${s.name}(${s.hex})用作主颜色。`)"
>
{{ s.name }}
</button>
</li>
</ul>
</main>
</template>
<style scoped>
.picker { padding: 16px; font-family: system-ui, sans-serif; }
.swatches { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; padding: 0; list-style: none; }
.swatches button { width: 100%; aspect-ratio: 1; border-radius: 8px; border: 0; cursor: pointer; }
</style>
就是这样。该工具包会:
- 检测
defineMcpApp并根据文件名注册一个 MCP 工具,命名为color-picker。 - 生成一个 UI 资源
ui://mcp-app/color-picker,暴露text/html;profile=mcp-app。 - 使用
vite-plugin-singlefile将 SFC + 资源打包为一个 HTML 文件。 - 将处理器返回的
structuredContent注入 iframe,从而使 UI 无需第二次往返请求即可完成水合。
文件约定
MCP Apps 默认位于 app/mcp/(而不是 server/mcp/)。你可以在 nuxt.config.ts 中通过 mcp.appsDir 更改应用侧目录。它们位于 Nuxt 的客户端侧,因为它们要编写 Vue 组件——但你声明的 handler 会像工具一样在服务端运行。
app/
└── mcp/
├── color-picker.vue # → 工具:color-picker,资源:ui://mcp-app/color-picker
└── admin/
└── audit-log.vue # → 工具:audit-log
format.ts)——打包器会将它们内联。把数据生成逻辑保留在 server/api/ 中,并在处理器里通过 $fetch 调用它。自动生成的名称与标题
和工具与资源一样,name 和 title 会从文件名推断出来:
| 文件 | 名称 | 标题 |
|---|---|---|
color-picker.vue | color-picker | Color Picker |
weather-card.vue | weather-card | Weather Card |
admin/audit-log.vue | audit-log | Audit Log |
你也可以通过向 defineMcpApp 传入 name / title 来覆盖它们。
将应用路由到特定处理器
默认情况下,每个应用都会附加到隐式的 apps 处理器,并且只会在 /mcp/apps 上展示。将应用路由到其他命名处理器有两种方式:
1. 子文件夹约定 —— app/mcp/ 下的第一级子目录会成为处理器归属:
app/
└── mcp/
├── color-picker.vue # → /mcp/apps(默认)
├── finder/
│ └── stay-finder.vue # → /mcp/finder
└── checkout/
└── stay-checkout.vue # → /mcp/checkout
将每个处理器文件夹与 server/mcp/handlers/<name>/index.ts 配对:
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'
export default defineMcpHandler({})
2. 显式 attachTo 覆盖 —— 如果两者都存在,则会覆盖子文件夹默认值:
<script setup lang="ts">
defineMcpApp({
attachTo: 'finder',
group: 'stays',
tags: ['searchable'],
// ...
})
</script>
生成的工具和资源会携带 _meta.handler = 'finder'、顶层 group = 'stays',以及 tags = ['searchable']。你可以使用 getMcpTools({ handler: 'finder' })、getMcpTools({ tags: ['searchable'] }) 等对它们进行过滤。
defineMcpApp
一个宏——类似 definePageMeta——会在构建时提取,并且从浏览器打包产物中移除。它接受的字段如下:
defineMcpApp({
name?: string // 覆盖自动推断的名称
title?: string // 覆盖自动推断的标题
description?: string // 显示给 LLM,以帮助它选择这个应用
inputSchema?: ZodRawShape // 在服务器上验证工具输入
handler?: (args, extra) => Result // 在服务端运行;默认是 (args) => ({ structuredContent: args })
csp?: McpAppCsp | false // 收紧或禁用 iframe CSP
attachTo?: string // 该应用路由到的命名 MCP 处理器(默认:'apps' 或子文件夹)
group?: string // 顶层分组标签(默认:与 attachTo 相同)
tags?: string[] // 转发到生成工具的顶层标签
_meta?: Record<string, unknown> // 额外的 _meta 字段,向宿主暴露
})
attachTo、group 和 tags 必须是字面量('finder'、['a', 'b'])——工具包会在构建时静态读取它们,以便路由生成的工具和资源。动态表达式(attachTo: someVar)会以清晰的错误使构建失败。服务器处理器
handler 运行在你的 Nitro 服务器中,而不是在 iframe 里。它接收已验证的输入,并返回 UI 用于水合的 structuredContent。把它当作一个工具处理器来使用——调用 API、查询数据库、使用 $fetch:
defineMcpApp({
description: '选择一种颜色并预览一个 5 色调色板。',
inputSchema: {
base: z.string().describe('用于锚定调色板的十六进制颜色,例如 #2563eb'),
},
handler: async ({ base }) => {
const swatches = await $fetch('/api/palette', { query: { base } })
return { structuredContent: { base, swatches } }
},
})
structuredContent 会将数据以内联方式写入 HTML,形式为 <script type="application/json">。iframe 启动时已经拥有完整数据——无需额外 fetch,也不会闪烁。如果你省略 handler,工具包会默认使用 (args) => ({ structuredContent: args })。这适用于只需要回显输入的无状态应用。
在服务器与 UI 之间共享类型
将共享类型放在 Nuxt 的 shared/types/ 目录中——它们会在 SFC 和 API 端点中自动全局导入,无需 import 语句:
export interface Swatch { name: string, hex: string }
export interface PalettePayload { base: string, swatches: Swatch[] }
export default defineEventHandler(async (event): Promise<PalettePayload> => {
const { base } = getQuery(event)
return { base: String(base), swatches: buildPalette(String(base)) }
})
<script setup lang="ts">
defineMcpApp({
inputSchema: { base: z.string() },
handler: async ({ base }): Promise<{ structuredContent: PalettePayload }> => ({
structuredContent: await $fetch('/api/palette', { query: { base } }),
}),
})
const { data } = useMcpApp<PalettePayload>()
</script>
仅类型引用会被 esbuild 从浏览器打包产物中移除——运行时无需在 iframe 内解析任何内容。