mcpField
C
chatgpt-app-builder

chatgpt-app-builder

github.com/mcp-use开发2277 次安装39 周前更新
代码仓库

关于 chatgpt-app-builder

面向开发者的技能,使用 mcp-use 和 OpenAI Apps SDK 构建带交互式 React 组件的 ChatGPT 应用。组件可自动注册,并支持 MCP Apps 与 ChatGPT 协议。

主要功能

  1. 01通过模板初始化应用
  2. 02自动注册 React 组件
  3. 03内置 useWidget 状态与工具调用
  4. 04自动打包并支持热重载
  5. 05配置 CSP 并用 Inspector 测试
  6. 06支持 MCP Apps 和 ChatGPT

适用场景

  1. 01创建带交互组件的 ChatGPT 应用
  2. 02构建可调用 MCP 工具的组件
  3. 03在 Inspector 中测试组件
  4. 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
  • 自行实现 useWidgetState Hook
  • 手动编写 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:如果只通过自定义工具使用小组件,则设为 false
  • metadata:兼容两种协议的统一配置(推荐)

内容安全策略(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 调用的 API
  • resourceDomains:用于加载图像、字体、样式表和视频
  • 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
  1. 启动开发服务器:

    yarn dev
    
  2. 打开 Inspector:

    • 前往 http://localhost:3000/inspector
  3. 测试 Widget:

    • 点击 Tools 标签页
    • 找到你的 Widget 工具
    • 输入测试参数
    • 执行以查看 Widget 渲染效果
  4. 调试交互:

    • 使用浏览器控制台
    • 检查 RPC 日志
    • 测试状态持久化
    • 验证工具调用
在 ChatGPT 中测试
  1. 启用开发者模式:

    • 设置 → 连接器 → 高级 → 开发者模式
  2. 添加你的服务:

    • 前往连接器标签页
    • 添加远程 MCP 服务 URL
  3. 在对话中测试:

    • 从 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 的基础 URL
  • MCP_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 错误

问题: 生产环境中的资源被内容安全策略拦截

解决方案:

  1. 检查浏览器控制台中的 CSP 违规消息
  2. 将缺少的域名添加到 CSP 配置中:
    metadata: {
      csp: {
        connectDomains: ['https://api.example.com'], // Add missing API domain
        resourceDomains: ['https://cdn.example.com'], // Add missing CDN domain
      }
    }
    
  3. 使用确切的域名——在生产环境中避免使用通配符
  4. 部署前在 Inspector 中测试,以便及早发现 CSP 问题
  5. 环境变量替代方案:设置 CSP_URLS 环境变量,并使用逗号分隔域名
协议兼容性问题

问题: 小部件在 ChatGPT 中可用,但在 MCP Apps 客户端中不可用(反之亦然)

解决方案:

  • 使用 type: "mcpApps" 以支持双协议(推荐)
  • 检查服务配置中的 baseUrl 是否设置正确
  • 确认元数据格式:双协议场景使用 metadata(驼峰命名),而不是 appsSdkMetadata(蛇形命名)
  • 在 Inspector 中测试,它同时支持两种协议

何时使用各类型:

  • type: "mcpApps" - 兼容性最高(推荐)
  • type: "appsSdk" - 仅适用于 ChatGPT(需要使用规范中未包含的 ChatGPT 专属功能时选用)

了解更多

快速参考

命令:

  • 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 - 要调用的 API
  • resourceDomains - 要加载的静态资源
  • frameDomains - 要嵌入的 iframe
  • scriptDirectives - 脚本策略

useWidget hook:

  • props - 小部件输入参数
  • isPending - 加载状态标志
  • state, setState - 持久状态
  • callTool - 调用其他工具
  • theme - 当前主题(浅色/深色)
  • displayMode, requestDisplayMode - 显示控制

评论

支持 Markdown