ChatGPTAppsSDKDevelopmentGuideSkill building-chatgpt-apps

本指南旨在帮助开发者创建具有交互式小部件的 ChatGPT 应用,这些小部件可以在 ChatGPT 对话中提供丰富的用户界面体验。涉及到的关键技术包括 MCP 服务器的搭建、嵌入式 HTML 小部件的开发、以及通过 `window.openai` API 实现的小部件与 ChatGPT 之间的通信。关键词包括:ChatGPT应用开发、MCP服务器、交互式小部件、API通信。

AI应用 0 次安装 4 次浏览 更新于 3/2/2026

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"]     │
└─────────────────────────────────────────────────────────────────┘

快速开始

  1. 创建 MCP 服务器 带有 FastMCP 和小部件资源
  2. 定义小部件 HTML 使用 window.openai.toolOutput
  3. 添加响应元数据 带有 _meta["openai.com/widget"]
  4. 通过 ngrok 暴露 以便 ChatGPT 访问
  5. 在 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>

关键小部件规则

  1. 始终检查 window.openai.toolOutput 然后再调用
  2. 使用内联样式 - 外部 CSS 可能无法可靠加载
  3. 保持小部件自包含 - 所有 HTML/CSS/JS 在一个文件中
  4. 使用实际的 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 中注册

  1. 访问 https://chatgpt.com/apps
  2. 点击设置(齿轮图标)
  3. 启用 开发者模式
  4. 点击 创建应用
  5. 填写:
    • 名称:您的应用名称
    • MCP 服务器 URLhttps://abc123.ngrok-free.app/mcp
    • 认证:无认证(开发中)
  6. 检查 “我理解并希望继续”
  7. 点击 创建

4. 测试应用

  1. 在 ChatGPT 中开始新对话
  2. 输入 @ 查看可用应用
  3. 选择您的应用
  4. 让它使用您的工具

常见问题和解决方案

小部件显示 “加载中…” 永远

原因:小部件 HTML 未正确交付。

解决方案

  1. 检查服务器日志中的 CallToolRequest 处理
  2. 验证响应中的 _meta["openai.com/widget"]
  3. 确保 MIME 类型是 text/html+skybridge

缓存的小部件不更新

原因:ChatGPT 积极缓存小部件。

解决方案

  1. 删除设置 > 应用中的应用
  2. 关闭服务器和 ngrok
  3. 开启新的 ngrok 隧道(新 URL)
  4. 使用新 URL 创建新应用
  5. 在新对话中测试

小部件 JavaScript 错误

原因window.openai 不可用。

解决方案:始终在调用前检查:

if (window.openai && window.openai.toolOutput) {
  window.openai.toolOutput({...});
}

工具在 @mentions 中不显示

原因:MCP 服务器未连接或工具未注册。

解决方案

  1. 检查服务器是否运行并且可以通过 ngrok URL 访问
  2. 验证 ngrok 隧道是否活跃:curl https://your-url.ngrok-free.app/mcp
  3. 检查服务器日志中的 ListToolsRequest

验证

运行:python3 scripts/verify.py

预期:✓ building-chatgpt-apps skill ready

如果验证失败

  1. 运行诊断:检查 references/ 文件夹是否存在
  2. 检查:所有参考文件是否在
  3. 停止并报告 如果仍然失败

参考资料