OpenAI Apps MCP について
Cloudflare Workers 上でステートレスな MCP サーバーを使い、ChatGPT Apps を構築する開発者向けスキルです。ツールやウィジェットの実装、デプロイ要件、よくある問題への対処を扱います。
主な機能
- 01ChatGPT Apps 向け MCP サーバー設定
- 02HTML・JavaScript ウィジェット
- 03Cloudflare Workers のデプロイ手順
- 04トラブルシューティングと互換性情報
活用シーン
- 01ChatGPT App のカスタムツール作成
- 02MCP リソースによるウィジェット提供
- 03転送方式やデプロイの問題回避
MCP 対応
JSON-RPC 2.0 を介して ChatGPT Apps にツールと HTML ウィジェットリソースを提供する、ステートレスな MCP サーバーの構築方法を説明します。
インストール
コマンドをコピーしてターミナルで実行します。
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.jsonassets/example-template.txtreferences/example-reference.mdopenai-metadata-format.mdscripts/example-script.shtemplates/basic/package.jsonvite.config.tswrangler.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 Apps を構築します。カスタムツールとインタラクティブなウィジェット(iframe 内にレンダリングされる HTML/JS UI)で ChatGPT を拡張します。
アーキテクチャ: ChatGPT → MCP エンドポイント(JSON-RPC 2.0)→ ツールハンドラー → ウィジェットリソース(HTML)
ステータス: Business/Enterprise/Edu でアプリを利用できます(2025 年 11 月 13 日に GA)。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 から配信します
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 Issue #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を使用します - または: Next.js/Remix/SvelteKit アプリとは別のポートで MCP サーバーを実行します
// ✅ 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,
});
Issue #10:Cloudflare Workers での Elicitation(ユーザー入力)の失敗
エラー:EvalError: Code generation from strings disallowed
ソース:GitHub Issue #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 を追跡してください。
Issue #11:SSE トランスポートのステートフルな性質がサーバーレスデプロイを妨げる
エラー:400: No transport found for sessionId
ソース:GitHub Issue #273
発生理由:SSEServerTransport はインメモリのセッションストレージに依存しています。サーバーレス環境(AWS Lambda、Cloudflare Workers)では、最初の GET /sse リクエストがインスタンス A で処理されても、後続の POST /messages リクエストが、インメモリ状態を持たないインスタンス B に届くことがあります。
予防策:サーバーレスデプロイでは、SSE ではなく Streamable HTTP トランスポート(v1.24.0 で追加)を使用してください。
解決策:ステートフルな SSE を使う場合は、サーバーレスではない環境(VPS、長時間稼働するコンテナー)にデプロイしてください。
公式ステータス:Streamable HTTP の導入(v1.24+)によって解決済みです。現在、サーバーレス向けの推奨標準です。
Issue #12:OAuth の設定には 2 つのアプリが必要
ソース:Cloudflare Remote MCP Server Docs 発生理由:OAuth プロバイダーはリダイレクト URL を厳密に検証します。localhost と本番環境では 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- 設定変更後にクライアントの再起動が必要
Issue #13:ウィジェットの状態が 4k トークンを超えるとパフォーマンスの問題が発生(コミュニティ情報)
ソース:OpenAI Apps SDK - ChatGPT UI 発生理由:ウィジェットの状態が保持されるのは、1 つの会話メッセージに紐づく単一のウィジェットインスタンスのみです。ユーザーがウィジェットのコントロールではなく、メインチャットの入力欄から送信すると、状態がリセットされます。 予防策:最適なパフォーマンスを得るため、状態のペイロードは 4k トークン未満にしてください。
// ✅ 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 サーバーからデータを取得する
- 重要なデータの永続化にはツール呼び出しを使用する
Issue #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() がサポートする画像形式は、image/png、image/jpeg、image/webp の 3 種類のみです。それ以外の形式では、通知なく失敗します。
// ✅ 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' });
その他のファイル形式を使う代替方法:
- ツールの引数で Base64 エンコーディングを使用する
- ユーザーにテキスト内容の貼り付けを依頼する
- 外部アップロードサービス(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) を削除 - 具体的なスキーマを使用してください
- ES2020 ターゲットが必須 (以前: ES2018)
setRequestHandlerが型安全になりました - 不正確なスキーマは型エラーをスローします
新機能:
- Tasks (v1.24.0以降): 進捗追跡を伴う長時間実行オペレーション
- Sampling with Tools (v1.24.0以降): ツールがモデルのサンプリングを要求可能
- OAuth Client Credentials (M2M): マシン間認証
移行: 緩い型インポートを使用している場合は、具体的なスキーマインポートに更新してください:
// ❌ 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"
}
}
公式ドキュメント
- MCP 仕様: https://modelcontextprotocol.io/ (最新: 2025-11-25)
- MCP SDK: https://github.com/modelcontextprotocol/typescript-sdk
- OpenAI Apps SDK: https://developers.openai.com/apps-sdk
- MCP Apps Extension (SEP-1865): http://blog.modelcontextprotocol.io/posts/2025-11-21-mcp-apps/
- Context7 ライブラリ ID: /modelcontextprotocol/typescript-sdk
本番環境リファレンス
オープンソース例: 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 One-Click Deploy: 事前構築済みテンプレートと自動構成された CI/CD を使用して、MCP サーバーを Cloudflare Workers にデプロイします。OAuth ラッパーと Python サポートを含みます。
- ドキュメント: https://developers.cloudflare.com/agents/guides/remote-mcp-server/
- ブログ: https://blog.cloudflare.com/model-context-protocol/
フレームワーク
Skybridge (コミュニティ): ウィジェット向けの HMR サポートと拡張された MCP サーバーヘルパーを備えた React 中心のフレームワーク。非公式ですが活発にメンテナンスされています。
- GitHub: https://github.com/alpic-ai/skybridge
- ドキュメント: https://www.skybridge.tech/
注意: コミュニティフレームワークは公式にはサポートされていません。自己責任でご使用ください