mcpField
O
OpenAI Apps MCP

OpenAI Apps MCP

github.com/jezweb开发1.1k302 次安装3 周前更新
代码仓库

关于 OpenAI Apps MCP

面向开发者的技能,介绍如何在 Cloudflare Workers 上用无状态 MCP 服务构建 ChatGPT Apps,涵盖工具、交互式组件、部署要求和常见问题处理。

主要功能

  1. 01ChatGPT Apps 的 MCP 服务设置
  2. 02交互式 HTML 和 JavaScript 组件
  3. 03Cloudflare Workers 部署指南
  4. 04故障排查与兼容性说明

适用场景

  1. 01为 ChatGPT App 构建自定义工具
  2. 02通过 MCP 资源提供交互式组件
  3. 03规避传输与部署问题

MCP 相关

指导构建无状态 MCP 服务,通过 JSON-RPC 2.0 向 ChatGPT Apps 提供工具和 HTML 组件资源。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill openai-apps-mcp

安装后在 Claude Code 中输入/openai-apps-mcp确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
example-reference.md
openai-metadata-format.md
scripts/
example-script.sh
templates/basic/
package.json
vite.config.ts
wrangler.jsonc

详细介绍

使用无状态 MCP 服务构建 OpenAI 应用

状态:已可用于生产环境 最近更新:2026-01-21 依赖项:cloudflare-worker-base、hono-routing(可选) 最新版本:@modelcontextprotocol/[email protected]、[email protected]、[email protected]、[email protected]


概述

使用 Cloudflare Workers 上的 MCP(Model Context Protocol) 服务构建 ChatGPT 应用。通过自定义工具和交互式小组件(在 iframe 中渲染的 HTML/JS UI)扩展 ChatGPT。

架构:ChatGPT → MCP 端点(JSON-RPC 2.0)→ 工具处理程序 → 小组件资源(HTML)

状态:面向 Business/Enterprise/Edu 用户的应用已上线(2025 年 11 月 13 日正式发布)。MCP Apps Extension(SEP-1865)于 2025 年 11 月 21 日正式确立。


快速开始

1. 创建项目脚手架并安装
npm create cloudflare@latest my-openai-app -- --type hello-world --ts --git --deploy false
cd my-openai-app
npm install @modelcontextprotocol/[email protected] [email protected] [email protected]
npm install -D @cloudflare/[email protected] [email protected]
2. 配置 wrangler.jsonc
{
  "name": "my-openai-app",
  "main": "dist/index.js",
  "compatibility_flags": ["nodejs_compat"],  // Required for MCP SDK
  "assets": {
    "directory": "dist/client",
    "binding": "ASSETS"  // Must match TypeScript
  }
}
3. 创建 MCP 服务(src/index.ts)
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';

const app = new Hono<{ Bindings: { ASSETS: Fetcher } }>();

// CRITICAL: Must allow chatgpt.com
app.use('/mcp/*', cors({ origin: 'https://chatgpt.com' }));

const mcpServer = new Server(
  { name: 'my-app', version: '1.0.0' },
  { capabilities: { tools: {}, resources: {} } }
);

// Tool registration
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'hello',
    description: 'Use this when user wants to see a greeting',
    inputSchema: {
      type: 'object',
      properties: { name: { type: 'string' } },
      required: ['name']
    },
    annotations: {
      openai: { outputTemplate: 'ui://widget/hello.html' }  // Widget URI
    }
  }]
}));

// Tool execution
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'hello') {
    const { name } = request.params.arguments as { name: string };
    return {
      content: [{ type: 'text', text: `Hello, ${name}!` }],
      _meta: { initialData: { name } }  // Passed to widget
    };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

app.post('/mcp', async (c) => {
  const body = await c.req.json();
  const response = await mcpServer.handleRequest(body);
  return c.json(response);
});

app.get('/widgets/*', async (c) => c.env.ASSETS.fetch(c.req.raw));

export default app;
4. 创建小组件(src/widgets/hello.html)
<!DOCTYPE html>
<html>
<head>
  <style>
    body { margin: 0; padding: 20px; font-family: system-ui; }
  </style>
</head>
<body>
  <div id="greeting">Loading...</div>
  <script>
    if (window.openai && window.openai.getInitialData) {
      const data = window.openai.getInitialData();
      document.getElementById('greeting').textContent = `Hello, ${data.name}! 👋`;
    }
  </script>
</body>
</html>
5. 部署
npm run build
npx wrangler deploy
npx @modelcontextprotocol/inspector https://my-app.workers.dev/mcp

关键要求

CORS:必须允许 /mcp/* 路由上的 https://chatgpt.com 小组件 URI:必须使用 ui://widget/ 前缀(例如,ui://widget/map.html) MIME 类型:HTML 资源必须使用 text/html+skybridge 小组件数据:通过 _meta.initialData 传递(通过 window.openai.getInitialData() 访问) 工具描述:以操作为导向(“当用户想要……时使用此工具”) ASSETS 绑定:从 ASSETS 提供小组件,而不是将其打包进 Worker 代码 SSE:每 30 秒发送一次心跳(Workers 的超时时间为 100 秒)


预防已知问题

此技能可预防 14 个已记录的问题:

问题 #1:CORS 策略阻止 MCP 端点

错误:Access to fetch blocked by CORS policy 修复:app.use('/mcp/*', cors({ origin: 'https://chatgpt.com' }))

问题 #2:小组件返回 404 Not Found

错误:小组件 URL 返回 404 (Not Found) 修复:使用 ui://widget/ 前缀(不要使用 resource:// 或 /widgets/)

annotations: { openai: { outputTemplate: 'ui://widget/map.html' } }
问题 #3:小组件显示为纯文本

错误:显示 HTML 源代码,而不是渲染后的小组件 修复:MIME 类型必须为 text/html+skybridge(不能是 text/html)

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [{ uri: 'ui://widget/map.html', mimeType: 'text/html+skybridge' }]
}));
问题 #4:ASSETS 绑定未定义

错误:TypeError: Cannot read property 'fetch' of undefined 修复:wrangler.jsonc 中的绑定名称必须与 TypeScript 中的一致

{ "assets": { "binding": "ASSETS" } }  // wrangler.jsonc
type Bindings = { ASSETS: Fetcher };  // index.ts
问题 #5:SSE 连接在 100 秒后断开

错误:SSE 流意外关闭 修复:每 30 秒发送一次心跳(Workers 在闲置 100 秒后超时)

const heartbeat = setInterval(async () => {
  await stream.writeSSE({ data: JSON.stringify({ type: 'heartbeat' }), event: 'ping' });
}, 30000);
问题 #6:ChatGPT 不会推荐工具

错误:工具已注册,但从未出现在推荐中 修复:使用以操作为导向的描述

// ✅ Good: 'Use this when user wants to see a location on a map'
// ❌ Bad: 'Shows a map'
问题 #7:小组件无法访问初始数据

错误:window.openai.getInitialData() 返回 undefined 修复:通过 _meta.initialData 传递数据

return {
  content: [{ type: 'text', text: 'Here is your map' }],
  _meta: { initialData: { location: 'SF', zoom: 12 } }
};
问题 #8:CSP 阻止小组件脚本

错误:Refused to load script (CSP directive) 修复:使用内联脚本或同源脚本。第三方 CDN 会被阻止。

 <script>console.log('ok');</script>
 <script src="https://cdn.example.com/lib.js"></script>
问题 #9:Hono 全局响应覆盖导致 Next.js 出错(v1.25.0-1.25.2)

错误:No response is returned from route handler(Next.js App Router) 来源:GitHub 问题 #1369 受影响版本:v1.25.0 至 v1.25.2 修复版本:v1.25.3 原因:Hono(MCP SDK 的依赖项)会覆盖 global.Response,导致扩展它的框架(Next.js、Remix、SvelteKit)出错。NextResponse 的 instanceof 检查会失败。 预防措施:

  • 升级到 v1.25.3+(推荐)
  • 修复前:改用 webStandardStreamableHTTPServerTransport
  • 或者:将 MCP 服务运行在与 Next.js/Remix/SvelteKit 应用不同的端口上
// ✅ v1.25.3+ - Fixed
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined,
});

// ✅ v1.25.0-1.25.2 - Workaround
import { webStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/index.js';
const transport = webStandardStreamableHTTPServerTransport({
  sessionIdGenerator: undefined,
});
问题 #10:Cloudflare Workers 上的 Elicitation(用户输入)失败

错误:EvalError: Code generation from strings disallowed 来源:GitHub 问题 #689 原因:内部 AJV v6 验证器在边缘平台上使用了被禁止的 API 预防方法:避免在边缘平台(Cloudflare Workers、Vercel Edge、Deno Deploy)上调用 elicitInput()

解决方法:

// ❌ Don't use on Cloudflare Workers
const userInput = await server.elicitInput({
  prompt: "What is your name?",
  schema: { type: "string" }
});

// ✅ Use tool parameters instead
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name } = request.params.arguments as { name: string };
  // User provides via tool call, not elicitation
});

状态:需要 MCP SDK v2 才能妥善修复。请关注 PR #844。

问题 #11:SSE 传输的有状态特性会导致无服务器部署失败

错误:400: No transport found for sessionId 来源:GitHub 问题 #273 原因:SSEServerTransport 依赖内存中的会话存储。在无服务器环境(AWS Lambda、Cloudflare Workers)中,初始的 GET /sse 请求可能由实例 A 处理,而后续的 POST /messages 请求则落到缺少内存状态的实例 B 上。 预防方法:在无服务器部署中使用流式 HTTP 传输(在 v1.24.0 中新增),而非 SSE 解决方案:对于有状态 SSE,请部署到非无服务器环境(VPS、长时间运行的容器)

官方状态:通过引入流式 HTTP(v1.24+)解决——现在这是无服务器环境的推荐标准。

问题 #12:OAuth 配置需要两个独立的应用

来源:Cloudflare 远程 MCP 服务文档 原因:OAuth 提供商会严格验证重定向 URL。本地主机和生产环境使用不同的 URL,因此需要分别注册 OAuth 客户端。 预防方法:

# Development OAuth App
Callback URL: http://localhost:8788/callback

# Production OAuth App
Callback URL: https://my-mcp-server.workers.dev/callback

其他要求:

  • 用于存储身份验证状态的 KV 命名空间(需手动创建)
  • COOKIE_ENCRYPTION_KEY 环境变量:openssl rand -hex 32
  • 配置更改后必须重启客户端
问题 #13:小组件状态超过 4k 个 token 会导致性能问题(社区来源)

来源:OpenAI Apps SDK - ChatGPT UI 原因:小组件状态仅会持久保存在与一条对话消息关联的单个小组件实例中。用户通过主聊天输入框而非小组件控件提交内容时,状态会被重置。 预防方法:将状态负载保持在 4k 个 token 以下,以获得最佳性能

// ✅ Good - Lightweight state
window.openai.setWidgetState({ selectedId: "item-123", view: "grid" });

// ❌ Bad - Will cause performance issues
window.openai.setWidgetState({
  items: largeArray,           // Don't store full datasets
  history: conversationLog,    // Don't store conversation history
  cache: expensiveComputation  // Don't cache large results
});

最佳实践:

  • 仅存储 UI 状态(所选项目、视图模式、筛选条件)
  • 在小组件挂载时从 MCP 服务获取数据
  • 使用工具调用持久化重要数据
问题 #14:小组件发起的工具调用若未设置权限标记便会失败(社区来源)

来源:OpenAI Apps SDK - ChatGPT UI 原因:组件通过 window.openai.callTool() 发起工具调用时,必须在 MCP 服务中将相应工具标记为“可由组件发起”。如果未设置此标记,调用会静默失败。 预防方法:在注解中将工具标记为 widgetCallable: true

// MCP Server - Mark tool as widget-callable
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'update_item',
    description: 'Update an item',
    inputSchema: { /* ... */ },
    annotations: {
      openai: {
        outputTemplate: 'ui://widget/item.html',
        // ✅ Required for widget-initiated calls
        widgetCallable: true
      }
    }
  }]
}));

// Widget - Now allowed to call tool
window.openai.callTool({
  name: 'update_item',
  arguments: { id: itemId, status: 'completed' }
});

小组件开发最佳实践

文件上传限制(社区来源)

来源:OpenAI Apps SDK - ChatGPT UI

window.openai.uploadFile() 仅支持 3 种图像格式:image/png、image/jpeg 和 image/webp。其他格式会静默失败。

// ✅ Supported
window.openai.uploadFile({ accept: 'image/png,image/jpeg,image/webp' });

// ❌ Not supported (fails silently)
window.openai.uploadFile({ accept: 'application/pdf' });
window.openai.uploadFile({ accept: 'text/csv' });

其他文件类型的替代方案:

  1. 在工具参数中使用 base64 编码
  2. 请用户粘贴文本内容
  3. 使用外部上传服务(S3、R2)并传入 URL
工具性能目标(来自社区)

来源:OpenAI Apps SDK - 故障排除

在 ChatGPT 中,工具调用耗时超过“几百毫秒”会导致 UI 响应迟缓。官方文档建议分析后端性能,并为耗时较长的操作实现缓存。

性能目标:

  • < 200ms:理想响应时间
  • 200-500ms:可以接受,但能察觉到延迟
  • > 500ms:响应迟缓,需要优化

优化策略:

// 1. Cache expensive computations
const cache = new Map();
if (cache.has(key)) return cache.get(key);
const result = await expensiveOperation();
cache.set(key, result);

// 2. Use KV/D1 for pre-computed data
const cached = await env.KV.get(`result:${id}`);
if (cached) return JSON.parse(cached);

// 3. Paginate large datasets
return {
  content: [{ type: 'text', text: 'First 20 results...' }],
  _meta: { hasMore: true, nextPage: 2 }
};

// 4. Move slow work to async tasks
// Return immediately, update via follow-up

MCP SDK 1.25.x 更新(2025 年 12 月)

从 @modelcontextprotocol/[email protected] 升级到 1.25.x 的破坏性变更:

  • 移除了宽泛类型导出(Prompts、Resources、Roots、Sampling、Tools)——请改用特定的 schema
  • 必须将目标设为 ES2020(此前为 ES2018)
  • setRequestHandler 现已支持类型安全——错误的 schema 会触发类型错误

新功能:

  • Tasks(v1.24.0+):支持进度跟踪的长时间运行操作
  • Sampling with Tools(v1.24.0+):工具可以请求模型采样
  • OAuth Client Credentials(M2M):机器间身份验证

迁移:如果使用宽泛类型导入,请改为导入特定的 schema:

// ❌ Old (removed in 1.25.0)
import { Tools } from '@modelcontextprotocol/sdk/types.js';

// ✅ New (1.25.1+)
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';

Zod 4.0 迁移说明(重大更新——2025 年 7 月)

从 [email protected] 升级到 4.x 的破坏性变更:

  • .default() 现在要求传入输入类型(而非输出类型)。如需沿用旧行为,请使用 .prefault()。
  • ZodError:使用 error.issues(而非 error.errors)
  • .merge() 和 .superRefine() 已弃用
  • 带默认值的可选属性现在始终会应用默认值

性能:字符串解析速度提升 14 倍,数组提升 7 倍,对象提升 6.5 倍

迁移:更新验证代码:

// Zod 4.x
try {
  const validated = schema.parse(data);
} catch (error) {
  if (error instanceof z.ZodError) {
    return { content: [{ type: 'text', text: error.issues.map(e => e.message).join(', ') }] };
  }
}

依赖项

{
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.25.3",
    "hono": "^4.11.3",
    "zod": "^4.3.5"
  },
  "devDependencies": {
    "@cloudflare/vite-plugin": "^1.17.1",
    "@cloudflare/workers-types": "^4.20260103.0",
    "vite": "^7.2.4",
    "wrangler": "^4.54.0"
  }
}

官方文档

生产环境参考

开源示例:https://github.com/jezweb/chatgpt-app-sdk(作品集轮播组件)

  • 已在生产环境运行:在 ChatGPT Business 中渲染
  • MCP 服务:完整的 JSON-RPC 2.0 实现,包含工具和资源(约 310 行)
  • 组件集成:WordPress API → window.openai.toolOutput → React 轮播组件
  • 数据库:使用 D1(SQLite)存储联系表单提交内容
  • 技术栈:Hono 4 + React 19 + Tailwind v4 + Drizzle ORM
  • 关键文件:
    • /src/lib/mcp/server.ts - 完整的 MCP 处理程序
    • /src/server/tools/portfolio.ts - 带有组件注解的工具
    • /src/widgets/PortfolioWidget.tsx - 数据访问模式
  • 已验证:已避免所有 14 个已知问题,生产环境中零错误

社区资源

部署工具

Cloudflare 一键部署:使用预构建模板和自动配置的 CI/CD,将 MCP 服务部署到 Cloudflare Workers。包含 OAuth 包装器和 Python 支持。

框架

Skybridge(社区):专注于 React 的框架,支持组件 HMR,并提供增强的 MCP 服务辅助工具。非官方,但维护活跃。

注意:社区框架不受官方支持。请自行决定是否使用

评论

支持 Markdown