你是否曾想过,让 ChatGPT 不仅能回答问题,还能展示一个交互式的待办事项列表、一个3D太阳系模型,甚至是一个披萨点餐界面?OpenAI Apps SDK 让这一切成为可能。本文将为你完整拆解如何利用 Apps SDK 及其生态工具,一步步构建并部署属于你自己的 ChatGPT 嵌入式应用。
文章摘要
OpenAI Apps SDK 允许开发者为 ChatGPT 创建交互式应用界面。其核心是基于 Model Context Protocol (MCP) 协议构建的服务器,用于向模型暴露工具,并结合前端组件(Widget)实现富交互。开发者需构建一个前端组件和一个 MCP 服务器,通过定义工具、返回带有 UI 元数据的响应,即可在 ChatGPT 对话中渲染出丰富的内嵌应用。本文将以官方示例为基础,详解从环境搭建、组件开发、服务器部署到在 ChatGPT 中集成的完整流程。
第一部分:理解核心概念与架构
在开始敲代码之前,我们需要理清几个关键概念。OpenAI Apps SDK 的整个工作流围绕着两个核心部分展开:前端组件和MCP服务器。
什么是 Model Context Protocol (MCP)?
MCP 是一个开放的协议,用于将大型语言模型连接到外部工具、数据和用户界面。你可以把它想象成 ChatGPT 与你的服务之间的一座标准化桥梁。
一个为 Apps SDK 设计的最小化 MCP 服务器需要实现三种核心能力:
-
列出工具:告诉 ChatGPT 你的服务器支持哪些工具,以及每个工具需要的输入和输出格式。 -
调用工具:当 ChatGPT 决定使用某个工具时,它会发送请求,你的服务器执行相应的操作(如查询数据库、处理数据)并返回结构化的结果。 -
返回组件:在返回数据的同时,附带可以渲染成界面的元数据,这样 ChatGPT 就能在对话中直接显示一个交互式的小部件。
关键点:MCP 是传输无关的,你可以使用 Server-Sent Events 或流式 HTTP 来承载它,Apps SDK 对两者都支持。
Apps SDK 如何工作?
你的前端组件(一个 HTML 文件或由框架构建的网页)将被加载到 ChatGPT 界面内的一个 iframe 中。这个组件通过一个名为 window.openai 的全局对象与 ChatGPT 通信。
-
window.openai.toolOutput:当 ChatGPT 加载 iframe 时,它会将最近一次工具调用的结果注入到这个属性中。你的组件可以读取它来初始化界面。 -
window.openai.callTool:你的组件可以通过调用这个函数,主动请求 ChatGPT 去调用 MCP 服务器上的另一个工具,从而实现用户交互。调用后会返回新的结构化内容,使界面保持同步。
简单来说,MCP服务器负责逻辑和数据处理,前端组件负责展示和交互,而Apps SDK则负责将它们无缝地嵌入到ChatGPT的对话流中。
第二部分:探索官方示例与工具库
在动手构建之前,先看看官方提供了哪些“轮子”,这能极大提升我们的开发效率和质量。
1. Apps SDK UI 组件库
为了帮助开发者快速构建出符合 ChatGPT 设计风格的界面,OpenAI 提供了 Apps SDK UI。这是一个基于 Tailwind CSS 和 React 的轻量级设计系统。
它的核心优势包括:
-
开箱即用的设计令牌:提供了预设的颜色、字体、间距、阴影等设计规范。 -
可访问的组件:基于 Radix UI 原语构建,确保了良好的无障碍支持。 -
Tailwind 4 深度集成:直接使用设计令牌定义的工具类,开发体验流畅。
安装与设置非常简单:
npm install @openai/apps-sdk-ui
随后,在你的全局 CSS 文件(如 main.css)开头引入必要的样式:
@import “tailwindcss”;
@import “@openai/apps-sdk-ui/css”;
/* 确保 Tailwind 能扫描到组件库中的类 */
@source “../node_modules/@openai/apps-sdk-ui”;
然后,你就能在 React 组件中导入并使用诸如 <Button>, <Badge>, <Card> 等预制的、风格一致的组件了。
2. Apps SDK 示例库
OpenAI 维护了一个功能丰富的 示例库,其中包含了多个可以直接运行的前端组件和配套的 MCP 服务器。这是绝佳的学习资源和灵感来源。
示例库结构一览:
-
src/:各个示例组件的前端源代码。 -
assets/:构建后生成的 HTML、JS、CSS 打包文件。 -
多种语言的 MCP 服务器示例: -
Pizzaz(Node & Python):一个包含列表、轮播图、地图视图和结账流程的披萨店示例,使用了 Apps SDK UI 库。 -
Solar System(Python):一个交互式的 3D 太阳系查看器。 -
Kitchen Sink Lite(Node & Python):一个“全能”示例,演示了读取/设置组件状态、调用工具、使用宿主 API(如打开外部链接)等所有核心功能。 -
Shopping Cart(Python):演示如何利用 widgetSessionId在多次工具调用间保持购物车状态。 -
Authenticated(Python):演示需要 OAuth 认证的工具调用。
-
运行示例的准备工作:
-
环境要求:Node.js 18+,Python 3.10+,推荐使用 pnpm 包管理器。 -
通用步骤: -
克隆仓库并安装依赖: pnpm install -
构建所有组件: pnpm run build(这会生成assets/目录下的静态文件) -
启动静态文件服务器: pnpm run serve(在http://localhost:4444提供服务) -
进入具体服务器目录,按说明启动对应的 MCP 服务器(例如 uvicorn pizzaz_server_python.main:app --port 8000)。
-
注意:如果你使用 Chrome 142 及以上版本,需要禁用 #local-network-access-check 标志(在 chrome://flags/ 中设置)才能正常查看本地运行的组件 UI,修改后记得重启浏览器。
第三部分:实战演练 – 构建待办事项应用
理论看再多,不如亲手做一遍。让我们跟随官方快速入门指南,构建一个最简单的待办事项应用。
第一步:创建前端组件 (todo-widget.html)
我们创建一个独立的 HTML 文件,它包含了所有的样式、结构和逻辑。
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”utf-8” />
<title>Todo list</title>
<style>
/* 样式代码:定义字体、颜色、布局、表单和列表样式 */
:root { font-family: “Inter”, system-ui, -apple-system, sans-serif; }
body { background: #f6f8fb; padding: 16px; }
main { background: #fff; max-width: 360px; border-radius: 16px; padding: 20px; box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08); }
/* … 更多具体样式定义 */
</style>
</head>
<body>
<main>
<h2>Todo list</h2>
<form id=”add-form”>
<input id=”todo-input” placeholder=”Add a task” />
<button type=”submit”>Add</button>
</form>
<ul id=”todo-list”></ul>
</main>
<script type=”module”>
// 核心逻辑代码
const listEl = document.querySelector(“#todo-list”);
const formEl = document.querySelector(“#add-form”);
const inputEl = document.querySelector(“#todo-input”);
// 1. 初始化任务列表,优先从ChatGPT注入的数据中读取
let tasks = […(window.openai?.toolOutput?.tasks ?? [])];
// 2. 渲染函数:根据tasks数组生成列表DOM
const render = () => { /* … 动态创建li元素 … */ };
// 3. 监听全局状态更新事件(由ChatGPT触发)
window.addEventListener(“openai:set_globals”, (event) => {
if (event.detail?.globals?.toolOutput?.tasks) {
tasks = event.detail.globals.toolOutput.tasks;
render();
}
});
// 4. 调用工具的统一函数
const callTodoTool = async (name, payload) => {
if (window.openai?.callTool) {
// 在ChatGPT环境中,通过SDK调用
const response = await window.openai.callTool(name, payload);
if (response?.structuredContent?.tasks) {
tasks = response.structuredContent.tasks;
render();
}
} else {
// 本地测试时,模拟工具调用
if (name === “add_todo”) {
tasks = […tasks, { id: crypto.randomUUID(), title: payload.title, completed: false }];
}
if (name === “complete_todo”) {
tasks = tasks.map(task => task.id === payload.id ? { …task, completed: true } : task);
}
render();
}
};
// 5. 绑定表单提交事件(添加任务)
formEl.addEventListener(“submit”, async (e) => {
e.preventDefault();
const title = inputEl.value.trim();
if (title) {
await callTodoTool(“add_todo”, { title });
inputEl.value = “”;
}
});
// 6. 绑定列表复选框变化事件(完成任务)
listEl.addEventListener(“change”, async (e) => {
const checkbox = e.target;
if (checkbox.type === “checkbox”) {
const id = checkbox.closest(“li”)?.dataset.id;
if (id) {
await callTodoTool(“complete_todo”, { id });
}
}
});
// 初始渲染
render();
</script>
</body>
</html>
第二步:构建 MCP 服务器 (server.js)
前端负责交互,后端(MCP服务器)负责提供工具和数据处理。我们使用 Node.js 和官方 MCP SDK。
import { createServer } from “node:http”;
import { readFileSync } from “node:fs”;
import { McpServer } from “@modelcontextprotocol/sdk/server/mcp.js”;
import { StreamableHTTPServerTransport } from “@modelcontextprotocol/sdk/server/streamableHttp.js”;
import { z } from “zod”;
// 读取我们刚写好的HTML组件
const todoHtml = readFileSync(“public/todo-widget.html”, “utf8”);
let todos = []; // 简单的内存存储
let nextId = 1;
function createTodoServer() {
const server = new McpServer({ name: “todo-app”, version: “0.1.0” });
// 关键一步:注册UI资源,让ChatGPT知道如何获取这个组件
server.registerResource(
“todo-widget”,
“ui://widget/todo.html”, // 这个URI将在工具元数据中引用
{},
async () => ({
contents: [{
uri: “ui://widget/todo.html”,
mimeType: “text/html+skybridge”, // 特殊的MIME类型,表示这是Apps SDK组件
text: todoHtml,
_meta: { “openai/widgetPrefersBorder”: true } // 可选元数据:建议显示边框
}]
})
);
// 注册“添加待办”工具
server.registerTool(
“add_todo”,
{
title: “Add todo”,
description: “Creates a todo item with the given title.”,
inputSchema: { title: z.string().min(1) }, // 使用Zod定义输入参数校验
_meta: {
“openai/outputTemplate”: “ui://widget/todo.html”, // 关键:指定此工具的响应用哪个UI组件渲染
“openai/toolInvocation/invoking”: “Adding todo”, // 调用中显示的文本
“openai/toolInvocation/invoked”: “Added todo”, // 调用完成显示的文本
}
},
async (args) => {
const title = args?.title?.trim?.() ?? “”;
if (!title) {
return {
content: [{ type: “text”, text: “Missing title.” }],
structuredContent: { tasks: todos }
};
}
const todo = { id: `todo-${nextId++}`, title, completed: false };
todos = […todos, todo];
return {
content: [{ type: “text”, text: `Added “${todo.title}”.` }],
structuredContent: { tasks: todos } // 返回结构化的任务列表数据
};
}
);
// 注册“完成待办”工具(结构类似)
server.registerTool(
“complete_todo”,
{
title: “Complete todo”,
description: “Marks a todo as done by id.”,
inputSchema: { id: z.string().min(1) },
_meta: {
“openai/outputTemplate”: “ui://widget/todo.html”,
“openai/toolInvocation/invoking”: “Completing todo”,
“openai/toolInvocation/invoked”: “Completed todo”,
}
},
async (args) => {
const id = args?.id;
const todo = todos.find(task => task.id === id);
if (todo) {
todos = todos.map(task => task.id === id ? { …task, completed: true } : task);
return {
content: [{ type: “text”, text: `Completed “${todo.title}”.` }],
structuredContent: { tasks: todos }
};
}
return {
content: [{ type: “text”, text: `Todo ${id} was not found.` }],
structuredContent: { tasks: todos }
};
}
);
return server;
}
// 创建HTTP服务器,在指定路径(/mcp)上挂载MCP服务
const httpServer = createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host ?? “localhost”}`);
// 处理CORS预检请求
if (req.method === “OPTIONS” && url.pathname === “/mcp”) {
res.writeHead(204, { …CORS headers }).end();
return;
}
// 将 /mcp 路径的请求交给MCP服务器处理
if (url.pathname === “/mcp” && [“POST”, “GET”, “DELETE”].includes(req.method)) {
const server = createTodoServer();
const transport = new StreamableHTTPServerTransport({ … });
await server.connect(transport);
await transport.handleRequest(req, res);
return;
}
// 其他请求返回404
res.writeHead(404).end(“Not Found”);
});
httpServer.listen(8787, () => console.log(“Todo MCP server listening on http://localhost:8787/mcp”));
第三步:本地运行与测试
-
启动服务器:确保 package.json中设置了”type”: “module”,然后运行node server.js。 -
使用 MCP Inspector 测试:这是一个可视化测试工具,可以检查你的服务器是否正常列出了工具,以及工具调用是否正确。 npx @modelcontextprotocol/inspector@latest http://localhost:8787/mcp -
暴露到公网:为了让远端的 ChatGPT 能访问你本地的服务器,需要使用内网穿透工具如 ngrok。 ngrok http 8787命令执行后会生成一个临时的公网 URL(如
https://abc123.ngrok.app)。
第四步:在 ChatGPT 中集成
-
在 ChatGPT 设置中启用 “开发者模式”。 -
进入 “设置” > “连接器”,点击 “创建”。 -
在 URL 字段中,填入你的 MCP 服务器端点,即上一步获得的 ngrok URL 加上 /mcp路径(例如:https://abc123.ngrok.app/mcp)。 -
为你的应用命名(如“我的待办事项”),添加描述,然后创建。 -
打开一个新对话,点击输入框旁的 “+” 或 “更多” 按钮,从列表中选择你刚刚添加的应用。 -
现在,你可以尝试对 ChatGPT 说:“帮我添加一个任务:阅读 Apps SDK 文档”。ChatGPT 将会调用你的 add_todo工具,并在对话中渲染出交互式的待办事项列表。你可以直接在列表里勾选完成任务。
第四部分:进阶技巧与最佳实践
在掌握了基础构建流程后,了解以下概念能让你的应用更强大、更健壮。
1. 状态管理:widgetSessionId 的妙用
在购物车示例中,演示了一个重要概念:跨工具调用的状态保持。当用户通过多次对话(如“添加牛奶”、“再添加面包”)修改购物车时,如何让模型和组件看到同一个最新状态?
答案是使用 _meta[“widgetSessionId”]。MCP 服务器在响应中返回一个唯一的会话 ID。之后,每当组件通过 window.openai.callTool 调用工具时,这个 ID 会自动附加到请求中。服务器可以利用这个 ID 来查找和更新之前的状态(例如,存储在服务器内存或数据库中的购物车数据),确保状态的连续性和一致性。
2. 组件与宿主的深度交互
Kitchen Sink Lite 示例展示了前端组件如何深度利用 window.openai 提供的宿主能力:
-
setWidgetState:组件可以主动更新自己的状态,这个状态会持久化并能在后续工具调用中被模型看到。 -
requestDisplayMode:请求切换显示模式(如紧凑或展开)。 -
openExternal:安全地打开外部链接。 -
sendFollowUpMessage:以助理的身份自动发送一条后续消息。
3. 部署注意事项
当你准备将应用部署到生产环境时:
-
设置环境变量:在服务器环境中设置 BASE_URL=https://your-server.com。这用于生成正确的静态资源引用路径。 -
持久化状态:像购物车示例中提到的,生产环境应将状态(如购物车内容)持久化在服务器端的数据库或缓存中,而非仅存在内存。 -
认证集成:对于需要用户登录的工具,可以参考 Authenticated Server 示例,实现 OAuth 等认证流程。
FAQ:常见问题解答
Q:我必须使用 React 和 Apps SDK UI 库吗?
A:完全不是。你的前端组件可以用任何你喜欢的技术栈构建(Vanilla HTML/JS, Vue, Svelte 等)。Apps SDK UI 库只是一个为了提升开发效率和一致性的可选工具包。
Q:MCP 服务器只能用 Node.js 或 Python 写吗?
A:不。MCP 是一个协议,任何能实现该协议的语言都可以。官方提供了 TypeScript/Node.js 和 Python 的 SDK,社区也可能有其他语言的实现。
Q:每次更新了 MCP 服务器(比如添加了新工具),需要做什么?
A:你需要在 ChatGPT 的 “设置” > “连接器” 页面,找到你的应用,点击 “刷新” 按钮。这样 ChatGPT 才会重新获取你服务器最新的工具列表和配置。
Q:我的应用可以收费吗?
A:本文档未涉及商业化政策。请关注 OpenAI 官方平台的开发者条款和指南。
Q:除了 iframe 渲染 UI,MCP 还能做什么?
A:MCP 的核心是提供“工具”。即使不返回 UI 组件,你也可以通过它让 ChatGPT 调用你的 API、查询你的数据库或执行任何你定义的操作。返回 UI 组件(Widget)是 Apps SDK 在 MCP 基础上增加的、专用于丰富 ChatGPT 交互体验的能力。
总结与启程
通过本文,你已经了解了 OpenAI Apps SDK 的核心理念、掌握了从零构建一个交互式 ChatGPT 应用的全套流程,并窥见了更高级的应用模式。从简单的待办列表到复杂的3D可视化,ChatGPT 应用生态的想象力边界正由开发者们定义。
你的下一步可以是:
-
深入探索示例库,运行 Pizzaz 或 Solar System 示例,感受更复杂的交互。 -
定制化你的数据:修改示例服务器中的处理程序,将其连接到你的真实业务系统(数据库、API)。 -
创造全新组件:在示例库的 src/目录中放入你的新组件,它会自动被构建系统打包。然后仿照示例,编写一个专属的 MCP 服务器来驱动它。
构建 ChatGPT 应用不仅仅是创造一个新功能,更是在塑造人与 AI 协作的未来界面。现在,是时候将你的想法,变成 ChatGPT 对话中那个令人惊叹的交互瞬间了。

