OpenAI Apps MCP
Build ChatGPT apps with MCP servers on Cloudflare Workers. Extend ChatGPT with custom tools and interactive widgets (HTML/JS UI). Use when: developing ChatGPT extensions, implementing MCP servers, or troubleshooting CORS blocking (allow chatgpt.com), widget 404s (missing ui://widget/), wrong MIME type (text/html+skybridge), or ASSETS binding undefined.
Best use case
OpenAI Apps MCP is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Build ChatGPT apps with MCP servers on Cloudflare Workers. Extend ChatGPT with custom tools and interactive widgets (HTML/JS UI). Use when: developing ChatGPT extensions, implementing MCP servers, or troubleshooting CORS blocking (allow chatgpt.com), widget 404s (missing ui://widget/), wrong MIME type (text/html+skybridge), or ASSETS binding undefined.
Teams using OpenAI Apps MCP should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/openai-apps-mcp/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How OpenAI Apps MCP Compares
| Feature / Agent | OpenAI Apps MCP | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Build ChatGPT apps with MCP servers on Cloudflare Workers. Extend ChatGPT with custom tools and interactive widgets (HTML/JS UI). Use when: developing ChatGPT extensions, implementing MCP servers, or troubleshooting CORS blocking (allow chatgpt.com), widget 404s (missing ui://widget/), wrong MIME type (text/html+skybridge), or ASSETS binding undefined.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Building OpenAI Apps with Stateless MCP Servers
**Status**: Production Ready
**Last Updated**: 2025-11-17
**Dependencies**: `cloudflare-worker-base`, `hono-routing` (optional, helpful for routing patterns)
**Latest Versions**: @modelcontextprotocol/sdk@1.21.0, hono@4.10.2, wrangler@4.42.2
---
## Overview
This skill provides production-tested patterns for building **OpenAI Apps** - applications that extend ChatGPT's functionality through the Model Context Protocol (MCP). Focus on **stateless MCP servers** using Cloudflare Workers, which covers 80% of OpenAI Apps use cases.
### What Are OpenAI Apps?
OpenAI Apps are extensions that integrate into the ChatGPT interface, allowing users to:
- Access third-party services directly in conversations
- Display interactive widgets (maps, carousels, lists, etc.)
- Execute tools that return structured UI components
- Enhance ChatGPT with domain-specific capabilities
### Architecture
```
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)
```
### Key Components
1. **MCP Server** - HTTP endpoint exposing tools via Model Context Protocol
2. **Tool Handlers** - Functions that process inputs and return results
3. **Widget Resources** - HTML pages that render in ChatGPT's iframe
4. **OpenAI Metadata** - Special annotations for widget routing and display
---
## Quick Start (10 Minutes)
### 1. Scaffold Project
```bash
npm create cloudflare@latest my-openai-app -- --type hello-world --ts --git --deploy false
cd my-openai-app
# Install dependencies
npm install @modelcontextprotocol/sdk@1.21.0 hono@4.10.2 zod@3.25.76
npm install -D @cloudflare/vite-plugin@1.13.13 vite@7.1.9
```
**Why this matters:**
- `@modelcontextprotocol/sdk` is the official MCP protocol implementation
- `hono` provides lightweight routing perfect for API endpoints
- Vite + CloudFlare plugin enable building and serving widgets
### 2. Configure wrangler.jsonc
```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
}
}
```
**CRITICAL:**
- `nodejs_compat` flag is required for MCP SDK
- `assets.binding: "ASSETS"` must match TypeScript binding name
- `assets.directory` must match Vite build output
### 3. Create MCP Server
```typescript
// 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;
```
---
## The 5-Step Setup Process
### Step 1: Project Scaffolding
Use Cloudflare's official scaffolding:
```bash
npm create cloudflare@latest my-openai-app -- --type hello-world --ts --git --deploy false
```
**Key Points:**
- Creates Workers project with TypeScript
- Includes wrangler.jsonc
- Initializes git repository
### Step 2: Install Dependencies
```bash
npm install @modelcontextprotocol/sdk@1.21.0 hono@4.10.2 zod@3.25.76
npm install -D @cloudflare/vite-plugin@1.13.13 vite@7.1.9
```
**What each package does:**
- `@modelcontextprotocol/sdk` - Official MCP protocol (Anthropic)
- `hono` - Fast, lightweight routing framework
- `zod` - Runtime type validation for tool inputs
- `@cloudflare/vite-plugin` - Build tool for Workers + static assets
- `vite` - Frontend build tool
### Step 3: Configure Build System
Create `vite.config.ts`:
```typescript
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';
}
}
}
}
});
```
**Why this matters:**
- Builds both worker code and static assets
- Proper output structure for Workers + ASSETS binding
- Content hashing for cache busting
### Step 4: Create Widget HTML
```bash
mkdir -p src/widgets
```
Create `src/widgets/hello.html`:
```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>
```
**What to avoid:**
- Don't use third-party CDN scripts (CSP may block)
- Don't use custom fonts (use system fonts)
- Don't make external API calls without CORS
### Step 5: Deploy and Test
```bash
# Build
npm run build
# Deploy to Cloudflare
npx wrangler deploy
# Test with MCP Inspector
npx @modelcontextprotocol/inspector https://my-openai-app.workers.dev/mcp
```
---
## Critical Rules
### Always Do
✅ Set CORS to allow `https://chatgpt.com`
✅ Use resource URI pattern `ui://widget/` for widgets
✅ Set MIME type to `text/html+skybridge` for HTML resources
✅ Include `_meta.initialData` in tool responses for widget initialization
✅ Use action-oriented tool descriptions ("Use this when...")
✅ Validate tool inputs with Zod schemas
✅ Test with MCP Inspector before deploying to ChatGPT
### Never Do
❌ Use custom MIME types (must be `text/html+skybridge`)
❌ Forget CORS configuration (ChatGPT won't connect)
❌ Use resource URIs without `ui://widget/` prefix
❌ Bundle widgets in worker code (use ASSETS binding)
❌ Skip input validation (tools receive untrusted data)
❌ Use SSE without heartbeat (100s timeout on Workers)
❌ Deploy without testing in MCP Inspector first
---
## Known Issues Prevention
This skill prevents **8** documented issues:
### Issue #1: CORS Policy Blocks MCP Endpoint
**Error**: `Access to fetch at 'https://my-app.workers.dev/mcp' from origin 'https://chatgpt.com' has been blocked by CORS policy`
**Source**: Common browser console error when CORS not configured
**Why It Happens**: ChatGPT makes cross-origin requests to your MCP server. Without CORS headers, browsers block these requests.
**Prevention**:
```typescript
app.use('/mcp/*', cors({
origin: 'https://chatgpt.com', // Must allow ChatGPT specifically
credentials: true,
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization']
}));
```
### Issue #2: Widget Returns 404 Not Found
**Error**: `Failed to load resource: the server responded with a status of 404 (Not Found)` for widget URL
**Source**: OpenAI Apps SDK requirement for `ui://widget/` prefix
**Why It Happens**: Resource URI must follow specific pattern. Wrong prefix causes ChatGPT to fail widget resolution.
**Prevention**:
```typescript
// ✅ Correct
annotations: {
openai: {
outputTemplate: 'ui://widget/map.html'
}
}
// ❌ Wrong - will 404
outputTemplate: 'resource://map.html'
outputTemplate: '/widgets/map.html'
```
### Issue #3: Widget Displays as Plain Text
**Error**: HTML source code visible instead of rendered widget
**Source**: OpenAI Apps SDK MIME type requirement
**Why It Happens**: ChatGPT expects specific MIME type `text/html+skybridge` to render widgets. Standard `text/html` won't work.
**Prevention**:
```typescript
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [{
uri: 'ui://widget/map.html',
mimeType: 'text/html+skybridge' // Must use +skybridge
}]
}));
```
### Issue #4: ASSETS Binding Undefined
**Error**: `TypeError: Cannot read property 'fetch' of undefined` when accessing `c.env.ASSETS`
**Source**: Misconfigured wrangler.jsonc or TypeScript types
**Why It Happens**: ASSETS binding name in wrangler.jsonc must match TypeScript binding interface.
**Prevention**:
```jsonc
// wrangler.jsonc
{
"assets": {
"directory": "dist/client",
"binding": "ASSETS" // Must match TypeScript
}
}
```
```typescript
// src/index.ts
type Bindings = {
ASSETS: Fetcher; // Must match wrangler.jsonc binding name
};
```
### Issue #5: SSE Connection Drops After 100 Seconds
**Error**: SSE stream closes unexpectedly, tools stop responding
**Source**: Cloudflare Workers SSE timeout when no data sent
**Why It Happens**: Workers close SSE connections after 100 seconds of inactivity. Must send heartbeat events.
**Prevention**:
```typescript
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 Doesn't Suggest Tool
**Error**: Tool registered but never appears in ChatGPT suggestions
**Source**: OpenAI tool discovery requires action-oriented descriptions
**Why It Happens**: Generic descriptions don't match user intent patterns. ChatGPT uses descriptions to decide when to invoke tools.
**Prevention**:
```typescript
// ✅ 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: Widget Can't Access Initial Data
**Error**: `window.openai.getInitialData()` returns `undefined` in widget
**Source**: Missing `_meta.initialData` in tool response
**Why It Happens**: Data must be explicitly passed via `_meta` field. Regular response content doesn't reach widgets.
**Prevention**:
```typescript
// 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: Widget Scripts Blocked by CSP
**Error**: `Refused to load the script because it violates the following Content Security Policy directive`
**Source**: ChatGPT's Content Security Policy for widget iframes
**Why It Happens**: External scripts from third-party CDNs are blocked for security.
**Prevention**:
```html
<!-- ✅ Inline scripts work -->
<script>
console.log('Widget loaded');
</script>
<!-- ✅ Same-origin scripts work -->
<script src="/widgets/lib.js"></script>
<!-- ❌ Third-party CDNs may be blocked -->
<script src="https://cdn.example.com/library.js"></script>
<!-- Workaround: Bundle libraries with Vite -->
```
---
## Configuration Files Reference
### wrangler.jsonc (Full Example)
```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"
}
}
```
**Why these settings:**
- `nodejs_compat` - Required for MCP SDK (uses Node.js APIs)
- `assets.directory` - Where Vite outputs widget HTML files
- `assets.binding: "ASSETS"` - How to access static assets in code
- `observability` - Enable logging and tracing for debugging
### vite.config.ts (Full Example)
```typescript
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]'
}
}
}
});
```
**Why these settings:**
- Multiple input entry points (worker + widgets)
- Separate output for worker (`index.js`) vs client assets (`client/`)
- Content hashing for cache busting (`[hash]`)
---
## Common Patterns
### Pattern 1: Simple Text-Only Tool
Tool that returns text without widgets:
```typescript
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`
}]
};
}
});
```
**When to use**: Simple data retrieval that doesn't need visual representation
### Pattern 2: List Widget
Tool that displays a list of items:
```typescript
// 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
}))
}
}
};
```
```html
<!-- product-list.html -->
<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>
```
**When to use**: Displaying collections of items (search results, recommendations, etc.)
### Pattern 3: Interactive Widget with Tool Callbacks
Widget that can trigger other tools:
```typescript
// 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' }
}
}
}
```
```html
<!-- tasks.html -->
<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>
```
**When to use**: Interactive widgets that need to trigger follow-up actions
---
## Using Bundled Resources
### Scripts (scripts/)
Run `./scripts/scaffold-openai-app.sh` to generate a complete starter project:
```bash
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/)
- `references/mcp-protocol-basics.md` - MCP SDK usage patterns
- `references/openai-metadata-format.md` - All OpenAI-specific annotation fields
- `references/widget-patterns.md` - Widget HTML examples and best practices
- `references/optional-agents-upgrade.md` - When and how to add stateful features
**When Claude should load these**: When you need detailed API references or advanced patterns beyond this SKILL.md
### Templates (templates/)
- `templates/basic/` - Minimal stateless MCP server
- `templates/with-react/` - React-based widgets with Vite build
**Usage**: Copy entire template directory as starting point for new projects
---
## Advanced Topics
### Using Zod for Input Validation
```typescript
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
};
}
}
}
});
```
### Environment Variables and Secrets
```bash
# 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;
```
### React Widgets with Vite
```typescript
// vite.config.ts - add React plugin
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react(),
cloudflare({ /* ... */ })
],
// ...
});
```
```tsx
// 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>
);
}
```
---
## Dependencies
**Required**:
- `@modelcontextprotocol/sdk@1.21.0` - Official MCP protocol implementation
- `hono@4.10.2` - Lightweight routing framework
- `zod@3.25.76` - Runtime type validation
**Optional**:
- `@cloudflare/vite-plugin@1.13.13` - Build tool for Workers + static assets (recommended)
- `@vitejs/plugin-react@4.3.4` - React support for widgets
- `agents@0.2.23` - Cloudflare Agents SDK (only if you need stateful agents)
---
## Official Documentation
- **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 Framework**: https://hono.dev/
- **Context7 Library ID**: /modelcontextprotocol/typescript-sdk
---
## Package Versions (Verified 2025-11-17)
```json
{
"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"
}
}
```
---
## Production Example
This skill is based on patterns from:
- **Toolbase-AI OpenAI Apps Template**: https://github.com/Toolbase-AI/openai-apps-sdk-cloudflare-vite-template
- **Build Time**: ~45 minutes from zero to deployed
- **Errors**: 0 (all 8 known issues prevented with this skill)
- **Validation**: ✅ MCP Inspector tests passing, widgets rendering correctly in ChatGPT
---
## Troubleshooting
### Problem: MCP Inspector shows "Connection refused"
**Solution**: Ensure dev server is running on correct port and `/mcp` endpoint is configured:
```bash
npm run dev # Should show port (usually 8787)
npx @modelcontextprotocol/inspector http://localhost:8787/mcp
```
### Problem: Widget shows blank page in ChatGPT
**Solution**: Check browser console for errors. Common causes:
1. CSP blocking external scripts - bundle them with Vite
2. Missing `window.openai` check - widget loads before API ready
3. CORS headers missing - add to `/mcp/*` route
### Problem: Tool registered but never invoked
**Solution**: Improve tool description to be more action-oriented:
```typescript
// ✅ Good
description: 'Use this when the user wants to see weather forecast for a specific city'
// ❌ Bad
description: 'Weather tool'
```
---
## Complete Setup Checklist
- [ ] Project scaffolded with `npm create cloudflare`
- [ ] Dependencies installed (`@modelcontextprotocol/sdk`, `hono`, `zod`)
- [ ] `wrangler.jsonc` configured with ASSETS binding
- [ ] `vite.config.ts` configured with CloudFlare plugin
- [ ] CORS middleware added to `/mcp/*` routes
- [ ] At least one tool registered with MCP server
- [ ] Tool has action-oriented description
- [ ] Widget HTML created in `src/widgets/`
- [ ] Resource registered with `ui://widget/` URI pattern
- [ ] MIME type set to `text/html+skybridge`
- [ ] Tool handler returns `_meta.initialData` for widgets
- [ ] Dev server runs without errors (`npm run dev`)
- [ ] MCP Inspector connects successfully
- [ ] Production build succeeds (`npm run build`)
- [ ] Deployed to Cloudflare Workers (`npx wrangler deploy`)
- [ ] Tested in ChatGPT developer mode
---
**Questions? Issues?**
1. Check `references/openai-metadata-format.md` for all annotation fields
2. Verify all steps in the setup process
3. Check official MCP docs: https://modelcontextprotocol.io/
4. Ensure CORS is configured to allow `https://chatgpt.com`
5. Test with MCP Inspector before trying in ChatGPTRelated Skills
openai-responses
Build agentic AI applications with OpenAI's Responses API - the stateful successor to Chat Completions. Preserves reasoning across turns for 5% better multi-turn performance and 40-80% improved cache utilization. Use when: building AI agents with persistent reasoning, integrating MCP servers for external tools, using built-in Code Interpreter/File Search/Web Search, managing stateful conversations, implementing background processing for long tasks, or migrating from Chat Completions to gain polymorphic outputs and server-side tools.
openai-assistants
Build stateful chatbots with OpenAI Assistants API v2 - Code Interpreter, File Search (10k files), Function Calling. ⚠️ Sunset H1 2026; use openai-responses for new projects. Use when: building stateful chatbots, implementing RAG, or troubleshooting "thread has active run", vector store delays, polling timeouts, or file upload errors.
openai-api
Build with OpenAI's stateless APIs - Chat Completions (GPT-5, GPT-4o), Embeddings, Images (DALL-E 3), Audio (Whisper + TTS), and Moderation. Includes Node.js SDK and fetch-based approaches for Cloudflare Workers. Use when: implementing chat completions with GPT-5/GPT-4o, streaming responses with SSE, using function calling/tools, creating structured outputs with JSON schemas, generating embeddings for RAG (text-embedding-3-small/large), generating images with DALL-E 3, editing images with GPT-Image-1, transcribing audio with Whisper, synthesizing speech with TTS (11 voices), moderating content (11 safety categories), or troubleshooting rate limits (429), invalid API keys (401), function calling failures, streaming parse errors, embeddings dimension mismatches, or token limit exceeded.
openai-agents
Build AI applications with OpenAI Agents SDK - text agents, voice agents (realtime), multi-agent workflows with handoffs, tools with Zod schemas, input/output guardrails, structured outputs, and streaming. Deploy to Cloudflare Workers, Next.js, or React with human-in-the-loop patterns. Use when: building text-based agents with tools and Zod schemas, creating realtime voice agents with WebRTC/WebSocket, implementing multi-agent workflows with handoffs between specialists, setting up input/output guardrails for safety, requiring human approval for critical actions, streaming agent responses, deploying agents to Cloudflare Workers or Next.js, or troubleshooting Zod schema type errors, MCP tracing failures, infinite loops (MaxTurnsExceededError), tool call failures, schema mismatches, or voice agent handoff constraints.
zustand-state-management
Build type-safe global state in React applications with Zustand. Supports TypeScript, persist middleware, devtools, slices pattern, and Next.js SSR. Use when setting up React state, migrating from Redux/Context API, implementing localStorage persistence, or troubleshooting Next.js hydration errors, TypeScript inference issues, or infinite render loops.
zinc-database
Access ZINC (230M+ purchasable compounds). Search by ZINC ID/SMILES, similarity searches, 3D-ready structures for docking, analog discovery, for virtual screening and drug discovery.
zarr-python
Chunked N-D arrays for cloud storage. Compressed arrays, parallel I/O, S3/GCS integration, NumPy/Dask/Xarray compatible, for large-scale scientific computing pipelines.
youtube-transcript
Download YouTube video transcripts when user provides a YouTube URL or asks to download/get/fetch a transcript from YouTube. Also use when user wants to transcribe or get captions/subtitles from a YouTube video.
xlsx
Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas
wordpress-plugin-core
Build secure WordPress plugins with core patterns for hooks, database interactions, Settings API, custom post types, REST API, and AJAX. Covers three architecture patterns (Simple, OOP, PSR-4) and the Security Trinity. Use when creating plugins, implementing nonces/sanitization/escaping, working with $wpdb prepared statements, or troubleshooting SQL injection, XSS, CSRF vulnerabilities, or plugin activation errors.
whisper
OpenAI's general-purpose speech recognition model. Supports 99 languages, transcription, translation to English, and language identification. Six model sizes from tiny (39M params) to large (1550M params). Use for speech-to-text, podcast transcription, or multilingual audio processing. Best for robust, multilingual ASR.
weights-and-biases
Track ML experiments with automatic logging, visualize training in real-time, optimize hyperparameters with sweeps, and manage model registry with W&B - collaborative MLOps platform