@iflow-mcp/jezweb-chatgpt-app-sdk
v0.1.0
Published
ChatGPT Portfolio App - MCP-powered portfolio with interactive widgets
Readme
Jezweb Portfolio - ChatGPT App SDK Demo
A proof-of-concept ChatGPT App demonstrating the OpenAI Apps SDK with MCP (Model Context Protocol)
This project showcases how to build interactive widgets that render directly inside ChatGPT conversations. Built as a learning exercise to understand the Apps SDK architecture before building more complex applications.

What This Demonstrates
- MCP Server: JSON-RPC 2.0 protocol implementation for ChatGPT
- Interactive Widgets: React components rendered in ChatGPT's iframe sandbox
- Real Data Integration: WordPress REST API for portfolio projects
- Lead Capture: Contact form with Cloudflare D1 database storage
- Edge-First: Cloudflare Workers with global CDN for widget assets
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ ChatGPT │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ User: "Show me Jezweb's portfolio" │ │
│ │ │ │
│ │ ChatGPT calls → show_portfolio tool │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Portfolio Widget (iframe) │ │ │
│ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │
│ │ │ │Card │ │Card │ │Card │ ← React Carousel │ │ │
│ │ │ └─────┘ └─────┘ └─────┘ │ │ │
│ │ │ [◀] [▶] │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Workers (Edge) │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ /mcp endpoint │ │ /api/contact │ │
│ │ (JSON-RPC 2.0) │ │ (Direct API) │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ ┌────────▼─────────────────────▼─────────┐ │
│ │ Hono Server │ │
│ │ • tools/list, tools/call │ │
│ │ • resources/list, resources/read │ │
│ └────────┬─────────────────────┬─────────┘ │
│ │ │ │
│ ┌────────▼─────────┐ ┌────────▼─────────┐ │
│ │ WordPress API │ │ D1 Database │ │
│ │ (Portfolio) │ │ (Leads) │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Key Learnings
1. MCP Protocol Basics
The Model Context Protocol uses JSON-RPC 2.0. Key methods:
| Method | Purpose |
|--------|---------|
| initialize | Handshake with protocol version |
| tools/list | Advertise available tools with schemas |
| tools/call | Execute a tool with arguments |
| resources/list | List available widget resources |
| resources/read | Return widget HTML content |
2. Widget Integration (Critical!)
Widgets are HTML served with mimeType: "text/html+skybridge". ChatGPT:
- Calls your tool
- Reads the widget resource from
openai/outputTemplateURI - Renders HTML in an iframe sandbox
- Injects data via
window.openai.toolOutput
Important metadata fields:
_meta: {
"openai/outputTemplate": "ui://widget/portfolio.html", // Widget URI
"openai/widgetDescription": "Description for ChatGPT", // Reduces narration
"openai/widgetAccessible": true, // Widget can call tools
"openai/resultCanProduceWidget": true, // Tool produces widget
"openai/toolInvocation/invoking": "Loading...", // Loading text
"openai/toolInvocation/invoked": "Ready", // Complete text
}3. Widget Data Access
ChatGPT passes structuredContent as window.openai.toolOutput:
// In your widget React component:
useEffect(() => {
// Direct path (ChatGPT flattens structuredContent)
const projects = window.openai?.toolOutput?.projects;
// Listen for updates
window.addEventListener('openai:set_globals', (event) => {
const data = event.detail?.globals?.toolOutput;
});
}, []);4. Layout Control
ChatGPT provides layout constraints via window.openai:
// Read constraints
const maxHeight = window.openai?.maxHeight;
const displayMode = window.openai?.displayMode; // 'inline' | 'pip' | 'fullscreen'
// Request more space
await window.openai?.requestDisplayMode({ mode: 'fullscreen' });5. Calling Tools from Widgets
This was tricky! window.openai.callTool() only works reliably for tools that produce widgets. For data-only operations (like our contact form), use a direct API endpoint instead:
// DON'T rely on this for non-widget tools:
// await window.openai.callTool('contact_about_project', data);
// DO use direct API:
await fetch('https://your-worker.workers.dev/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});6. CORS Configuration
Widget sandbox has unusual origins. Use permissive CORS for widget-accessible endpoints:
app.use('/api/*', cors({
origin: '*', // Widget sandbox can have null/blob origins
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type'],
}));7. Widget Asset Serving
Serve CSS/JS from the same origin as your MCP server to avoid CSP issues:
<!-- Widget HTML returned by resources/read -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://your-worker.workers.dev/widgets/styles.css">
</head>
<body>
<div id="root"></div>
<script src="https://your-worker.workers.dev/widgets/portfolio.js"></script>
</body>
</html>Tech Stack
| Layer | Technology | |-------|------------| | Runtime | Cloudflare Workers + Static Assets | | Frontend | React 19 + Vite 7 + Tailwind v4 | | UI Components | shadcn/ui + Radix UI | | Backend | Hono 4 | | Database | Cloudflare D1 + Drizzle ORM | | Validation | Zod | | Data Source | WordPress REST API |
Project Structure
chatgpt-app-sdk/
├── src/
│ ├── client/ # React frontend (dev UI)
│ │ ├── components/ui/ # shadcn/ui components
│ │ └── types/ # TypeScript types
│ ├── server/ # Hono backend
│ │ ├── routes/mcp.ts # MCP endpoint handler
│ │ ├── tools/ # Tool implementations
│ │ │ ├── portfolio.ts # show_portfolio tool
│ │ │ └── contact.ts # contact_about_project tool
│ │ └── index.ts # Server entry
│ ├── lib/
│ │ ├── mcp/ # MCP protocol (types, server)
│ │ ├── db/ # Drizzle schema
│ │ └── wordpress/ # WordPress API client
│ └── widgets/ # Widget entry points
│ └── PortfolioWidget.tsx
├── docs/ # Architecture docs
├── wrangler.jsonc # Cloudflare config
├── vite.config.ts # Main Vite config
└── vite.widget.config.ts # Widget bundle configDevelopment
Prerequisites
- Node.js 18+ and pnpm
- Cloudflare account
- Wrangler CLI
Setup
# Clone and install
git clone https://github.com/jezweb/chatgpt-app-sdk.git
cd chatgpt-app-sdk
pnpm install
# Create D1 database
npx wrangler d1 create chatgpt-portfolio-db
# Update wrangler.jsonc with database ID
# Run migrations
pnpm db:generate
npx wrangler d1 execute chatgpt-portfolio-db --local --file=drizzle/0000_*.sql
# Start dev server
pnpm devBuild & Deploy
# Build for production (includes widget bundle)
CLOUDFLARE_ENV=production pnpm build
# Deploy to Cloudflare
npx wrangler deployTesting MCP Endpoints
# List tools
curl -X POST https://your-worker.workers.dev/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# Call show_portfolio
curl -X POST https://your-worker.workers.dev/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"show_portfolio","arguments":{}}}'Gotchas & Solutions
1. Widget Not Rendering
Problem: Widget shows blank or errors in ChatGPT.
Solution: Check mimeType: "text/html+skybridge" in resources/read response.
2. Data Not Loading
Problem: window.openai.toolOutput is undefined.
Solution: Use event listener for openai:set_globals as data may load after widget.
3. Contact Form 400 Error
Problem: window.openai.callTool() returns 400 for non-widget tools.
Solution: Use direct API endpoint instead of MCP callTool.
4. Foreign Key Errors
Problem: Database insert fails with FK constraint. Solution: Remove FK references if using external IDs (e.g., WordPress post IDs).
5. CSP Blocking Assets
Problem: CSS/JS blocked by Content Security Policy. Solution: Serve assets from same origin as MCP server.
6. ChatGPT Over-Narrating
Problem: ChatGPT adds redundant text below widget.
Solution: Add openai/widgetDescription metadata and simplify tool response text.
Development Timeline
| Phase | Description | Status | |-------|-------------|--------| | 1 | Project Setup (Vite + Cloudflare) | ✅ | | 2 | Database Setup (D1 + Drizzle) | ✅ | | 3 | MCP Server (JSON-RPC 2.0) | ✅ | | 4 | Portfolio Widget (React Carousel) | ✅ | | 5 | Widget-MCP Integration | ✅ | | 6 | Contact Form | ✅ | | 7 | Styling & Polish | ✅ | | 8 | Content Hashing | ⏸️ | | 9 | Production Deployment | ✅ | | 10 | Documentation | ✅ |
Resources
Future Ideas
This POC opens the door to many possibilities:
- E-commerce product browser with cart
- Real-time dashboard widgets
- Interactive forms with multi-step flows
- Map/location-based interfaces
- Document viewers with annotations
- Scheduling/booking widgets
License
MIT License - Use this as a template for your own ChatGPT Apps!
Author
Jeremy Dawes (Jezweb)
- Website: https://www.jezweb.com.au
- Email: [email protected]
- GitHub: @jezweb
Built with Claude Code AI assistant
