OpenAI Apps MCP について
Cloudflare Workers 上のステートレス MCP サーバーで ChatGPT を拡張する OpenAI Apps の開発スキルです。サーバー、ツール、ウィジェットの実装パターンとデプロイ時の注意点を扱います。
主な機能
- 01ステートレス MCP サーバーのパターン
- 02Cloudflare Workers の設定
- 03Zod によるツール入力検証
- 04ChatGPT 向けウィジェットリソース
- 05デプロイとテストのガイド
活用シーン
- 01ChatGPT と連携するアプリの構築
- 02ChatGPT 内でのインタラクティブなウィジェット表示
- 03デプロイ前の MCP エンドポイント検証
MCP 対応
ChatGPT にツールとウィジェットリソースを公開するステートレス MCP サーバーの実装パターンを提供し、設定とテストも案内します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ovachiever/ droid-tings --skill openai-apps-mcpインストール後、Claude Code で/openai-apps-mcpを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonassets/example-template.txtreferences/openai-metadata-format.mdexample-reference.mdscripts/example-script.shtemplates/basic/wrangler.jsoncvite.config.tspackage.json詳細
ステートレス MCP サーバーで OpenAI Apps を構築する
ステータス:本番環境で利用可能
最終更新日:2025-11-17
依存関係:cloudflare-worker-base、hono-routing(任意。ルーティングパターンに役立ちます)
最新バージョン:@modelcontextprotocol/[email protected]、[email protected]、[email protected]
概要
このスキルでは、Model Context Protocol(MCP)を通じて ChatGPT の機能を拡張するアプリケーションである OpenAI Apps を構築するための、本番環境で検証済みのパターンを紹介します。Cloudflare Workers を使用する ステートレス MCP サーバー に焦点を当てます。これにより、OpenAI Apps のユースケースの 80% をカバーできます。
OpenAI Apps とは?
OpenAI Apps は ChatGPT のインターフェースに統合される拡張機能で、ユーザーは次のことができます。
- 会話の中からサードパーティサービスに直接アクセスする
- インタラクティブなウィジェット(地図、カルーセル、リストなど)を表示する
- 構造化された UI コンポーネントを返すツールを実行する
- ドメイン固有の機能で ChatGPT を強化する
アーキテクチャ
ChatGPT User
↓
ChatGPT (discovers and invokes tools)
↓
MCP Server (your Cloudflare Worker)
├── Tool handlers (business logic)
├── Widget resources (HTML/JS UI)
└── OpenAI metadata (output templates)
主なコンポーネント
- MCP サーバー - Model Context Protocol を通じてツールを公開する HTTP エンドポイント
- ツールハンドラー - 入力を処理して結果を返す関数
- ウィジェットリソース - ChatGPT の iframe 内で表示される HTML ページ
- OpenAI メタデータ - ウィジェットのルーティングと表示のための特別な注釈
クイックスタート(10 分)
1. プロジェクトのひな形を作成する
npm create cloudflare@latest my-openai-app -- --type hello-world --ts --git --deploy false
cd my-openai-app
# Install dependencies
npm install @modelcontextprotocol/[email protected] [email protected] [email protected]
npm install -D @cloudflare/[email protected] [email protected]
重要な理由:
@modelcontextprotocol/sdkは公式の MCP プロトコル実装ですhonoは API エンドポイントに最適な軽量ルーティングを提供します- Vite と CloudFlare プラグインにより、ウィジェットをビルドして配信できます
2. wrangler.jsonc を設定する
{
"name": "my-openai-app",
"main": "dist/index.js",
"compatibility_date": "2025-10-08",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "dist/client",
"binding": "ASSETS"
},
"observability": {
"enabled": true
}
}
重要:
- MCP SDK には
nodejs_compatフラグが必要です assets.binding: "ASSETS"は TypeScript のバインディング名と一致している必要がありますassets.directoryは Vite のビルド出力先と一致している必要があります
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';
type Bindings = {
ASSETS: Fetcher;
};
const app = new Hono<{ Bindings: Bindings }>();
// CORS - must allow ChatGPT
app.use('/mcp/*', cors({
origin: 'https://chatgpt.com',
credentials: true,
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization']
}));
// Create MCP server
const mcpServer = new Server(
{ name: 'my-openai-app', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {} } }
);
// Register a simple tool
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'hello_world',
description: 'Use this when the user wants to see a hello world message',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name to greet' }
},
required: ['name']
},
annotations: {
openai: {
outputTemplate: 'ui://widget/hello.html'
}
}
}]
}));
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'hello_world') {
const { name } = request.params.arguments as { name: string };
return {
content: [{ type: 'text', text: `Hello, ${name}!` }],
_meta: { initialData: { name } }
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
// MCP endpoint
app.post('/mcp', async (c) => {
const body = await c.req.json();
const response = await mcpServer.handleRequest(body);
return c.json(response);
});
// Serve widgets
app.get('/widgets/*', async (c) => c.env.ASSETS.fetch(c.req.raw));
export default app;
5 ステップのセットアップ手順
ステップ 1:プロジェクトのひな形を作成する
Cloudflare の公式ひな形作成ツールを使用します。
npm create cloudflare@latest my-openai-app -- --type hello-world --ts --git --deploy false
主なポイント:
- TypeScript を使用する Workers プロジェクトを作成します
- wrangler.jsonc が含まれます
- git リポジトリを初期化します
ステップ 2:依存関係をインストールする
npm install @modelcontextprotocol/[email protected] [email protected] [email protected]
npm install -D @cloudflare/[email protected] [email protected]
各パッケージの役割:
@modelcontextprotocol/sdk- 公式の MCP プロトコル(Anthropic)hono- 高速で軽量なルーティングフレームワークzod- ツール入力の実行時型検証@cloudflare/vite-plugin- Workers と静的アセット用のビルドツールvite- フロントエンドのビルドツール
ステップ 3:ビルドシステムを設定する
vite.config.ts を作成します。
import { defineConfig } from 'vite';
import { cloudflareDevProxyVitePlugin as cloudflare } from '@cloudflare/vite-plugin';
export default defineConfig({
plugins: [
cloudflare({
configPath: 'wrangler.jsonc',
persist: { path: '.wrangler/state' }
})
],
build: {
outDir: 'dist',
rollupOptions: {
input: {
worker: './src/index.ts'
},
output: {
entryFileNames: (chunkInfo) => {
if (chunkInfo.name === 'worker') return 'index.js';
return 'client/[name]-[hash].js';
}
}
}
}
});
重要な理由:
- Worker コードと静的アセットの両方をビルドします
- Workers と ASSETS バインディングに適した出力構成になります
- キャッシュ無効化のためにコンテンツをハッシュ化します
ステップ 4:ウィジェットの HTML を作成する
mkdir -p src/widgets
src/widgets/hello.html を作成します。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Widget</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--background);
color: var(--foreground);
}
.greeting {
font-size: 24px;
font-weight: 600;
}
</style>
</head>
<body>
<div class="greeting" id="greeting">Loading...</div>
<script>
// Access initial data from tool handler
if (window.openai && window.openai.getInitialData) {
const data = window.openai.getInitialData();
document.getElementById('greeting').textContent = `Hello, ${data.name}! 👋`;
}
</script>
</body>
</html>
避けること:
- サードパーティの CDN スクリプトを使用しないでください(CSP によりブロックされる場合があります)
- カスタムフォントを使用しないでください(システムフォントを使用してください)
- CORS なしで外部 API を呼び出さないでください
ステップ 5:デプロイしてテストする
# Build
npm run build
# Deploy to Cloudflare
npx wrangler deploy
# Test with MCP Inspector
npx @modelcontextprotocol/inspector https://my-openai-app.workers.dev/mcp
重要なルール
必ず行うこと
✅ CORS を設定し、https://chatgpt.com を許可する
✅ ウィジェットにはリソース URI パターン ui://widget/ を使用する
✅ HTML リソースの MIME タイプを text/html+skybridge に設定する
✅ ウィジェットの初期化に備えて、ツールの応答に _meta.initialData を含める
✅ 行動を促すツールの説明(「次の場合に使用します…」)を記述する
✅ Zod スキーマでツール入力を検証する
✅ ChatGPT にデプロイする前に MCP Inspector でテストする
絶対に行わないこと
❌ カスタム MIME タイプを使用する(text/html+skybridge が必須です)
❌ CORS の設定を忘れる(ChatGPT が接続できません)
❌ ui://widget/ プレフィックスのないリソース URI を使用する
❌ ウィジェットを Worker コードにバンドルする(ASSETS バインディングを使用してください)
❌ 入力検証を省略する(ツールは信頼できないデータを受け取ります)
❌ ハートビートなしで SSE を使用する(Workers では 100 秒でタイムアウトします)
❌ 事前に MCP Inspector でテストせずにデプロイする
既知の問題の防止
このスキルでは、文書化されている 8 件の問題を防止します。
Issue #1: CORS ポリシーにより MCP エンドポイントがブロックされる
エラー: Access to fetch at 'https://my-app.workers.dev/mcp' from origin 'https://chatgpt.com' has been blocked by CORS policy
発生元: CORS が設定されていない場合によく表示されるブラウザーコンソールエラーです。
原因: ChatGPT は MCP サーバーにクロスオリジンリクエストを送信します。CORS ヘッダーがないと、ブラウザーはこれらのリクエストをブロックします。
予防策:
app.use('/mcp/*', cors({
origin: 'https://chatgpt.com', // Must allow ChatGPT specifically
credentials: true,
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization']
}));
Issue #2: ウィジェットが 404 Not Found を返す
エラー: ウィジェットの URL に対して Failed to load resource: the server responded with a status of 404 (Not Found)
発生元: ui://widget/ プレフィックスに関する OpenAI Apps SDK の要件です。
原因: リソース URI は特定のパターンに従う必要があります。プレフィックスが正しくないと、ChatGPT はウィジェットを解決できません。
予防策:
// ✅ Correct
annotations: {
openai: {
outputTemplate: 'ui://widget/map.html'
}
}
// ❌ Wrong - will 404
outputTemplate: 'resource://map.html'
outputTemplate: '/widgets/map.html'
Issue #3: ウィジェットがプレーンテキストとして表示される
エラー: レンダリングされたウィジェットではなく、HTML ソースコードが表示される
発生元: MIME タイプに関する OpenAI Apps SDK の要件です。
原因: ChatGPT でウィジェットをレンダリングするには、特定の MIME タイプ text/html+skybridge が必要です。標準の text/html では動作しません。
予防策:
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [{
uri: 'ui://widget/map.html',
mimeType: 'text/html+skybridge' // Must use +skybridge
}]
}));
Issue #4: ASSETS バインディングが未定義
エラー: c.env.ASSETS へのアクセス時に TypeError: Cannot read property 'fetch' of undefined が発生する
発生元: wrangler.jsonc または TypeScript の型定義の設定ミスです。
原因: wrangler.jsonc の ASSETS バインディング名は、TypeScript のバインディングインターフェースと一致している必要があります。
予防策:
// wrangler.jsonc
{
"assets": {
"directory": "dist/client",
"binding": "ASSETS" // Must match TypeScript
}
}
// src/index.ts
type Bindings = {
ASSETS: Fetcher; // Must match wrangler.jsonc binding name
};
Issue #5: SSE 接続が 100 秒後に切断される
エラー: SSE ストリームが予期せず閉じ、ツールが応答しなくなる
発生元: データが送信されない場合に発生する Cloudflare Workers の SSE タイムアウトです。
原因: Workers は、100 秒間アクティビティがないと SSE 接続を閉じます。ハートビートイベントを送信する必要があります。
予防策:
import { streamSSE } from 'hono/streaming';
app.get('/mcp/stream', (c) => {
return streamSSE(c, async (stream) => {
// Send heartbeat every 30 seconds
const heartbeat = setInterval(async () => {
await stream.writeSSE({
data: JSON.stringify({ type: 'heartbeat' }),
event: 'ping'
});
}, 30000);
// Clean up on close
stream.onAbort(() => clearInterval(heartbeat));
});
});
Issue #6: ChatGPT がツールを提案しない
エラー: ツールは登録されているが、ChatGPT の候補に一度も表示されない
発生元: OpenAI によるツールの検出には、アクション指向の説明が必要です。
原因: 一般的な説明では、ユーザーの意図のパターンに一致しません。ChatGPT は説明をもとに、ツールを呼び出すタイミングを判断します。
予防策:
// ✅ Good - action-oriented
description: 'Use this when the user wants to see a location on a map'
// ❌ Bad - generic
description: 'Shows a map'
// ✅ Good - specific use case
description: 'Use this when the user asks about weather in a specific city'
// ❌ Bad - vague
description: 'Gets weather data'
Issue #7: ウィジェットが初期データにアクセスできない
エラー: ウィジェット内で window.openai.getInitialData() が undefined を返す
発生元: ツールの応答に _meta.initialData がありません。
原因: データは _meta フィールドを介して明示的に渡す必要があります。通常の応答コンテンツはウィジェットに渡されません。
予防策:
// Tool handler
return {
content: [{
type: 'text',
text: 'Here is your map'
}],
_meta: {
initialData: { // This object reaches window.openai.getInitialData()
location: 'San Francisco',
zoom: 12
}
}
};
Issue #8: ウィジェットのスクリプトが CSP によりブロックされる
エラー: Refused to load the script because it violates the following Content Security Policy directive
発生元: ウィジェットの iframe に適用される ChatGPT の Content Security Policy です。
原因: セキュリティ上の理由により、サードパーティ CDN の外部スクリプトはブロックされます。
予防策:
<script>
console.log('Widget loaded');
</script>
<script src="/widgets/lib.js"></script>
<script src="https://cdn.example.com/library.js"></script>
設定ファイルのリファレンス
wrangler.jsonc(完全な例)
{
"name": "my-openai-app",
"main": "dist/index.js",
"compatibility_date": "2025-10-08",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "dist/client",
"binding": "ASSETS"
},
"observability": {
"enabled": true
},
"vars": {
"ENVIRONMENT": "production"
}
}
これらの設定の理由:
nodejs_compat- MCP SDK に必要です(Node.js API を使用します)assets.directory- Vite がウィジェットの HTML ファイルを出力する場所ですassets.binding: "ASSETS"- コードから静的アセットにアクセスする方法ですobservability- デバッグ用のログとトレースを有効にします
vite.config.ts(完全な例)
import { defineConfig } from 'vite';
import { cloudflareDevProxyVitePlugin as cloudflare } from '@cloudflare/vite-plugin';
export default defineConfig({
plugins: [
cloudflare({
configPath: 'wrangler.jsonc',
persist: { path: '.wrangler/state' }
})
],
build: {
outDir: 'dist',
rollupOptions: {
input: {
worker: './src/index.ts',
// Widget entry points
hello: './src/widgets/hello.html',
map: './src/widgets/map.html'
},
output: {
entryFileNames: (chunkInfo) => {
if (chunkInfo.name === 'worker') return 'index.js';
return 'client/[name]-[hash].js';
},
assetFileNames: 'client/[name]-[hash][extname]'
}
}
}
});
これらの設定の理由:
- 複数の入力エントリーポイント(ワーカーとウィジェット)
- ワーカー(
index.js)とクライアントアセット(client/)で出力先を分離 - キャッシュ更新のためのコンテンツハッシュ(
[hash])
よく使われるパターン
パターン 1: テキストのみを返すシンプルなツール
ウィジェットを使わずにテキストを返すツールです。
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'get_weather',
description: 'Use this when the user asks about current weather in a city',
inputSchema: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' }
},
required: ['city']
}
// No annotations - returns plain text
}]
}));
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'get_weather') {
const { city } = request.params.arguments as { city: string };
// In production, call weather API
const weather = await fetchWeather(city);
return {
content: [{
type: 'text',
text: `Weather in ${city}: ${weather.description}, ${weather.temp}°F`
}]
};
}
});
使用する場面: 視覚的な表示を必要としない、シンプルなデータ取得
パターン 2: リストウィジェット
項目のリストを表示するツールです。
// Tool registration
{
name: 'search_products',
description: 'Use this when the user wants to search for products',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string' }
}
},
annotations: {
openai: {
outputTemplate: 'ui://widget/product-list.html'
}
}
}
// Tool handler
const products = await searchProducts(query);
return {
content: [{
type: 'text',
text: `Found ${products.length} products`
}],
_meta: {
initialData: {
products: products.map(p => ({
id: p.id,
name: p.name,
price: p.price,
image: p.image
}))
}
}
};
<div id="products"></div>
<script>
const data = window.openai.getInitialData();
const container = document.getElementById('products');
data.products.forEach(product => {
const item = document.createElement('div');
item.className = 'product-item';
item.innerHTML = `
<img src="${product.image}" alt="${product.name}">
<h3>${product.name}</h3>
<p>$${product.price}</p>
`;
container.appendChild(item);
});
</script>
使用する場面: 項目のコレクションを表示する場合(検索結果、おすすめなど)
パターン 3: ツールのコールバックに対応したインタラクティブウィジェット
ほかのツールを呼び出せるウィジェットです。
// Register multiple related tools
{
name: 'show_tasks',
description: 'Use this when user wants to see their task list',
annotations: {
openai: {
outputTemplate: 'ui://widget/tasks.html'
}
}
},
{
name: 'complete_task',
description: 'Mark a task as completed',
inputSchema: {
type: 'object',
properties: {
taskId: { type: 'number' }
}
}
}
<div id="tasks"></div>
<script>
const data = window.openai.getInitialData();
data.tasks.forEach(task => {
const taskEl = document.createElement('div');
taskEl.innerHTML = `
<span>${task.title}</span>
<button onclick="completeTask(${task.id})">Complete</button>
`;
document.getElementById('tasks').appendChild(taskEl);
});
function completeTask(taskId) {
if (window.openai && window.openai.callTool) {
window.openai.callTool({
name: 'complete_task',
arguments: { taskId }
});
}
}
</script>
使用する場面: 後続のアクションを呼び出す必要があるインタラクティブウィジェット
バンドルされたリソースの使用
スクリプト(scripts/)
./scripts/scaffold-openai-app.sh を実行すると、スタータープロジェクト一式を生成できます。
chmod +x ./scripts/scaffold-openai-app.sh
./scripts/scaffold-openai-app.sh my-new-app
cd my-new-app
npm install
npm run dev
リファレンス(references/)
references/mcp-protocol-basics.md- MCP SDK の使用パターンreferences/openai-metadata-format.md- OpenAI 固有のアノテーションフィールドすべてreferences/widget-patterns.md- ウィジェットの HTML サンプルとベストプラクティスreferences/optional-agents-upgrade.md- ステートフル機能を追加するタイミングと方法
Claude がこれらを読み込むタイミング:この SKILL.md に記載されている内容より詳しい API リファレンスや高度なパターンが必要な場合
テンプレート(templates/)
templates/basic/- 最小構成のステートレス MCP サーバーtemplates/with-react/- Vite ビルドを使用する React ベースのウィジェット
使用方法:新しいプロジェクトの出発点として、テンプレートディレクトリ全体をコピーします
高度なトピック
入力検証に Zod を使用する
import { z } from 'zod';
const CreateTaskInputSchema = z.object({
title: z.string().min(1).max(200),
dueDate: z.string().datetime().optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium')
});
// Convert Zod schema to JSON Schema for MCP
const jsonSchema = zodToJsonSchema(CreateTaskInputSchema);
// Use in tool registration
{
name: 'create_task',
inputSchema: jsonSchema,
// ...
}
// Validate in handler
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'create_task') {
try {
const validated = CreateTaskInputSchema.parse(request.params.arguments);
// validated is typed and validated
} catch (error) {
if (error instanceof z.ZodError) {
return {
content: [{
type: 'text',
text: `Invalid input: ${error.errors.map(e => e.message).join(', ')}`
}],
isError: true
};
}
}
}
});
環境変数とシークレット
# Set secrets via Wrangler
wrangler secret put API_KEY
# Access in code
type Bindings = {
ASSETS: Fetcher;
API_KEY: string; // Secret binding
};
// In handler
const apiKey = c.env.API_KEY;
Vite を使用した React ウィジェット
// vite.config.ts - add React plugin
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react(),
cloudflare({ /* ... */ })
],
// ...
});
// src/widgets/TaskList.tsx
import { useState, useEffect } from 'react';
export function TaskList() {
const [tasks, setTasks] = useState([]);
useEffect(() => {
if (window.openai) {
const data = window.openai.getInitialData();
setTasks(data.tasks);
}
}, []);
return (
<div>
{tasks.map(task => (
<div key={task.id}>{task.title}</div>
))}
</div>
);
}
依存関係
必須:
@modelcontextprotocol/[email protected]- MCP プロトコルの公式実装[email protected]- 軽量ルーティングフレームワーク[email protected]- 実行時の型検証
任意:
@cloudflare/[email protected]- Workers と静的アセット向けのビルドツール(推奨)@vitejs/[email protected]- ウィジェット向けの React サポート[email protected]- Cloudflare Agents SDK(ステートフルなエージェントが必要な場合のみ)
公式ドキュメント
- Model Context Protocol:https://modelcontextprotocol.io/
- MCP SDK(GitHub):https://github.com/modelcontextprotocol/typescript-sdk
- OpenAI Apps SDK:https://developers.openai.com/apps-sdk
- Cloudflare Workers:https://developers.cloudflare.com/workers/
- Hono フレームワーク:https://hono.dev/
- Context7 ライブラリ ID:/modelcontextprotocol/typescript-sdk
パッケージのバージョン(2025-11-17 に検証済み)
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.21.0",
"hono": "^4.10.2",
"zod": "^3.25.76"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.13.13",
"@cloudflare/workers-types": "^4.20250531.0",
"@vitejs/plugin-react": "^4.3.4",
"vite": "^7.1.9",
"wrangler": "^4.42.2"
}
}
本番環境での例
このスキルは、以下のパターンに基づいています:
- Toolbase-AI OpenAI Apps Template:https://github.com/Toolbase-AI/openai-apps-sdk-cloudflare-vite-template
- 構築時間:ゼロからデプロイまで約45分
- エラー:0件(既知の問題8件すべてをこのスキルで防止)
- 検証:✅ MCP Inspector のテストに合格し、ChatGPT でウィジェットが正しく表示されることを確認
トラブルシューティング
問題:MCP Inspector に「Connection refused」と表示される
解決策:開発サーバーが正しいポートで実行されており、/mcp エンドポイントが設定されていることを確認します:
npm run dev # Should show port (usually 8787)
npx @modelcontextprotocol/inspector http://localhost:8787/mcp
問題:ChatGPT でウィジェットが空白のページとして表示される
解決策:ブラウザーのコンソールでエラーを確認します。よくある原因:
- CSP が外部スクリプトをブロックしている - Vite でバンドルします
window.openaiのチェックがない - API の準備が整う前にウィジェットが読み込まれています- CORS ヘッダーがない -
/mcp/*ルートに追加します
問題:ツールは登録されているが、呼び出されない
解決策:ツールの説明を、よりアクション指向の内容に改善します:
// ✅ Good
description: 'Use this when the user wants to see weather forecast for a specific city'
// ❌ Bad
description: 'Weather tool'
セットアップ完了チェックリスト
-
npm create cloudflareでプロジェクトのスキャフォールドを作成 - 依存関係をインストール(
@modelcontextprotocol/sdk、hono、zod) -
wrangler.jsoncで ASSETS バインディングを設定 -
vite.config.tsで CloudFlare プラグインを設定 -
/mcp/*ルートに CORS ミドルウェアを追加 - MCP サーバーに少なくとも 1 つのツールを登録
- ツールにアクション指向の説明を設定
- ウィジェットの HTML を
src/widgets/に作成 -
ui://widget/URI パターンでリソースを登録 - MIME タイプを
text/html+skybridgeに設定 - ツールハンドラーがウィジェット用の
_meta.initialDataを返す - 開発サーバーがエラーなく起動(
npm run dev) - MCP Inspector が正常に接続
- 本番ビルドが成功(
npm run build) - Cloudflare Workers にデプロイ(
npx wrangler deploy) - ChatGPT の開発者モードでテスト
質問や問題がありますか?
- すべてのアノテーションフィールドについては
references/openai-metadata-format.mdを確認してください - セットアップ手順のすべてのステップを確認してください
- MCP の公式ドキュメントを確認してください:https://modelcontextprotocol.io/
- CORS で
https://chatgpt.comが許可されていることを確認してください - ChatGPT で試す前に MCP Inspector でテストしてください