ChatGPT 应用 SDK 开发指南
概览
创建带有交互式小部件的 ChatGPT 应用,这些小部件在 ChatGPT 对话中呈现丰富的 UI。应用结合了 MCP 服务器(提供工具)和嵌入式 HTML 小部件,它们通过 window.openai API 进行通信。
window.openai API 参考
小部件通过这些 API 与 ChatGPT 通信:
sendFollowUpMessage(推荐用于动作)
代表用户向 ChatGPT 发送后续提示:
// 触发后续对话
if (window.openai?.sendFollowUpMessage) {
await window.openai.sendFollowUpMessage({
prompt: '为我总结这一章节'
});
}
用途:动作按钮建议后续步骤(总结、解释等)
toolOutput
从小部件交互中发送结构化数据回来:
// 将数据发送回 ChatGPT
if (window.openai?.toolOutput) {
window.openai.toolOutput({
action: 'chapter_selected',
chapter: 1,
title: '引言'
});
}
用途:选择、表单提交、用户选择,这些输入工具响应。
callTool
从一个小部件中调用另一个 MCP 工具:
// 直接调用工具
if (window.openai?.callTool) {
await window.openai.callTool({
name: 'read-chapter',
arguments: { chapter: 2 }
});
}
用途:内容导航,链式工具调用。
重要:按钮交互限制
重要发现:小部件按钮可能呈现为静态 UI 元素而不是交互式 JavaScript 按钮。ChatGPT 在沙盒化的 iframe 中渲染小部件,其中一些点击处理器可能不可靠。
可行的
sendFollowUpMessage- 可靠地触发后续提示- 简单的 onclick 处理器用于
toolOutput调用 - CSS 悬停效果和视觉反馈
可能不可行的
- 复杂的交互式 JavaScript(选择 API 等)
- 从按钮中进行多个链式工具调用
- 文本选择功能中的
window.getSelection()
推荐模式:建议按钮
而不是复杂的交互,使用简单的按钮来建议提示:
<div class="action-buttons">
<button class="btn btn-primary" id="summarizeBtn">
📝 为我总结这一章节
</button>
<button class="btn btn-primary" id="explainBtn">
💡 解释关键概念
</button>
</div>
<script>
document.getElementById('summarizeBtn')?.addEventListener('click', async () => {
if (window.openai?.sendFollowUpMessage) {
await window.openai.sendFollowUpMessage({
prompt: '为我总结这一章节'
});
}
});
document.getElementById('explainBtn')?.addEventListener('click', async () => {
if (window.openai?.sendFollowUpMessage) {
await window.openai.sendFollowUpMessage({
prompt: '解释这一章节的关键概念'
});
}
});
</script>
架构概要
┌─────────────────────────────────────────────────────────────────┐
│ ChatGPT UI │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ 小部件 (iframe) ││
│ │ HTML + CSS + JS ││
│ │ 调用:window.openai.toolOutput({action: "...", ...}) ││
│ └─────────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ChatGPT 后端 │
│ │ │
│ ▼ │
│ MCP 服务器 (FastMCP + HTTP) │
│ - 工具:open-book, read-chapter, 等。 │
│ - 资源:小部件 HTML (text/html+skybridge) │
│ - 响应包括:_meta["openai.com/widget"] │
└─────────────────────────────────────────────────────────────────┘
快速开始
- 创建 MCP 服务器 带有 FastMCP 和小部件资源
- 定义小部件 HTML 使用
window.openai.toolOutput - 添加响应元数据 带有
_meta["openai.com/widget"] - 通过 ngrok 暴露 以便 ChatGPT 访问
- 在 ChatGPT 中注册 开发者模式设置
小部件 HTML 要求
基本小部件模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的小部件</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 24px;
color: white;
}
.container { max-width: 600px; margin: 0 auto; }
.card {
background: rgba(255,255,255,0.95);
color: #333;
padding: 24px;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.btn {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
}
.btn:hover { background: #5a6fd6; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>小部件标题</h1>
<p>小部件内容在这里</p>
<button class="btn" onclick="handleAction()">点击我</button>
</div>
</div>
<script>
function handleAction() {
// 与 ChatGPT 通信
if (window.openai && window.openai.toolOutput) {
window.openai.toolOutput({
action: "button_clicked",
data: { timestamp: Date.now() }
});
}
}
</script>
</body>
</html>
关键小部件规则
- 始终检查
window.openai.toolOutput然后再调用 - 使用内联样式 - 外部 CSS 可能无法可靠加载
- 保持小部件自包含 - 所有 HTML/CSS/JS 在一个文件中
- 使用实际的 ChatGPT 进行测试 - 浏览器预览不会有
window.openai
MCP 服务器设置(FastMCP Python)
项目结构
my_chatgpt_app/
├── main.py # FastMCP 服务器带有小部件
├── requirements.txt # 依赖项
└── .env # 环境变量
requirements.txt
mcp[cli]>=1.9.2
uvicorn>=0.32.0
httpx>=0.28.0
python-dotenv>=1.0.0
main.py 模板
import mcp.types as types
from mcp.server.fastmcp import FastMCP
# 小部件 MIME 类型供 ChatGPT 使用
MIME_TYPE = "text/html+skybridge"
# 定义你的小部件 HTML
MY_WIDGET = '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body { font-family: sans-serif; padding: 20px; }
.container { max-width: 500px; margin: 0 auto; }
</style>
</head>
<body>
<div class="container">
<h1>来自小部件的问候!</h1>
<p>此内容在 ChatGPT 中呈现。</p>
</div>
</body>
</html>'''
# 小部件注册表
WIDGETS = {
"main-widget": {
"uri": "ui://widget/main.html",
"html": MY_WIDGET,
"title": "我的小部件",
},
}
# 创建 FastMCP 服务器
mcp = FastMCP("My ChatGPT App")
@mcp.resource(
uri="ui://widget/{widget_name}.html",
name="Widget Resource",
mime_type=MIME_TYPE
)
def widget_resource(widget_name: str) -> str:
"""提供小部件 HTML."""
widget_key = f"{widget_name}"
if widget_key in WIDGETS:
return WIDGETS[widget_key]["html"]
return WIDGETS["main-widget"]["html"]
def _embedded_widget_resource(widget_id: str) -> types.EmbeddedResource:
"""为工具响应创建嵌入式小部件资源。"""
widget = WIDGETS[widget_id]
return types.EmbeddedResource(
type="resource",
resource=types.TextResourceContents(
uri=widget["uri"],
mimeType=MIME_TYPE,
text=widget["html"],
title=widget["title"],
),
)
def listing_meta() -> dict:
"""工具元数据供 ChatGPT 工具列表。"""
return {
"openai.com/widget": {
"uri": WIDGETS["main-widget"]["uri"],
"title": WIDGETS["main-widget"]["title"]
}
}
def response_meta() -> dict:
"""带有嵌入式小部件的响应元数据。"""
return {
"openai.com/widget": _embedded_widget_resource("main-widget")
}
@mcp.tool(
annotations={
"title": "我的工具",
"readOnlyHint": True,
"openWorldHint": False,
},
_meta=listing_meta(),
)
def my_tool() -> types.CallToolResult:
"""描述这个工具的作用。"""
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text="工具执行成功!"
)
],
structuredContent={
"status": "success",
"message": "小部件的数据"
},
_meta=response_meta(),
)
if __name__ == "__main__":
import uvicorn
print("Starting MCP Server on http://localhost:8001")
print("Connect via: https://your-tunnel.ngrok-free.app/mcp")
uvicorn.run(
"main:mcp.app",
host="0.0.0.0",
port=8001,
reload=True
)
响应元数据格式
关键:_meta["openai.com/widget"]
工具响应必须包含小部件元数据:
types.CallToolResult(
content=[types.TextContent(type="text", text="...")],
structuredContent={"key": "value"}, # 小部件的数据
_meta={
"openai.com/widget": types.EmbeddedResource(
type="resource",
resource=types.TextResourceContents(
uri="ui://widget/my-widget.html",
mimeType="text/html+skybridge",
text=WIDGET_HTML,
title="我的小部件",
),
)
},
)
structuredContent
传递给小部件的数据。小部件可以通过 window.openai API 访问此数据。
开发设置
1. 启动本地服务器
cd my_chatgpt_app
python main.py
# 服务器在 http://localhost:8001 上运行
2. 启动 ngrok 隧道
ngrok http 8001
# 获取 URL 如:https://abc123.ngrok-free.app
3. 在 ChatGPT 中注册
- 访问 https://chatgpt.com/apps
- 点击设置(齿轮图标)
- 启用 开发者模式
- 点击 创建应用
- 填写:
- 名称:您的应用名称
- MCP 服务器 URL:
https://abc123.ngrok-free.app/mcp - 认证:无认证(开发中)
- 检查 “我理解并希望继续”
- 点击 创建
4. 测试应用
- 在 ChatGPT 中开始新对话
- 输入
@查看可用应用 - 选择您的应用
- 让它使用您的工具
常见问题和解决方案
小部件显示 “加载中…” 永远
原因:小部件 HTML 未正确交付。
解决方案:
- 检查服务器日志中的
CallToolRequest处理 - 验证响应中的
_meta["openai.com/widget"] - 确保 MIME 类型是
text/html+skybridge
缓存的小部件不更新
原因:ChatGPT 积极缓存小部件。
解决方案:
- 删除设置 > 应用中的应用
- 关闭服务器和 ngrok
- 开启新的 ngrok 隧道(新 URL)
- 使用新 URL 创建新应用
- 在新对话中测试
小部件 JavaScript 错误
原因:window.openai 不可用。
解决方案:始终在调用前检查:
if (window.openai && window.openai.toolOutput) {
window.openai.toolOutput({...});
}
工具在 @mentions 中不显示
原因:MCP 服务器未连接或工具未注册。
解决方案:
- 检查服务器是否运行并且可以通过 ngrok URL 访问
- 验证 ngrok 隧道是否活跃:
curl https://your-url.ngrok-free.app/mcp - 检查服务器日志中的
ListToolsRequest
验证
运行:python3 scripts/verify.py
预期:✓ building-chatgpt-apps skill ready
如果验证失败
- 运行诊断:检查 references/ 文件夹是否存在
- 检查:所有参考文件是否在
- 停止并报告 如果仍然失败