Appearance
设置你的服务器
概述
你的MCP服务器是每个Apps SDK集成的基础。它暴露模型可以调用的工具、强制执行身份验证,并打包ChatGPT客户端inline渲染所需的结构化数据和component HTML。本指南将通过Python和TypeScript示例介绍核心构建块。
选择SDK
Apps SDK支持任何实现MCP规范的服务器,但官方SDK是最快的入门方式:
- Python SDK(官方) – 非常适合快速原型开发,包括官方FastMCP模块。查看仓库
modelcontextprotocol/python-sdk
。这与社区的"FastMCP"项目不同。 - TypeScript SDK(官方) – 如果你的技术栈已经是Node/React,这是理想选择。使用
@modelcontextprotocol/sdk
。文档:modelcontextprotocol.io
。
安装SDK和你喜欢的任何Web框架(FastAPI或Express是常见选择)。
描述你的工具
工具是ChatGPT和你后端之间的契约。定义清晰的机器名称、人类友好的标题和JSON schema,以便模型知道何时以及如何调用每个工具。这也是你连接每个工具元数据的地方,包括身份验证提示、状态字符串和component配置。
指向component模板
除了返回结构化数据外,MCP服务器上的每个工具还应在其描述符中引用HTML UI模板。这个HTML模板将由ChatGPT在iframe中渲染。
- 注册模板 – 暴露一个资源,其
mimeType
为text/html+skybridge
,其主体加载你编译的JS/CSS bundle。资源URI(例如ui://widget/kanban-board.html
)成为你component的规范ID。 - 将工具链接到模板 – 在工具描述符内部,将
_meta["openai/outputTemplate"]
设置为相同的URI。可选的_meta
字段允许你声明component是否可以发起工具调用或显示自定义状态文本。 - 谨慎版本控制 – 当你发布破坏性component更改时,注册新的资源URI并同步更新工具元数据。ChatGPT会积极缓存模板,因此唯一的URI(或缓存破坏的文件名)可以防止加载过时的资源。
模板和元数据就位后,ChatGPT使用每个工具响应中的 structuredContent
负载来hydrate iframe。
以下是示例:
ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { readFileSync } from "node:fs";
// 创建MCP服务器
const server = new McpServer({
name: "kanban-server",
version: "1.0.0"
});
// 加载本地构建的资源(由你的component构建产生)
const KANBAN_JS = readFileSync("web/dist/kanban.js", "utf8");
const KANBAN_CSS = (() => {
try {
return readFileSync("web/dist/kanban.css", "utf8");
} catch {
return ""; // CSS可选
}
})();
// UI资源(无内联数据赋值;宿主将注入数据)
server.registerResource(
"kanban-widget",
"ui://widget/kanban-board.html",
{},
async () => ({
contents: [
{
uri: "ui://widget/kanban-board.html",
mimeType: "text/html+skybridge",
text: `
<div id="kanban-root"></div>
${KANBAN_CSS ? `<style>${KANBAN_CSS}</style>` : ""}
<script type="module">${KANBAN_JS}</script>
`.trim(),
},
],
})
);
server.registerTool(
"kanban-board",
{
title: "Show Kanban Board",
_meta: {
"openai/outputTemplate": "ui://widget/kanban-board.html",
"openai/toolInvocation/invoking": "Displaying the board",
"openai/toolInvocation/invoked": "Displayed the board"
},
inputSchema: { tasks: z.string() }
},
async () => {
return {
content: [{ type: "text", text: "Displayed the kanban board!" }],
structuredContent: {}
};
}
);
构造你的工具返回的数据
工具响应中的每个工具结果可以包含三个并列字段,这些字段决定了ChatGPT和你的component如何使用负载:
structuredContent
– 用于hydrate你component的结构化数据,例如播放列表的曲目、房地产应用的房屋、看板应用的任务。ChatGPT将此对象注入你的iframe作为window.openai.toolOutput
,因此请将其限定为你UI需要的数据。模型会读取这些值,可能会叙述或总结它们。content
– 可选的自由格式文本(Markdown或纯字符串),模型会逐字接收。_meta
– 仅传递给component的任意JSON。用于不应影响模型推理的数据,例如支持下拉列表的完整位置集。_meta
永远不会显示给模型。
你的component接收所有三个字段,但只有 structuredContent
和 content
对模型可见。如果你想控制component下方的文本,请使用 widgetDescription
。
继续看板示例,获取看板数据并返回三个字段,以便component hydrate时不会向模型暴露额外的上下文:
ts
async function loadKanbanBoard() {
const tasks = [
{ id: "task-1", title: "Design empty states", assignee: "Ada", status: "todo" },
{ id: "task-2", title: "Wireframe admin panel", assignee: "Grace", status: "in-progress" },
{ id: "task-3", title: "QA onboarding flow", assignee: "Lin", status: "done" }
];
return {
columns: [
{ id: "todo", title: "To do", tasks: tasks.filter((task) => task.status === "todo") },
{ id: "in-progress", title: "In progress", tasks: tasks.filter((task) => task.status === "in-progress") },
{ id: "done", title: "Done", tasks: tasks.filter((task) => task.status === "done") }
],
tasksById: Object.fromEntries(tasks.map((task) => [task.id, task])),
lastSyncedAt: new Date().toISOString()
};
}
server.registerTool(
"kanban-board",
{
title: "Show Kanban Board",
_meta: {
"openai/outputTemplate": "ui://widget/kanban-board.html",
"openai/toolInvocation/invoking": "Displaying the board",
"openai/toolInvocation/invoked": "Displayed the board"
},
inputSchema: { tasks: z.string() }
},
async () => {
const board = await loadKanbanBoard();
return {
structuredContent: {
columns: board.columns.map((column) => ({
id: column.id,
title: column.title,
tasks: column.tasks.slice(0, 5) // 为模型保持负载简洁
}))
},
content: [{ type: "text", text: "Here's your latest board. Drag cards in the component to update status." }],
_meta: {
tasksById: board.tasksById, // 仅供component使用的完整任务映射
lastSyncedAt: board.lastSyncedAt
}
};
}
);
构建你的component
现在你已经设置好MCP服务器脚手架,请按照构建自定义UX页面上的说明来构建你的component体验。
本地运行
- 构建你的component bundle(参见构建自定义UX页面上的说明)。
- 启动MCP服务器。
- 将MCP Inspector指向
http://localhost:<port>/mcp
,列出工具并调用它们。
Inspector会验证你的响应是否包含结构化内容和component元数据,并inline渲染component。
暴露公共端点
ChatGPT需要HTTPS。在开发过程中,你可以使用隧道服务,例如ngrok。
在单独的终端窗口中运行:
bash
ngrok http <port>
# Forwarding: https://<subdomain>.ngrok.app -> http://127.0.0.1:<port>
在开发者模式下创建connector时使用生成的URL。对于生产环境,部署到具有低冷启动延迟的HTTPS端点。
添加身份验证和存储层
一旦服务器处理匿名流量,决定是否需要用户身份或持久化。身份验证和存储指南展示了如何添加OAuth 2.1流程、Token验证和数据库集成,以便你可以跟踪用户状态和权限。
Component发起的工具访问
默认情况下,只有ChatGPT可以调用工具。如果你希望component能够直接调用工具(例如,在地图上单击标记获取详细信息),请在 _meta
中添加 openai/componentToolAccess: true
:
ts
server.registerTool(
"get-location-detail",
{
title: "Get Location Detail",
_meta: {
"openai/componentToolAccess": true,
"openai/outputTemplate": "ui://widget/location-detail.html"
},
inputSchema: { locationId: z.string() }
},
async ({ locationId }) => {
const detail = await fetchLocationDetail(locationId);
return {
structuredContent: detail,
content: [{ type: "text", text: `Details for ${detail.name}` }]
};
}
);
现在你的component可以通过 window.openai.callTool
直接调用此工具。
添加component描述
如果工具返回UI component,ChatGPT仍需要文本摘要以在对话中显示并在后续轮次中引用。使用工具响应中的 widgetDescription
字段提供人类可读的一两句话总结,描述component中显示的内容:
ts
return {
structuredContent: { columns },
widgetDescription: "A kanban board with 3 columns and 8 total tasks across them.",
content: [{ type: "text", text: "Here's your board." }]
};
如果省略,ChatGPT会从 structuredContent
和 content
推断描述,但显式描述可以提供更好的上下文。
提供内容安全策略
默认情况下,component在iframe中运行,具有严格的内容安全策略,可防止未经授权的脚本执行和数据泄漏。如果你的component需要从特定域加载资源(例如地图tiles或字体),请在资源 _meta
中声明CSP覆盖:
ts
server.registerResource(
"map-widget",
"ui://widget/map.html",
{
_meta: {
"openai/contentSecurityPolicy": {
"script-src": ["'self'", "https://api.mapbox.com"],
"img-src": ["'self'", "https://*.tile.openstreetmap.org"]
}
}
},
async () => ({ contents: [/* ... */] })
);
ChatGPT会合并你的覆盖与基本策略,因此你只需指定额外的来源。
支持本地化
如果你的应用面向多个地区,component可以通过 window.openai.locale
访问用户的首选语言。使用它来切换UI字符串或日期格式:
ts
const locale = window.openai.locale; // 例如 "en-US", "ja-JP"
const formatter = new Intl.DateTimeFormat(locale);
你的MCP服务器还可以读取 clientContext
头部,以根据区域调整工具响应。
利用客户端上下文提示
ChatGPT在每个工具请求中发送 clientContext
,其中包含用户环境的提示,例如他们的时区、设备类型或显示偏好。你的服务器可以使用此上下文来调整响应:
ts
server.registerTool(
"show-events",
{ title: "Show Events", inputSchema: {} },
async (args, { clientContext }) => {
const timezone = clientContext.timezone || "UTC";
const events = await fetchEvents({ timezone });
return {
structuredContent: { events },
content: [{ type: "text", text: `Events in ${timezone}` }]
};
}
);
后续步骤
通过运行的MCP服务器和component,你已准备好添加身份验证、存储和更多工具。浏览身份验证、存储和示例页面,深入了解每个主题。