npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

Portfolio Widget in ChatGPT

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:

_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 config

Development

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 dev

Build & Deploy

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

# Deploy to Cloudflare
npx wrangler deploy

Testing 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)


Built with Claude Code AI assistant