npm package: @iflow-mcp/jezweb-chatgpt-app-sdk
Description: ChatGPT Portfolio App - MCP-powered portfolio with interactive widgets
Author: Jeremy Dawes
Latest version: 0.1.0

# 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.

![Portfolio Widget in ChatGPT](docs/images/widget-screenshot.png)

## 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:
1. Calls your tool
2. Reads the widget resource from `openai/outputTemplate` URI
3. Renders HTML in an iframe sandbox
4. Injects data via `window.openai.toolOutput`

**Important metadata fields:**
```typescript
_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`:

```typescript
// 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`:

```typescript
// 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:

```typescript
// 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:

```typescript
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:

```html
<!-- 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 config
```

## Development

### Prerequisites

- Node.js 18+ and pnpm
- Cloudflare account
- Wrangler CLI

### Setup

```bash
# 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 dev
```

### Build & Deploy

```bash
# Build for production (includes widget bundle)
CLOUDFLARE_ENV=production pnpm build

# Deploy to Cloudflare
npx wrangler deploy
```

### Testing MCP Endpoints

```bash
# 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

- [OpenAI Apps SDK Docs](https://developers.openai.com/apps-sdk/)
- [OpenAI Apps SDK Examples](https://github.com/openai/openai-apps-sdk-examples)
- [MCP Specification](https://modelcontextprotocol.io/)
- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)

## 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: jeremy@jezweb.net
- GitHub: [@jezweb](https://github.com/jezweb)

---

Built with [Claude Code](https://claude.ai/code) AI assistant