关于 chatgpt-app-builder
面向开发者的技能,使用 mcp-use 和 OpenAI Apps SDK 构建带交互式 React 组件的 ChatGPT 应用。组件可自动注册,并支持 MCP Apps 与 ChatGPT 协议。
主要功能
- 01通过模板初始化应用
- 02自动注册 React 组件
- 03内置 useWidget 状态与工具调用
- 04自动打包并支持热重载
- 05配置 CSP 并用 Inspector 测试
- 06支持 MCP Apps 和 ChatGPT
适用场景
- 01创建带交互组件的 ChatGPT 应用
- 02构建可调用 MCP 工具的组件
- 03在 Inspector 中测试组件
- 04让组件兼容 MCP Apps 客户端
MCP 相关
指导开发者使用 mcp-use 构建 MCP 服务,将组件注册为 MCP 工具和资源,并支持 MCP Apps 客户端。也支持 ChatGPT Apps SDK 协议。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mcp-use/ skills --skill chatgpt-app-builder安装后在 Claude Code 中输入/chatgpt-app-builder确认已加载。
附带文件LICENSE.txt
详细介绍
ChatGPT 应用构建器
使用 mcp-use 框架和 OpenAI Apps SDK 构建可用于生产环境的 ChatGPT 应用,并配备交互式小组件。此技能提供零配置小组件开发,并支持自动注册和内置 React Hooks。
快速入门
始终使用 MCP Apps 模板进行初始化:
npx create-mcp-use-app my-chatgpt-app --template mcp-apps
cd my-chatgpt-app
yarn install
yarn dev
这会创建以下项目结构:
my-chatgpt-app/
├── resources/ # React widgets (auto-registered!)
│ ├── display-weather.tsx # Example widget
│ └── product-card.tsx # Another widget
├── public/ # Static assets
│ └── images/
├── index.ts # MCP server entry
├── package.json
├── tsconfig.json
└── README.md
为什么使用 mcp-use 构建 ChatGPT 应用?
传统的 OpenAI Apps SDK 需要大量手动设置:
- 独立的项目结构(
server/和web/文件夹) - 手动配置 esbuild/webpack
- 自行实现
useWidgetStateHook - 手动编写 React 挂载代码
- 手动配置 CSP
- 手动注册小组件
mcp-use 简化了所有这些工作:
- ✅ 一条命令即可完成设置
- ✅ 将小组件放入
resources/文件夹,即可自动注册 - ✅ 内置
useWidget()Hook,支持状态、属性和工具调用 - ✅ 自动打包并支持热重载
- ✅ 自动配置 CSP
- ✅ 内置用于测试的 Inspector
- ✅ 支持双协议(同时兼容 ChatGPT 和 MCP Apps 客户端)
MCP Apps 与 ChatGPT Apps SDK
mcp-use 支持多种小组件协议,确保最大兼容性:
| 协议 | 用途 | 兼容性 | 状态 |
|---|---|---|---|
MCP Apps(type: "mcpApps") | 最大兼容性 | ✅ ChatGPT + MCP Apps 客户端 | 推荐 |
ChatGPT Apps SDK(type: "appsSdk") | 仅限 ChatGPT 的功能 | ✅ 仅限 ChatGPT | 支持 |
| MCP-UI | 简单的静态内容 | ✅ 仅限 MCP 客户端 | 专用 |
为什么选择 MCP Apps?
MCP Apps 是 Model Context Protocol 中交互式小组件的官方标准(SEP-1865):
- 通用:兼容 ChatGPT、Claude Desktop、Goose 以及所有 MCP Apps 客户端
- 面向未来:基于开放规范,确保长期兼容性
- 安全:采用双层 iframe 沙箱,并支持细粒度 CSP 控制
- 零配置:使用
type: "mcpApps"时,mcp-use 会自动为两种协议生成元数据
要点:在服务端配置中使用 type: "mcpApps" 时,你的小组件会自动兼容 ChatGPT(Apps SDK 协议)和 MCP Apps 客户端。你只需编写一次小组件,mcp-use 会处理协议转换。
创建小组件
简单小组件(单个文件)
创建 resources/weather-display.tsx:
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
// Define widget metadata
export const widgetMetadata: WidgetMetadata = {
description: "Display current weather for a city",
props: z.object({
city: z.string().describe("City name"),
temperature: z.number().describe("Temperature in Celsius"),
conditions: z.string().describe("Weather conditions"),
humidity: z.number().describe("Humidity percentage"),
}),
};
const WeatherDisplay: React.FC = () => {
const { props, isPending } = useWidget();
// Always handle loading state first
if (isPending) {
return (
<McpUseProvider autoSize>
<div className="animate-pulse p-4">Loading weather...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div className="weather-card p-4 rounded-lg shadow">
<h2 className="text-2xl font-bold">{props.city}</h2>
<div className="temp text-4xl">{props.temperature}°C</div>
<p className="conditions">{props.conditions}</p>
<p className="humidity">Humidity: {props.humidity}%</p>
</div>
</McpUseProvider>
);
};
export default WeatherDisplay;
就是这样!该小组件会自动:
- 注册为 MCP 工具
weather-display - 注册为 MCP 资源
ui://widget/weather-display.html - 打包以兼容 Apps SDK
- 可在 ChatGPT 中使用
复杂小组件(文件夹结构)
对于包含多个组件的小组件:
resources/
└── product-search/
├── widget.tsx # Entry point (required name)
├── components/
│ ├── ProductCard.tsx
│ └── FilterBar.tsx
├── hooks/
│ └── useFilter.ts
├── types.ts
└── constants.ts
入口文件(widget.tsx):
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
import { ProductCard } from "./components/ProductCard";
import { FilterBar } from "./components/FilterBar";
export const widgetMetadata: WidgetMetadata = {
description: "Display product search results with filtering",
props: z.object({
products: z.array(
z.object({
id: z.string(),
name: z.string(),
price: z.number(),
image: z.string(),
})
),
query: z.string(),
}),
};
const ProductSearch: React.FC = () => {
const { props, isPending, state, setState } = useWidget();
if (isPending) {
return (
<McpUseProvider autoSize>
<div>Loading...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div>
<h1>Search: {props.query}</h1>
<FilterBar onFilter={(filters) => setState({ filters })} />
<div className="grid grid-cols-3 gap-4">
{props.products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
</div>
</McpUseProvider>
);
};
export default ProductSearch;
小组件元数据
自动注册所需的元数据:
export const widgetMetadata: WidgetMetadata = {
// Required: Human-readable description
description: "Display weather information",
// Required: Zod schema for widget props
props: z.object({
city: z.string().describe("City name"),
temperature: z.number(),
}),
// Optional: Disable automatic tool registration
exposeAsTool: true, // default
// Optional: Unified metadata (works for BOTH ChatGPT and MCP Apps)
metadata: {
csp: {
connectDomains: ["https://api.weather.com"],
resourceDomains: ["https://cdn.weather.com"],
},
prefersBorder: true,
autoResize: true,
widgetDescription: "Interactive weather display",
},
};
重要提示:
description:用于工具和资源的描述props:Zod schema 定义小组件的输入参数exposeAsTool:如果只通过自定义工具使用小组件,则设为falsemetadata:兼容两种协议的统一配置(推荐)
内容安全策略(CSP)
使用 CSP 配置控制小组件可访问的外部资源:
export const widgetMetadata: WidgetMetadata = {
description: "Weather widget",
props: z.object({ city: z.string() }),
metadata: {
csp: {
// APIs your widget needs to call
connectDomains: ["https://api.weather.com", "https://weather-backup.com"],
// Static assets (images, fonts, stylesheets)
resourceDomains: ["https://cdn.weather.com"],
// External content to embed in iframes
frameDomains: ["https://embed.weather.com"],
// Script CSP directives (use carefully!)
scriptDirectives: ["'unsafe-inline'"],
},
},
};
CSP 字段参考:
connectDomains:通过 fetch、WebSocket、XMLHttpRequest 调用的 APIresourceDomains:用于加载图像、字体、样式表和视频frameDomains:用于在 iframe 中嵌入外部内容scriptDirectives:script-src CSP 指令(生产环境中避免使用'unsafe-eval')
安全最佳实践:
- ✅ 指定确切的域名:
https://api.weather.com - ❌ 避免使用通配符:
https://*.weather.com(安全性较低) - ❌ 除非绝对必要,否则切勿使用
'unsafe-eval' - ✅ 部署前在开发环境中测试 CSP
元数据配置选项
现代统一方式(推荐)
使用 metadata 字段支持双协议:
export const widgetMetadata: WidgetMetadata = {
description: "Weather widget",
props: propSchema,
metadata: {
// Works for BOTH MCP Apps AND ChatGPT
csp: {
connectDomains: ["https://api.weather.com"],
resourceDomains: ["https://cdn.weather.com"],
},
prefersBorder: true,
autoResize: true,
widgetDescription: "Displays current weather",
},
};
旧版 Apps SDK 方式(已弃用)
旧版仅适用于 ChatGPT 的格式(仍受支持,但不推荐):
export const widgetMetadata: WidgetMetadata = {
description: "Weather widget",
props: propSchema,
appsSdkMetadata: {
// ChatGPT only - snake_case with openai/ prefix
"openai/widgetCSP": {
connect_domains: ["https://api.weather.com"],
resource_domains: ["https://cdn.weather.com"],
},
"openai/widgetPrefersBorder": true,
"openai/toolInvocation/invoking": "Loading...",
"openai/toolInvocation/invoked": "Loaded",
},
};
迁移说明:旧格式使用带有 openai/ 前缀的 appsSdkMetadata 和蛇形命名法(例如,connect_domains)。新格式使用驼峰命名法的 metadata(例如,connectDomains),且适用于两种协议。
同时使用两种方式实现 ChatGPT 自定义功能
你可以组合使用这两个字段,以便使用标准元数据和 ChatGPT 专属覆盖项:
export const widgetMetadata: WidgetMetadata = {
description: "Weather widget",
props: propSchema,
// Unified metadata (dual-protocol)
metadata: {
csp: { connectDomains: ["https://api.weather.com"] },
prefersBorder: true,
},
// ChatGPT-specific overrides/additions
appsSdkMetadata: {
"openai/widgetDescription": "ChatGPT-specific description",
"openai/customFeature": "some-value", // Any custom OpenAI metadata
"openai/locale": "en-US",
},
};
使用场景:如果需要传递统一格式中不存在的 OpenAI 专属自定义元数据,请将其添加到 appsSdkMetadata 中。这些字段会带有 openai/ 前缀,直接传递给 ChatGPT。
useWidget Hook
useWidget Hook 提供了你需要的一切:
const {
// Widget props from tool input
props,
// Loading state (true = tool still executing)
isPending,
// Persistent widget state
state,
setState,
// Theme from host (light/dark)
theme,
// Call other MCP tools
callTool,
// Display mode control
displayMode,
requestDisplayMode,
// Additional tool output
output,
} = useWidget<MyPropsType, MyOutputType>();
属性和加载状态
重要: 工具执行完成前,Widget 就会渲染。务必处理 isPending:
const { props, isPending } = useWidget<WeatherProps>();
// Pattern 1: Early return
if (isPending) {
return <div>Loading...</div>;
}
// Now props are safe to use
// Pattern 2: Conditional rendering
return <div>{isPending ? <LoadingSpinner /> : <div>{props.city}</div>}</div>;
// Pattern 3: Optional chaining (partial UI)
return (
<div>
<h1>{props.city ?? "Loading..."}</h1>
</div>
);
Widget 状态
在多次 Widget 交互间持久化数据:
const { state, setState } = useWidget();
// Save state (persists in ChatGPT localStorage)
const addFavorite = async (city: string) => {
await setState({
favorites: [...(state?.favorites || []), city],
});
};
// Update with function
await setState((prev) => ({
...prev,
count: (prev?.count || 0) + 1,
}));
调用 MCP 工具
Widget 可以调用其他工具:
const { callTool } = useWidget();
const refreshData = async () => {
try {
const result = await callTool("get-weather", {
city: "Tokyo",
});
console.log("Result:", result.content);
} catch (error) {
console.error("Tool call failed:", error);
}
};
显示模式控制
请求不同的显示模式:
const { displayMode, requestDisplayMode } = useWidget();
const goFullscreen = async () => {
await requestDisplayMode("fullscreen");
};
// Current mode: 'inline' | 'pip' | 'fullscreen'
console.log(displayMode);
使用 Widget 创建自定义工具
创建支持双协议并返回 Widget 的工具:
import { MCPServer, widget, text } from "mcp-use/server";
import { z } from "zod";
const server = new MCPServer({
name: "weather-app",
version: "1.0.0",
});
server.tool(
{
name: "get-weather",
description: "Get current weather for a city",
schema: z.object({
city: z.string().describe("City name"),
}),
// Widget config (registration-time metadata)
widget: {
name: "weather-display", // Must match widget in resources/
invoking: "Fetching weather...",
invoked: "Weather data loaded",
},
},
async ({ city }) => {
// Fetch data from API
const data = await fetchWeatherAPI(city);
// Return widget with runtime data
return widget({
props: {
city,
temperature: data.temp,
conditions: data.conditions,
humidity: data.humidity,
},
output: text(`Weather in ${city}: ${data.temp}°C`),
message: `Current weather for ${city}`,
});
}
);
server.listen();
要点:
- 服务配置中的
baseUrl可确保正确加载资源 - Widget 会自动兼容 ChatGPT 和 MCP Apps 客户端
- 在工具定义中使用
widget: { name, invoking, invoked } widget({ props, output })辅助函数会返回运行时数据props会传递给 Widget,output会展示给模型- Widget 必须位于
resources/文件夹中
静态资源
使用 public/ 文件夹存放图片、字体等资源:
my-app/
├── resources/
├── public/ # Static assets
│ ├── images/
│ │ ├── logo.svg
│ │ └── banner.png
│ └── fonts/
└── index.ts
在 Widget 中使用资源:
import { Image } from "mcp-use/react";
function MyWidget() {
return (
<div>
{/* Paths relative to public/ folder */}
<Image src="/images/logo.svg" alt="Logo" />
<img src={window.__getFile?.("images/banner.png")} alt="Banner" />
</div>
);
}
组件
McpUseProvider
整合所有通用设置的统一 Provider:
import { McpUseProvider } from "mcp-use/react";
function MyWidget() {
return (
<McpUseProvider
autoSize // Auto-resize widget
viewControls // Add debug/fullscreen buttons
debug // Show debug info
>
<div>Widget content</div>
</McpUseProvider>
);
}
Image 组件
同时处理数据 URL 和公共路径:
import { Image } from "mcp-use/react";
function MyWidget() {
return (
<div>
<Image src="/images/photo.jpg" alt="Photo" />
<Image src="data:image/png;base64,..." alt="Data URL" />
</div>
);
}
ErrorBoundary
优雅地处理错误:
import { ErrorBoundary } from "mcp-use/react";
function MyWidget() {
return (
<ErrorBoundary
fallback={<div>Something went wrong</div>}
onError={(error) => console.error(error)}
>
<MyComponent />
</ErrorBoundary>
);
}
测试
使用 Inspector
-
启动开发服务器:
yarn dev -
打开 Inspector:
- 前往
http://localhost:3000/inspector
- 前往
-
测试 Widget:
- 点击 Tools 标签页
- 找到你的 Widget 工具
- 输入测试参数
- 执行以查看 Widget 渲染效果
-
调试交互:
- 使用浏览器控制台
- 检查 RPC 日志
- 测试状态持久化
- 验证工具调用
在 ChatGPT 中测试
-
启用开发者模式:
- 设置 → 连接器 → 高级 → 开发者模式
-
添加你的服务:
- 前往连接器标签页
- 添加远程 MCP 服务 URL
-
在对话中测试:
- 从 Plus 菜单中选择开发者模式
- 选择你的连接器
- 让 ChatGPT 使用你的工具
提示词技巧:
- 明确说明:“使用 weather-app 连接器的 get-weather 工具……”
- 禁止使用其他方案:“不要使用内置工具,只使用我的连接器”
- 指定输入:“使用 { city: 'Tokyo' } 调用 get-weather”
双协议说明:在服务配置中使用 type: "mcpApps" 时,你的 Widget 会自动兼容 ChatGPT(通过 Apps SDK)和 MCP Apps 客户端(例如 Claude Desktop、Goose)。无需更改代码,即可在多个客户端中测试同一个 Widget!
最佳实践
Schema 设计
使用描述清晰的 Schema:
// ✅ Good
const schema = z.object({
city: z.string().describe("City name (e.g., Tokyo, Paris)"),
temperature: z.number().min(-50).max(60).describe("Temp in Celsius"),
});
// ❌ Bad
const schema = z.object({
city: z.string(),
temp: z.number(),
});
主题支持
始终支持两种主题:
const { theme } = useWidget();
const bgColor = theme === "dark" ? "bg-gray-900" : "bg-white";
const textColor = theme === "dark" ? "text-white" : "text-gray-900";
加载状态
始终先检查 isPending:
const { props, isPending } = useWidget<MyProps>();
if (isPending) {
return <LoadingSpinner />;
}
// Now safe to access props.field
return <div>{props.field}</div>;
Widget 聚焦
让 Widget 保持专注:
// ✅ Good: Single purpose
export const widgetMetadata: WidgetMetadata = {
description: "Display weather for a city",
props: z.object({ city: z.string() }),
};
// ❌ Bad: Too many responsibilities
export const widgetMetadata: WidgetMetadata = {
description: "Weather, forecast, map, news, and more",
props: z.object({
/* many fields */
}),
};
错误处理
妥善处理错误:
const { callTool } = useWidget();
const fetchData = async () => {
try {
const result = await callTool("fetch-data", { id: "123" });
if (result.isError) {
console.error("Tool returned error");
}
} catch (error) {
console.error("Tool call failed:", error);
}
};
配置
生产环境设置
为生产环境设置基础 URL:
const server = new MCPServer({
name: "my-app",
version: "1.0.0",
baseUrl: process.env.MCP_URL || "https://myserver.com",
});
环境变量
# Server URL
MCP_URL=https://myserver.com
# For static deployments
MCP_SERVER_URL=https://myserver.com/api
CSP_URLS=https://cdn.example.com,https://api.example.com
变量用途:
MCP_URL:Widget 资源和 CSP 的基础 URLMCP_SERVER_URL:用于工具调用的 MCP 服务 URL(静态部署)CSP_URLS:内容安全策略的其他域名
部署
部署到 mcp-use Cloud
# Login
npx mcp-use login
# Deploy
yarn deploy
面向生产环境构建
# Build
yarn build
# Start
yarn start
构建流程:
- 编译 TypeScript
- 打包 React 小部件
- 优化资源
- 生成生产环境 HTML
常见模式
数据获取小部件
const DataWidget: React.FC = () => {
const { props, isPending, callTool } = useWidget();
if (isPending) {
return <div>Loading...</div>;
}
const refresh = async () => {
await callTool("fetch-data", { id: props.id });
};
return (
<div>
<h1>{props.title}</h1>
<button onClick={refresh}>Refresh</button>
</div>
);
};
有状态小部件
const CounterWidget: React.FC = () => {
const { state, setState } = useWidget();
const increment = async () => {
await setState({
count: (state?.count || 0) + 1,
});
};
return (
<div>
<p>Count: {state?.count || 0}</p>
<button onClick={increment}>+1</button>
</div>
);
};
主题化小部件
const ThemedWidget: React.FC = () => {
const { theme } = useWidget();
return (
<div className={theme === "dark" ? "dark-theme" : "light-theme"}>
Content
</div>
);
};
故障排除
小部件未显示
问题: 小部件文件存在,但工具未显示
解决方案:
- 确保使用
.tsx扩展名 - 导出
widgetMetadata对象 - 导出默认 React 组件
- 检查服务日志中的错误
- 确认小部件名称与文件/文件夹名称一致
未收到 Props
问题: 组件收到的 props 为空
解决方案:
- 先检查
isPending(等待期间 props 为空) - 使用
useWidget()hook(而不是 React props) - 确认
widgetMetadata.props是有效的 Zod 架构 - 检查工具参数是否与架构匹配
CSP 错误
问题: 小部件已加载,但资源加载失败
解决方案:
- 在服务配置中设置
baseUrl - 通过
metadata.csp(现代方式)或appsSdkMetadata['openai/widgetCSP'](旧方式)将域名添加到 CSP - 所有资源均使用 HTTPS
- 检查浏览器控制台中的 CSP 违规信息
生产环境中的 CSP 错误
问题: 生产环境中的资源被内容安全策略拦截
解决方案:
- 检查浏览器控制台中的 CSP 违规消息
- 将缺少的域名添加到 CSP 配置中:
metadata: { csp: { connectDomains: ['https://api.example.com'], // Add missing API domain resourceDomains: ['https://cdn.example.com'], // Add missing CDN domain } } - 使用确切的域名——在生产环境中避免使用通配符
- 部署前在 Inspector 中测试,以便及早发现 CSP 问题
- 环境变量替代方案:设置
CSP_URLS环境变量,并使用逗号分隔域名
协议兼容性问题
问题: 小部件在 ChatGPT 中可用,但在 MCP Apps 客户端中不可用(反之亦然)
解决方案:
- 使用
type: "mcpApps"以支持双协议(推荐) - 检查服务配置中的
baseUrl是否设置正确 - 确认元数据格式:双协议场景使用
metadata(驼峰命名),而不是appsSdkMetadata(蛇形命名) - 在 Inspector 中测试,它同时支持两种协议
何时使用各类型:
type: "mcpApps"- 兼容性最高(推荐)type: "appsSdk"- 仅适用于 ChatGPT(需要使用规范中未包含的 ChatGPT 专属功能时选用)
了解更多
- 文档:https://docs.mcp-use.com
- MCP Apps 标准:https://docs.mcp-use.com/typescript/server/mcp-apps(双协议指南)
- 小部件指南:https://docs.mcp-use.com/typescript/server/ui-widgets
- Apps SDK 教程:https://docs.mcp-use.com/typescript/server/creating-apps-sdk-server
- 模板:https://docs.mcp-use.com/typescript/server/templates
- ChatGPT Apps 流程:https://docs.mcp-use.com/guides/chatgpt-apps-flow
- Inspector 调试:https://docs.mcp-use.com/inspector/debugging-chatgpt-apps
- GitHub:https://github.com/mcp-use/mcp-use
快速参考
命令:
npx create-mcp-use-app my-app --template mcp-apps- 初始化项目yarn dev- 开发并启用热重载yarn build- 构建生产版本yarn start- 运行生产服务器yarn deploy- 部署到 mcp-use Cloud
小部件结构:
resources/widget-name.tsx- 单文件小部件resources/widget-name/widget.tsx- 基于文件夹的小部件入口public/- 静态资源
小部件元数据:
description- 小部件描述props- 用于输入的 Zod 架构exposeAsTool- 自动注册为工具(默认:true)metadata- 统一配置(双协议,推荐)metadata.csp- 内容安全策略配置appsSdkMetadata- ChatGPT 专属覆盖项(可选)
CSP 字段:
connectDomains- 要调用的 APIresourceDomains- 要加载的静态资源frameDomains- 要嵌入的 iframescriptDirectives- 脚本策略
useWidget hook:
props- 小部件输入参数isPending- 加载状态标志state, setState- 持久状态callTool- 调用其他工具theme- 当前主题(浅色/深色)displayMode, requestDisplayMode- 显示控制