一次构建,多处运行。 ChatGPT 实现了用于 UI 集成的 MCP Apps 标准,该标准借鉴了我们在构建 ChatGPT Apps 时的经验。Apps SDK 的支持将持续存在——我们没有计划弃用它。请默认使用 MCP Apps 标准字段和 ui/* 网桥。OpenAI 扩展是可选的,当您需要 ChatGPT 特定功能时,它们存在于 window.openai 中。
MCP Apps UI 网桥
UI 集成使用基于 postMessage 的 JSON-RPC 2.0,并通过 ui/* 方法和通知进行通信。
常用消息
| 类别 | MCP Apps 方法/通知 | 目的 |
|---|---|---|
| 工具输入 | ui/notifications/tool-input | 调用 UI 的最新工具输入。 |
| 工具结果 | ui/notifications/tool-result | 最新的工具结果(包括 structuredContent、content、_meta)。 |
| 工具调用 | tools/call | 直接从 UI 调用 MCP 工具。 |
| 后续消息 | ui/message | 请求宿主发布消息。 |
| 模型上下文 | ui/update-model-context | 从 UI 状态更新模型可见的上下文。 |
有关概览及 Apps SDK API 的映射指南,请参阅 ChatGPT 中的 MCP Apps 兼容性。
window.openai 组件网桥
ChatGPT 提供 window.openai 作为 Apps SDK 兼容层和一组可选的 ChatGPT 扩展。
请参阅 构建 ChatGPT UI 获取实现指南。
能力
| 能力 | 它做什么 | 典型用法 |
|---|---|---|
| 状态与数据 | window.openai.toolInput | 调用工具时提供的参数。 |
| 状态与数据 | window.openai.toolOutput | 您的 structuredContent。请保持字段简洁;模型会逐字阅读它们。 |
| 状态与数据 | window.openai.toolResponseMetadata | _meta 负载;只有小部件能看到它,模型永远无法看到。 |
| 状态与数据 | window.openai.widgetState | 在渲染之间持久化的 UI 状态快照。 |
| 状态与数据 | window.openai.setWidgetState(state) | 同步存储新的快照;在每次重要的 UI 交互后调用它。 |
| 小部件运行时 API | window.openai.callTool(name, args) | 从组件调用另一个 MCP 工具(镜像模型发起的调用)。 |
| 小部件运行时 API | window.openai.sendFollowUpMessage({ prompt, scrollToBottom }) | 请求 ChatGPT 发布由组件撰写的消息。scrollToBottom 是可选的,默认为 true,可以设置为 false 以防止自动滚动。 |
| 小部件运行时 API | window.openai.uploadFile(file, { library?: boolean }) | 上传用户选择的文件并接收 fileId。传入 { library: true } 以在可用时将上传文件同时保存到用户的 ChatGPT 文件库中。 |
| 小部件运行时 API | window.openai.selectFiles() | 打开 ChatGPT 的文件库选择器,并以 { fileId, fileName, mimeType }[] 的格式返回应用授权的文件。请对此辅助函数进行特性检测,因为文件库可能并非对所有用户可用。 |
| 小部件运行时 API | window.openai.getFileDownloadUrl({ fileId }) | 检索由组件上传、从文件库选择、通过文件参数传递或由工具文件引用返回的文件临时下载 URL。 |
| 小部件运行时 API | window.openai.requestDisplayMode(...) | 请求画中画/全屏模式。 |
| 小部件运行时 API | window.openai.requestModal({ params, template }) | 生成一个由 ChatGPT 拥有的模态框。省略 template 以使用当前模板,或传入已注册的模板 URI 以切换模态框内容。 |
| 小部件运行时 API | window.openai.requestClose() | 请求 ChatGPT 关闭当前小部件。 |
| 小部件运行时 API | window.openai.notifyIntrinsicHeight(...) | 上报动态小部件高度以避免滚动裁剪。 |
| 小部件运行时 API | window.openai.openExternal({ href, redirectUrl }) | 在用户的浏览器中打开已审核的外部链接。对于允许重定向的目标,ChatGPT 默认会附加 ?redirectUrl=...;设置 redirectUrl: false 可跳过此操作。 |
| 小部件运行时 API | window.openai.setOpenInAppUrl({ href }) | 可选地覆盖全屏“在 <App> 中打开”的目标。如果未设置,ChatGPT 将保持默认行为并打开小部件当前的 iframe 路径。 |
| 上下文 | window.openai.theme, window.openai.displayMode, window.openai.maxHeight, window.openai.safeArea, window.openai.view, window.openai.userAgent, window.openai.locale | 您可以读取(或通过 useOpenAiGlobal 订阅)的环境信号,以适配视觉效果和文案。 |
useOpenAiGlobal 辅助工具
许多 Apps SDK 项目将 window.openai 访问封装在小的辅助函数中,以便视图保持可测试性。此示例辅助工具会监听宿主 openai:set_globals 事件,并允许 React 组件订阅单个全局值。
export function useOpenAiGlobal<K extends keyof WebplusGlobals>(
key: K
): WebplusGlobals[K] {
return useSyncExternalStore(
(onChange) => {
const handleSetGlobal = (event: SetGlobalsEvent) => {
const value = event.detail.globals[key];
if (value === undefined) {
return;
}
onChange();
};
window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, {
passive: true,
});
return () => {
window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
};
},
() => window.openai[key]
);
}
文件 API
ChatGPT 支持作为可选 window.openai 扩展的文件上传/下载辅助工具。
| API | 目的 | 备注 |
|---|---|---|
window.openai.uploadFile(file, { library?: boolean }) | 上传用户选择的文件并接收 fileId。 | 传入 { library: true } 以在文件库对当前用户可用时,同时将其保存到 ChatGPT 文件库。 |
window.openai.selectFiles() | 为现有文件打开文件库选择器。 | 返回 [{ fileId, fileName, mimeType }]。请对此辅助函数进行特性检测,因为文件库可能并非对所有用户可用。 |
window.openai.getFileDownloadUrl({ fileId }) | 请求文件的临时下载 URL。 | 适用于由组件上传、从文件库选择、通过文件参数传递或由工具文件引用返回的文件。 |
ChatGPT 文件库是可选的,可能并非对每个用户都可用。当辅助函数可用时,从 window.openai.selectFiles() 返回的文件已获授权给当前应用。请在 window.openai.getFileDownloadUrl({ fileId }) 或使用文件参数的工具输入中使用返回的 fileId。
工具文件引用使用蛇形命名法(snake case)字段
{
"download_url": "https://...",
"file_id": "file_...",
"mime_type": "image/png",
"file_name": "input.png"
}
download_url 和 file_id 是必需的。mime_type 和 file_name 是可选的。当组件需要新的临时下载 URL 时,请将 file_id 用作 window.openai.getFileDownloadUrl({ fileId }) 的 fileId 值。
在持久化组件状态时,如果您希望模型在后续对话中看到图像 ID,请使用结构化形状(modelContent, privateContent, imageIds)。
工具描述符参数
需要更多关于这些字段的背景信息?请查看 MCP 服务器指南的高级部分。
默认情况下,工具描述应包含此处列出的字段。
为任何返回 structuredContent 的工具声明 outputSchema。该模式应准确描述您的工具返回的对象,以便客户端验证结果,并使模型能够推断后续工具调用。
工具描述符上的 _meta 字段
请在工具描述符上使用这些 _meta 字段。优先使用 MCP Apps 标准键 _meta.ui.resourceUri 来关联工具与 UI 模板。ChatGPT 支持特定于 OpenAI 的元数据以实现兼容性和可选扩展。
| 键 (Key) | 位置 | 类型 | 限制 | 目的 |
|---|---|---|---|---|
_meta["securitySchemes"] | 工具描述符 | 数组 | 无 | 为仅读取 _meta 的客户端提供向后兼容镜像。 |
_meta.ui.resourceUri | 工具描述符 | 字符串 (URI) | 无 | UI 模板的标准资源 URI。 |
_meta.ui.visibility | 工具描述符 | 字符串数组 | 默认 ["model", "app"] | 控制工具是对模型、UI (app) 还是两者都可用。 |
_meta["openai/outputTemplate"] | 工具描述符 | 字符串 (URI) | 无 | 针对 ChatGPT 中 _meta.ui.resourceUri 的 OpenAI 特定可选/兼容性别名。 |
_meta["openai/widgetAccessible"] | 工具描述符 | boolean | 默认 false | 现有 Apps SDK 应用使用的 OpenAI 特定兼容性字段;优先使用 _meta.ui.visibility + tools/call。 |
_meta["openai/visibility"] | 工具描述符 | string | public (默认) 或 private | 现有 Apps SDK 应用使用的 OpenAI 特定兼容性字段;优先使用 _meta.ui.visibility。 |
_meta["openai/toolInvocation/invoking"] | 工具描述符 | string | ≤ 64 字符 | 工具运行时的简短状态文本。 |
_meta["openai/toolInvocation/invoked"] | 工具描述符 | string | ≤ 64 字符 | 工具完成后显示的简短状态文本。 |
_meta["openai/fileParams"] | 工具描述符 | 字符串数组 | 无 | 表示文件的顶级输入字段列表。每个字段接收 { download_url, file_id, mime_type?, file_name? }。 |
示例
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";
registerAppTool(
server,
"search",
{
title: "Public Search",
description: "Search public documents.",
inputSchema: { q: z.string() },
outputSchema: {
results: z.array(
z.object({
id: z.string(),
title: z.string(),
url: z.string(),
})
),
},
securitySchemes: [
{ type: "noauth" },
{ type: "oauth2", scopes: ["search.read"] },
],
_meta: {
securitySchemes: [
{ type: "noauth" },
{ type: "oauth2", scopes: ["search.read"] },
],
ui: { resourceUri: "ui://widget/story.html" },
// Optional compatibility alias (ChatGPT only):
// "openai/outputTemplate": "ui://widget/story.html",
"openai/toolInvocation/invoking": "Searching…",
"openai/toolInvocation/invoked": "Results ready",
},
},
async ({ q }) => {
const results = await performSearch(q);
return {
structuredContent: { results },
content: [{ type: "text", text: `Found ${results.length} results.` }],
};
}
);
注释
要将工具标记为“只读”,请在工具描述符上使用以下 注释
| 键 (Key) | 类型 | 必需 | 备注 |
|---|---|---|---|
readOnlyHint | boolean | 必需 | 表示该工具仅检索或计算信息,不会在 ChatGPT 之外创建、更新、删除或发送数据。 |
destructiveHint | boolean | 必需 | 声明该工具可能会删除或覆盖用户数据,以便 ChatGPT 在此之前请求明确批准。 |
openWorldHint | boolean | 必需 | 声明该工具发布内容或触及当前用户账户之外的内容,提示客户端在请求批准前总结其影响。 |
idempotentHint | boolean | 可选 | 声明使用相同参数调用该工具对其环境没有额外影响。 |
这些提示仅影响 ChatGPT 如何向用户表述工具调用;服务器必须始终强制执行自己的授权逻辑。
示例
import { z } from "zod";
server.registerTool(
"list_saved_recipes",
{
title: "List saved recipes",
description: "Returns the user’s saved recipes without modifying them.",
inputSchema: {},
outputSchema: {
recipes: z.array(
z.object({
id: z.string(),
title: z.string(),
})
),
},
annotations: { readOnlyHint: true },
},
async () => ({
structuredContent: { recipes: await fetchSavedRecipes() },
})
);
需要更多关于这些字段的背景信息?请查看 MCP 服务器指南的高级部分。
组件资源 _meta 字段
关于这些资源设置的更多详细信息,请参见 MCP 服务器指南的高级部分。
在为您组件提供服务的资源模板 (registerResource) 上设置这些键。它们有助于 ChatGPT 描述和框架化渲染的 iframe,而不会将元数据泄漏给其他客户端。
| 键 (Key) | 位置 | 类型 | 目的 |
|---|---|---|---|
_meta.ui.prefersBorder | 资源内容 | boolean | 提示组件在支持的情况下应在带边框的卡片内渲染。 |
_meta.ui.csp | 资源内容 | object | 标准小部件 CSP 字段的首选元数据界面:connectDomains、resourceDomains 以及可选的 frameDomains。 |
_meta.ui.domain | 资源内容 | 字符串 (origin) | 托管组件的专用源(提交应用时必需;每个应用必须唯一)。默认值为 https://web-sandbox.oaiusercontent.com。 |
_meta["openai/widgetDescription"] | 资源内容 | string | 组件加载时向模型展示的人类可读摘要,可减少冗余的助手叙述。 |
_meta["openai/widgetPrefersBorder"] | 资源内容 | boolean | ChatGPT 中针对 _meta.ui.prefersBorder 的 OpenAI 特定兼容性别名。 |
_meta["openai/widgetCSP"] | 资源内容 | object | 旧版 ChatGPT 小部件 CSP 元数据的兼容性键。标准 CSP 字段已被 _meta.ui.csp 取代,但 redirect_domains 对于受信任的 openExternal 目标仍然是必需的。 |
_meta["openai/widgetDomain"] | 资源内容 | 字符串 (origin) | ChatGPT 中针对 _meta.ui.domain 的 OpenAI 特定兼容性别名。 |
ChatGPT 支持具有以下 snake_case 字段名的旧版 _meta["openai/widgetCSP"] 兼容性键
connect_domains:string[]resource_domains:string[]frame_domains?:string[]redirect_domains?:string[]。针对window.openai.openExternal重定向目标的 ChatGPT 扩展。
标准 _meta.ui.csp 对象通常更适合新应用并支持
connectDomains:string[]。小部件可通过 fetch/XHR 联系的域。resourceDomains:string[]。静态资产(图像、字体、脚本、样式)的域。frameDomains?:string[]。允许 iframe 嵌入的源列表(可选)。默认情况下,小部件不能渲染子框架;添加frameDomains表示启用 iframe 使用,并会触发更严格的应用审核。
然而,_meta.ui.csp 不支持 window.openai.openExternal(...) 链接的 redirect_domains。要将重定向目标加入白名单,您仍然必须设置 _meta["openai/widgetCSP"].redirect_domains。
工具结果
MCP 服务器指南的高级部分提供了关于调整这些响应字段的更多指导。
工具结果可以包含以下字段。值得注意的是
| 键 (Key) | 类型 | 必需 | 备注 |
|---|---|---|---|
structuredContent | object | 可选 | 向模型和组件展示。提供时,必须与声明的 outputSchema 匹配。 |
content | 字符串或 Content[] | 可选 | 向模型和组件展示。 |
_meta | object | 可选 | 仅发送给组件,对模型隐藏。 |
只有 structuredContent 和 content 会出现在对话记录中。宿主将 _meta 转发给组件,以便您可以水合 UI 而无需将数据暴露给模型。
宿主提供的工具结果元数据
| 键 (Key) | 位置 | 类型 | 目的 |
|---|---|---|---|
_meta["openai/widgetSessionId"] | 工具结果 _meta (来自宿主) | string | 当前挂载的小部件实例的稳定 ID;在小部件卸载前,使用它来关联日志和工具调用。 |
示例
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";
registerAppTool(
server,
"get_zoo_animals",
{
title: "get_zoo_animals",
inputSchema: { count: z.number().int().min(1).max(20).optional() },
outputSchema: {
animals: z.array(
z.object({
id: z.string(),
name: z.string(),
species: z.string(),
})
),
},
_meta: { ui: { resourceUri: "ui://widget/widget.html" } },
},
async ({ count = 10 }) => {
const animals = generateZooAnimals(count);
return {
structuredContent: { animals },
content: [{ type: "text", text: `Here are ${animals.length} animals.` }],
_meta: {
allAnimalsById: Object.fromEntries(
animals.map((animal) => [animal.id, animal])
),
},
};
}
);
错误工具结果
要在工具结果上返回错误,请使用以下 _meta 键
| 键 (Key) | 目的 | 类型 | 备注 |
|---|---|---|---|
_meta["mcp/www_authenticate"] | 错误结果 | 字符串或字符串数组 | 触发 OAuth 的 RFC 7235 WWW-Authenticate 挑战。 |
客户端提供的 _meta 字段
有关这些客户端提供的提示的更广泛背景,请参阅 MCP 服务器指南的高级部分。
| 键 (Key) | 提供时 | 类型 | 目的 |
|---|---|---|---|
_meta["openai/locale"] | 初始化 + 工具调用 | 字符串 (BCP 47) | 请求的区域设置(旧版客户端可能会发送 _meta["webplus/i18n"])。 |
_meta["openai/userAgent"] | 工具调用 | string | 用于分析或格式化的用户代理提示。 |
_meta["openai/userLocation"] | 工具调用 | object | 粗略位置提示(city, region, country, timezone, longitude, latitude)。 |
_meta["openai/subject"] | 工具调用 | string | 出于速率限制和识别目的发送给 MCP 服务器的匿名用户 ID。 |
_meta["openai/session"] | 工具调用 | string | 用于关联同一 ChatGPT 会话内工具调用的匿名对话 ID。 |
_meta["openai/organization"] | 工具调用 | string | 在可用时,与当前 ChatGPT 组织相关联的匿名组织 ID。 |
操作阶段的 _meta["openai/userAgent"] 和 _meta["openai/userLocation"] 仅为提示;服务器绝不应依赖它们进行授权决策,并且必须容忍它们的缺失。
示例
import { z } from "zod";
server.registerTool(
"recommend_cafe",
{
title: "Recommend a cafe",
inputSchema: {},
outputSchema: {
cafes: z.array(
z.object({
name: z.string(),
address: z.string(),
})
),
},
},
async (_args, { _meta }) => {
const locale = _meta?.["openai/locale"] ?? "en";
const location = _meta?.["openai/userLocation"]?.city;
const cafes = await findNearbyCafes(location);
return {
content: [{ type: "text", text: formatIntro(locale, location) }],
structuredContent: { cafes },
};
}
);