Openai apps mcp
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.
npx -y skills add coco-research/coco --skill openai-apps-mcpAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
17.6 KB, as published. Nobody here has run it
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/[email protected], [email protected], [email protected], [email protected]
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/[email protected] [email protected] [email protected]
npm install -D @cloudflare/[email protected] [email protected]
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: ChatGPT Doesn't Suggest Tool
Error: Tool registered but never appears in suggestions Fix: Use action-oriented descriptions
// β
Good: 'Use this when user wants to see a location on a map'
// β Bad: 'Shows a map'
Issue #7: Widget Can't Access Initial Data
Error: window.openai.getInitialData() returns undefined
Fix: Pass data via _meta.initialData
return {
content: [{ type: 'text', text: 'Here is your map' }],
_meta: { initialData: { location: 'SF', zoom: 12 } }
};
Issue #8: Widget Scripts Blocked by CSP
Error: Refused to load script (CSP directive)
Fix: Use inline scripts or same-origin scripts. Third-party CDNs blocked.
<!-- β
Works --> <script>console.log('ok');</script>
<!-- β Blocked --> <script src="https://cdn.example.com/lib.js"></script>
Issue #9: Hono Global Response Override Breaks Next.js (v1.25.0-1.25.2)
Error: No response is returned from route handler (Next.js App Router)
Source: GitHub Issue #1369
Affected Versions: v1.25.0 to v1.25.2
Fixed In: v1.25.3
Why It Happens: Hono (MCP SDK dependency) overwrites global.Response, breaking frameworks that extend it (Next.js, Remix, SvelteKit). NextResponse instanceof check fails.
Prevention:
- Upgrade to v1.25.3+ (recommended)
- Before fix: Use
webStandardStreamableHTTPServerTransportinstead - Or: Run MCP server on separate port from Next.js/Remix/SvelteKit app
// β
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: Elicitation (User Input) Fails on Cloudflare Workers
Error: EvalError: Code generation from strings disallowed
Source: GitHub Issue #689
Why It Happens: Internal AJV v6 validator uses prohibited APIs on edge platforms
Prevention: Avoid elicitInput() on edge platforms (Cloudflare Workers, Vercel Edge, Deno Deploy)
Workaround:
// β 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
});
Status: Requires MCP SDK v2 to fix properly. Track PR #844.
Issue #11: SSE Transport Statefulness Breaks Serverless Deployments
Error: 400: No transport found for sessionId
Source: GitHub Issue #273
Why It Happens: SSEServerTransport relies on in-memory session storage. In serverless environments (AWS Lambda, Cloudflare Workers), the initial GET /sse request may be handled by Instance A, but subsequent POST /messages requests land on Instance B, which lacks the in-memory state.
Prevention: Use Streamable HTTP transport (added in v1.24.0) instead of SSE for serverless deployments
Solution: For stateful SSE, deploy to non-serverless environments (VPS, long-running containers)
Official Status: Fixed by introducing Streamable HTTP (v1.24+) - now the recommended standard for serverless.
Issue #12: OAuth Configuration Requires TWO Separate Apps
Source: Cloudflare Remote MCP Server Docs Why It Happens: OAuth providers validate redirect URLs strictly. Localhost and production have different URLs, so they need separate OAuth client registrations. Prevention:
# Development OAuth App
Callback URL: http://localhost:8788/callback
# Production OAuth App
Callback URL: https://my-mcp-server.workers.dev/callback
Additional Requirements:
- KV namespace for auth state storage (create manually)
COOKIE_ENCRYPTION_KEYenv var:openssl rand -hex 32- Client restart required after config changes
Issue #13: Widget State Over 4k Tokens Causes Performance Issues (Community-sourced)
Source: OpenAI Apps SDK - ChatGPT UI Why It Happens: Widget state persists only to a single widget instance tied to one conversation message. State is reset when users submit via the main chat composer instead of widget controls. Prevention: Keep state payloads under 4k tokens for optimal performance
// β
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
});
Best Practice:
- Store only UI state (selected items, view mode, filters)
- Fetch data from MCP server on widget mount
- Use tool calls to persist important data
Issue #14: Widget-Initiated Tool Calls Fail Without Permission Flag (Community-sourced)
Source: OpenAI Apps SDK - ChatGPT UI
Why It Happens: Components initiating tool calls via window.openai.callTool() require the tool marked as "able to be initiated by the component" on the MCP server. Without this flag, calls fail silently.
Prevention: Mark tools as widgetCallable: true in annotations
// 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' }
});
Widget Development Best Practices
File Upload Limitations (Community-sourced)
Source: OpenAI Apps SDK - ChatGPT UI
window.openai.uploadFile() only supports 3 image formats: image/png, image/jpeg, and image/webp. Other formats fail silently.
// β
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' });
Alternative for Other File Types:
- Use base64 encoding in tool arguments
- Request user paste text content
- Use external upload service (S3, R2) and pass URL
Tool Performance Targets (Community-sourced)
Source: OpenAI Apps SDK - Troubleshooting
Tool calls exceeding "a few hundred milliseconds" cause UI sluggishness in ChatGPT. Official docs recommend profiling backends and implementing caching for slow operations.
Performance Targets:
- < 200ms: Ideal response time
- 200-500ms: Acceptable but noticeable
- > 500ms: Sluggish, needs optimization
Optimization Strategies:
// 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 Updates (December 2025)
Breaking Changes from @modelcontextprotocol/[email protected] β 1.25.x:
- Removed loose type exports (Prompts, Resources, Roots, Sampling, Tools) - use specific schemas
- ES2020 target required (previous: ES2018)
setRequestHandleris now typesafe - incorrect schemas throw type errors
New Features:
- Tasks (v1.24.0+): Long-running operations with progress tracking
- Sampling with Tools (v1.24.0+): Tools can request model sampling
- OAuth Client Credentials (M2M): Machine-to-machine authentication
Migration: If using loose type imports, update to specific schema imports:
// β 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 Migration Notes (MAJOR UPDATE - July 2025)
Breaking Changes from [email protected] β 4.x:
.default()now expects input type (not output type). Use.prefault()for old behavior.- ZodError:
error.issues(noterror.errors) .merge()and.superRefine()deprecated- Optional properties with defaults now always apply
Performance: 14x faster string parsing, 7x faster arrays, 6.5x faster objects
Migration: Update validation code:
// 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
{
"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"
}
}
Official Documentation
- MCP Specification: https://modelcontextprotocol.io/ (Latest: 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 Library ID: /modelcontextprotocol/typescript-sdk
Production Reference
Open Source Example: https://github.com/jezweb/chatgpt-app-sdk (portfolio carousel widget)
- Live in Production: Rendering in ChatGPT Business
- MCP Server: Full JSON-RPC 2.0 implementation with tools + resources (~310 lines)
- Widget Integration: WordPress API β
window.openai.toolOutputβ React carousel - Database: D1 (SQLite) for contact form submissions
- Stack: Hono 4 + React 19 + Tailwind v4 + Drizzle ORM
- Key Files:
/src/lib/mcp/server.ts- Complete MCP handler/src/server/tools/portfolio.ts- Tool with widget annotations/src/widgets/PortfolioWidget.tsx- Data access pattern
- Verified: All 14 known issues prevented, zero errors in production
Community Resources
Deployment Tools
Cloudflare One-Click Deploy: Deploy MCP servers to Cloudflare Workers with pre-built templates and auto-configured CI/CD. Includes OAuth wrapper and Python support.
- Docs: https://developers.cloudflare.com/agents/guides/remote-mcp-server/
- Blog: https://blog.cloudflare.com/model-context-protocol/
Frameworks
Skybridge (Community): React-focused framework with HMR support for widgets and enhanced MCP server helpers. Unofficial but actively maintained.
Note: Community frameworks are not officially supported. Use at your own discretion
Gives 0 of the 12 instructions most mcp tooling skills give
Counted across 638 of the 750 authors here whose files we hold, read 2026-08-06
- create ten complex read-only evaluation questionsin 71 of 638, across 17 files
- test servers using MCP Inspectorin 60 of 638, across 18 files
- provide actionable error messagesin 56 of 638, across 14 files
- prioritize comprehensive API coverage over specific workflowsin 54 of 638, across 12 files
- use TypeScript and Streamable HTTP for remote serversin 53 of 638, across 7 files
- define structured output schemas where possiblein 51 of 638, across 9 files
- use Zod or Pydantic for input schemasin 48 of 638, across 6 files
- fetch MCP specification pages with markdown suffixin 46 of 638, across 4 files
- load framework documentation using WebFetchin 45 of 638, across 3 files
- verify each evaluation answer independentlyin 45 of 638, across 3 files
- implement API client with authentication and paginationin 45 of 638, across 3 files
- Define input schemas with validationin 28 of 638, across 10 files
Said here and by no other author read
- Enable CORS for https://chatgpt.com
- Prefix widget URIs with ui://widget/
- Set widget MIME type to text/html+skybridge
- Pass widget data via _meta.initialData
- Write action-oriented tool descriptions
- Serve widgets from the ASSETS binding
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.