Skip to content
Development
Skill

/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, widget 404s, MIME types, ASSETS binding errors, Next.js

From plugin
coco
264174 skills37 agents41 commands
Install
$ npx -y skills add coco-research/coco --skill openai-apps-mcp --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/openai-apps-mcp

Context preview

The summary Claude sees to decide when to auto-load this skill.

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, widget 404s, MIME types, ASSETS binding errors, Next.js

SKILL.md

openai-apps-mcp.SKILL.md
name: OpenAI Apps MCP
description: |
  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, widget 404s, MIME types, ASSETS binding errors, Next.js integration issues, or edge platform limitations.
user-invocable: true
allowed-tools: [Read, Write, Edit, Bash, Glob, Grep]
domain: engineering

Building OpenAI Apps with Stateless MCP Servers

**Status**: Production Ready **Last Updated**: 2026-01-21 **Dependencies**: `cloudflare-worker-base`, `hono-routing` (optional) **Latest Versions**: @modelcontextprotocol/sdk@1.25.3, hono@4.11.3, zod@4.3.5, wrangler@4.58.0

---

Overview

Build **ChatGPT Apps** using **MCP (Model Context Protocol)** servers on Cloudflare Workers. Extends ChatGPT with custom tools and interactive widgets (HTML/JS UI rendered in iframe).

**Architecture**: ChatGPT → MCP endpoint (JSON-RPC 2.0) → Tool handlers → Widget resources (HTML)

**Status**: Apps available to Business/Enterprise/Edu (GA Nov 13, 2025). MCP Apps Extension (SEP-1865) formalized Nov 21, 2025.

---

Quick Start

1. Scaffold & Install

npm create cloudflare@latest my-openai-app -- --type hello-world --ts --git --deploy false
cd my-openai-app
npm install @modelcontextprotocol/sdk@1.25.3 hono@4.11.3 zod@4.3.5
npm install -D @cloudflare/vite-plugin@1.17.1 vite@7.2.4

2. Configure 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. Create MCP Server (`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. Create Widget (`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. Deploy

npm run build
npx wrangler deploy
npx @modelcontextprotocol/inspector https://my-app.workers.dev/mcp

---

Critical Requirements

**CORS**: Must allow `https://chatgpt.com` on `/mcp/*` routes **Widget URI**: Must use `ui://widget/` prefix (e.g., `ui://widget/map.html`) **MIME Type**: Must be `text/html+skybridge` for HTML resources **Widget Data**: Pass via `_meta.initialData` (accessed via `window.openai.getInitialData()`) **Tool Descriptions**: Action-oriented ("Use this when user wants to...") **ASSETS Binding**: Serve widgets from ASSETS, not bundled in worker code **SSE**: Send heartbeat every 30s (100s timeout on Workers)

---

Known Issues Prevention

This skill prevents **14** documented issues:

Issue #1: CORS Policy Blocks MCP Endpoint

**Error**: `Access to fetch blocked by CORS policy` **Fix**: `app.use('/mcp/*', cors({ origin: 'https://chatgpt.com' }))`

Issue #2: Widget Returns 404 Not Found

**Error**: `404 (Not Found)` for widget URL **Fix**: Use `ui://widget/` prefix (not `resource://` or `/widgets/`)

annotations: { openai: { outputTemplate: 'ui://widget/map.html' } }

Issue #3: Widget Displays as Plain Text

**Error**: HTML source code visible instead of rendered widget **Fix**: MIME type must be `text/html+skybridge` (not `text/html`)

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [{ uri: 'ui://widget/map.html', mimeType: 'text/html+skybridge' }]
}));

Issue #4: ASSETS Binding Undefined

**Error**: `TypeError: Cannot read property 'fetch' of undefined` **Fix**: Binding name in wrangler.jsonc must match TypeScript

{ "assets": { "binding": "ASSETS" } }  // wrangler.jsonc
type Bindings = { ASSETS: Fetcher };  // index.ts

Issue #5: SSE Connection Drops After 100 Seconds

**Error**: SSE stream closes unexpectedly **Fix**: Send heartbeat every 30s (Workers timeout at 100s inactivity)

const heartbeat = setInterval(async () => {
  await stream.writeSSE({ data: JSON.stringify({ type: 'heartbeat' }), event: 'ping' });
}, 30000);

Issue #6

Read more
Ships withcoco

Meet Coco. A superintelligent agent framework powered by an advisory board of 389 world-class minds. Scale your AI assistant into a complete engineering department with 142 skills, 277 commands, and persistent state. Universal compatibility. Local privacy. Free and open source.

Get the whole plugin

Other skills on coco.