Ladder Pick — ChatGPT App Development Plan#
- Project name:
ted-mcp-servers - App name (App Directory display): Ladder Pick
- App language: English first (UI / tool description / privacy policy all in English; only the plan document is in Korean)
Official Developer Documentation Reference Links#
Core Documents (Must-Read)#
- Apps SDK Home: https://developers.openai.com/apps-sdk
- Quickstart (includes Todo example): https://developers.openai.com/apps-sdk/quickstart
- MCP Server Concepts: https://developers.openai.com/apps-sdk/concepts/mcp-server
- MCP Apps in ChatGPT: https://developers.openai.com/apps-sdk/mcp-apps-in-chatgpt
Design#
- UX Principles: https://developers.openai.com/apps-sdk/concepts/ux-principles
- UI Guidelines: https://developers.openai.com/apps-sdk/concepts/ui-guidelines
- Design Components: https://developers.openai.com/apps-sdk/plan/components
- Tool Definitions: https://developers.openai.com/apps-sdk/plan/tools
- Use Case Research: https://developers.openai.com/apps-sdk/plan/use-case
Build#
- MCP Server Setup: https://developers.openai.com/apps-sdk/build/mcp-server
- ChatGPT UI Build: https://developers.openai.com/apps-sdk/build/chatgpt-ui
- State Management: https://developers.openai.com/apps-sdk/build/state-management
- Authentication: https://developers.openai.com/apps-sdk/build/auth
- Example Apps (GitHub): https://github.com/openai/openai-apps-sdk-examples
- UI Library (GitHub): https://github.com/openai/apps-sdk-ui
Deployment / Testing / Submission#
- Deployment Guide: https://developers.openai.com/apps-sdk/deploy
- Testing Guide: https://developers.openai.com/apps-sdk/deploy/testing
- ChatGPT Connection: https://developers.openai.com/apps-sdk/deploy/connect-chatgpt
- App Submission & Maintenance: https://developers.openai.com/apps-sdk/deploy/submission
- App Submission Guidelines (Policies / Rules): https://developers.openai.com/apps-sdk/app-submission-guidelines
- Developer Mode Setup: https://platform.openai.com/docs/guides/developer-mode
Guides / References#
- Security & Privacy: https://developers.openai.com/apps-sdk/guides/security-privacy
- Metadata Optimization: https://developers.openai.com/apps-sdk/guides/optimize-metadata
- Troubleshooting: https://developers.openai.com/apps-sdk/deploy/troubleshooting
- API Reference: https://developers.openai.com/apps-sdk/reference
- Changelog: https://developers.openai.com/apps-sdk/changelog
General References#
- Apps in ChatGPT (Help Center): https://help.openai.com/en/articles/12503483-apps-in-chatgpt-and-the-apps-sdk
- App Directory Submission (Help Center): https://help.openai.com/en/articles/20001040
- App Directory Browsing: https://chatgpt.com/apps
Goals#
- Build a “Ladder Pick” app that can be listed on the ChatGPT App Directory.
- Enable the complete flow — entering participants / items → generating a ladder (random matching) → revealing results — through an interactive widget (iframe) within a chat.
- After testing with ChatGPT Developer Mode, the ultimate goal is App Directory submission / approval / Publish.
Assumptions / Scope#
- MVP: Interactive widget (iframe UI) + MCP tool-based. The widget provides core interactions such as entering participants / items, displaying results, and reshuffling.
- Out of scope (initial): Login / payments, real-time multiplayer, complex animations, external data integration.
- No authentication required: Since there is no external service integration, we start without OAuth / authentication flows.
- Safety / Policy: No write operations to external systems. Tool hint annotations:
readOnlyHint: false— Not true because internal state is created / modifieddestructiveHint: false— No irreversible external impactopenWorldHint: false— No public internet state changes
User Experience (UX) Scenarios#
Basic Flow (Interactive Widget)#
- The user types
@Ladder Pickor something like “play a ladder game.” - ChatGPT calls the
create_gametool, and the Ladder Pick widget is displayed in an iframe. - Within the widget:
- Players list: Add / remove participant names (default 4)
- Items list: Add / remove result items (prizes / roles)
- Options: Reveal mode (All at once / One by one), Seed (auto / custom)
- Click the “Pick!” button → Generate matching results
- Results area:
- All at once: Display the full matching table immediately
- One by one: Reveal one person at a time with the “Reveal Next” button
- “Reshuffle” button: Reshuffle with a new seed
- “Export” button: Copy results as text
Text Fallback#
- Even without the widget, ChatGPT can display tool call results as text (table).
- Example: “Ladder Pick, match A,B,C,D with 1st,2nd,3rd,4th” → Text table response
Error Cases#
- Fewer than 2 participants → Error message: “At least 2 players are required.”
- Item count ≠ participant count → Error message: “Number of items must match number of players. You have {n} players and {m} items.”
- Items list empty → Error message: “Items list cannot be empty.”
Functional Requirements#
Input#
- Participant list (Players): 2–20 people
- Result items (Items): Must be exactly the same count as participants (error returned on mismatch)
- Options
- Reveal mode:
all|one-by-one - Seed: Auto-generated or user-specified (for reproducibility)
- Reveal mode:
Output#
- Matching results: Player ↔ Item 1:1 mapping
- Ladder visualization: Canvas-based visual ladder (vertical lines + horizontal rungs + color-coded path animations)
State / Storage#
- Module-level
Map<gameId, GameState>for in-process in-memory management - Unique ID issued on game creation; subsequent reshuffle / reveal_next lookups use that ID
- No persistent storage (resets on server restart; this is sufficient for the initial version)
Technical Design#
Architecture (Based on Official Structure)#
ChatGPT Apps consist of two components:
- MCP Server (required) — Defines the app’s functionality (tools) and exposes them via the
/mcpendpoint to ChatGPT. Operates as an HTTP server usingStreamableHTTPServerTransport. - Web Component — HTML / CSS / JS rendered in an iframe inside ChatGPT. When registered as a resource (
ui://...) on the MCP server, ChatGPT displays the UI alongside tool call results. Communicates with the MCP server via JSON-RPC overpostMessage(ui/*methods).
The MCP server is the app itself. The “Apps SDK” provides a way to register UI resources and tools on top of the MCP server — it is not a separate “app server” architecture.
Core Packages#
@modelcontextprotocol/sdk^1.27.1— MCP server framework (McpServer, StreamableHTTPServerTransport)@modelcontextprotocol/ext-apps^1.1.2— Apps SDK helpers (registerAppResource, registerAppTool, RESOURCE_MIME_TYPE)zod^3.25.76— Tool input schema validation
Stack#
- Node.js + TypeScript
- Package manager: pnpm
- UI: Vanilla HTML / CSS / JS single file (official Quickstart pattern), switch to React if needed
- (Optional)
@openai/apps-sdk-ui— Official open-source UI library - Deployment: AWS Lightsail (Nginx reverse proxy + Let’s Encrypt HTTPS, multi-app monorepo)
Folder Structure#
ted-mcp-servers/ # Monorepo root
├── README.md
├── apps/
│ └── ladder-pick/ # Ladder Pick app
│ ├── package.json
│ ├── tsconfig.json
│ ├── .env.example # Environment variable examples (PORT, etc.)
│ ├── assets/
│ │ └── ladder-pick-icon.png # App icon for App Directory submission
│ ├── public/
│ │ └── ladder-widget.html # ChatGPT iframe widget (includes standalone mode)
│ ├── src/
│ │ ├── server.ts # MCP server entry (HTTP + /mcp endpoint)
│ │ ├── types.ts # Shared type definitions
│ │ ├── tools/
│ │ │ ├── create-game.ts
│ │ │ ├── reshuffle.ts
│ │ │ ├── reveal-next.ts
│ │ │ └── export-result.ts
│ │ └── core/
│ │ ├── ladder.ts # Matching algorithm (Fisher-Yates shuffle)
│ │ ├── rng.ts # mulberry32 seed-based RNG
│ │ ├── validate.ts # Input validation
│ │ └── game-store.ts # Map-based in-memory game state storage
│ ├── dist/ # TypeScript build output
│ └── docs/
│ ├── privacy-policy.md
│ └── test-prompts.md
│ └── other-app/ # Future additional app (example)
│ └── ...Core Algorithm#
Matching#
- Validate that the Players array and Items array have the same length → Return error on mismatch (no auto-correction)
- mulberry32 seed-based RNG + Fisher-Yates shuffle to shuffle Items and create a 1:1 mapping with Players
- If no seed is provided, generate a 12-character seed based on
Math.random().toString(36)and return it (for reproducibility) - Implement the same mulberry32 algorithm on both the server (Node.js) and widget (browser JS) to guarantee identical results with the same seed
Ladder Visualization (Canvas)#
- Vertical lines: Arrange vertical lines equal to the number of participants
- Horizontal rungs: Place random horizontal lines between adjacent vertical lines based on the seed
- Path back-calculation: Decompose the Fisher-Yates result (permutation) into bubble sort adjacent swaps to place horizontal lines → achieve exact matching
- Path animation: easeInOutCubic easing, display paths in per-player colors
- All at once: Animate all paths simultaneously (2.2 seconds)
- One by one: Reveal one path at a time (1.6 seconds per person)
- Start / end badges: Player name (top) + item name (bottom), shown in the same color to indicate the connection
MCP Tool Design#
Common Metadata#
All tool descriptions / titles are written in English. Default hint annotation values:
{
"readOnlyHint": false,
"destructiveHint": false,
"openWorldHint": false
}readOnlyHint: false— Not a pure read since internal game state is created / modifieddestructiveHint: false— No irreversible impact on external systemsopenWorldHint: false— No public internet state changes- Exception:
export_resultis a pure read, soreadOnlyHint: true
If these annotations do not match the actual behavior, the submission will be rejected. (Refer to official rejection reasons)
Tool 1: create_game#
- title: “Create ladder game”
- description: “Creates a new ladder game with the given players and items, producing a random 1:1 matching.”
- Input schema (zod):
players: z.array(z.string().min(1)).min(2).max(20),items: z.array(z.string().min(1)).min(2).max(20),seed?: z.string(),revealMode?: z.enum(["all", "one-by-one"]).default("all") - Validation:
players.length !== items.length→ Return error - Output (structuredContent):
gameId,seed,revealMode,players[],items[],mapping[],totalCount,revealedCount- Always includes
players[],items[],mapping[]regardless of revealMode (needed for the widget to construct the ladder) revealedCount:mapping.lengthfor all mode,0for one-by-one mode
- Always includes
- UI:
_meta.ui.resourceUri: "ui://widget/ladder.html"
Tool 2: reshuffle#
- title: “Reshuffle”
- description: “Reshuffles the matching of an existing game with a new seed.”
- Input:
gameId: z.string(),seed?: z.string() - Output (structuredContent):
gameId,seed,revealMode,players[],items[], newmapping[],totalCount,revealedCount: 0
Tool 3: reveal_next#
- title: “Reveal next”
- description: “Reveals the next player-item pair in one-by-one mode.”
- Input:
gameId: z.string() - Output:
player,item,revealedSoFar,remainingCount
Tool 4: export_result#
- title: “Export result”
- description: “Exports the full game result as shareable text or JSON.”
- Input:
gameId: z.string(),format: z.enum(["text", "json"]) - Output: Result string
- hint:
readOnlyHint: true
Web Component (Widget) Design#
Rendering Approach#
public/ladder-widget.htmlsingle HTML file (CSS / JS inline), all UI text in English- Rendered as an iframe inside ChatGPT
- Communicates with MCP server via JSON-RPC over
postMessage:ui/initialize→ui/notifications/initialized(bridge initialization)tools/call(tool invocation from widget)ui/notifications/tool-result(receive tool results called by the model)
UI Layout (English)#
- Input Card
- “Players” list: Text input + Add / Remove buttons (tag format)
- “Items” list: Text input + Add / Remove buttons (tag format)
- Options: Reveal mode toggle (All at once / One by one), Seed input (optional)
- CTA button: "🎲 Pick!"
- When game results are received from ChatGPT, the Players / Items lists are automatically synced with server results
- Ladder Card
- Canvas-based ladder visualization (vertical lines + horizontal rungs + color-coded path animations)
- Buttons: “Reveal Next ▶” (one-by-one mode), “🔀 Reshuffle”, “📋 Export”, “↩ New”
- Progress indicator: “{n}/{total} revealed”
- Seed display (bottom)
- Error Display
- Inline error messages (red, within input card)
- “At least 2 players are required.”
- “Number of items must match number of players. You have {n} players and {m} items.”
- “Items list cannot be empty.”
Standalone Mode#
- Opening
public/ladder-widget.htmldirectly in a browser allows testing without the MCP server - Automatically detected via iframe detection + bridge initialization timeout (2 seconds)
- Displays a “Standalone Mode — testing without MCP server” banner at the top
- Executes
create_game,reshuffle,export_resultlocally via thelocalCall()function - Uses the same mulberry32 RNG as the server → guarantees identical results with the same seed
Styling#
- Clean card UI with rounded corners (14px), shadows, CSS variable-based color system
- 20-color fixed palette per player (same color for start point, path, and end point)
- Responsive: Works properly on mobile ChatGPT app (
max-width: 520px, mobile media queries)
Development Phases (Execution Order)#
0. Project Bootstrap#
pnpm init- Set
"type": "module"inpackage.json pnpm add @modelcontextprotocol/sdk @modelcontextprotocol/ext-apps zodpnpm add -D typescript @types/node- TypeScript configuration (
tsconfig.json):target: ES2022,module: NodeNext,moduleResolution: NodeNext - Build scripts:
build(tsc),start(node dist/server.js),dev(node –watch dist/server.js),build:watch(tsc –watch)
1. Core Logic Implementation#
- Input validation (
validate.ts): Return error on players / items length mismatch, trim whitespace, remove empty strings - RNG (
rng.ts): Seed-based RNG using the mulberry32 algorithm + Fisher-Yates shuffle,generateSeed()for generating 12-character random seeds - Matching algorithm (
ladder.ts):createMapping(players, items, seed)— 1:1 mapping from shuffle results - In-memory GameStore (
game-store.ts):Map<string, GameState>,setGame/getGame/updateGameAPI - Shared types (
types.ts):GameState,Pairing,RevealMode
2. MCP Server Implementation#
src/server.ts: HTTP server +/mcpendpoint + CORS handling +GET /health check- Register
public/ladder-widget.htmlasui://widget/ladder.htmlviaregisterAppResource - Register 4 tools via
registerAppTool(zod schemas +_meta.ui.resourceUribinding) - Specify hint annotations for each tool
- All tool titles / descriptions written in English
3. Interactive Widget Implementation#
- Write
public/ladder-widget.html(English UI, single file with inline CSS / JS) - MCP Bridge:
ui/initialize→ui/notifications/initialized,tools/call,ui/notifications/tool-resultreception - Standalone mode: Auto-detection via iframe detection + 2-second timeout, local execution via
localCall() - Canvas ladder visualization: Ladder construction using mulberry32 RNG + bubble swap back-calculation, path animation
- State synchronization: Update the Input card’s Players / Items UI when results are received from the server / ChatGPT
- Responsive layout (mobile ChatGPT app support)
4. Local Testing#
- Standalone browser testing (without MCP server):
open public/ladder-widget.html - Build and run the server:
pnpm build && pnpm start # → http://localhost:8787/mcp - Tool-level testing with MCP Inspector:
npx @modelcontextprotocol/inspector@latest --server-url http://localhost:8787/mcp --transport http - Expose local server via ngrok:
ngrok http 8787 - ChatGPT Developer Mode connection:
- Enable Developer mode in Settings → Apps & Connectors → Advanced settings
- Chat input
+button → More → Enter URL:https://<id>.ngrok.app/mcp, Auth: None - Test by selecting
@Ladder Pickin the conversation or from the More menu
- Test scope: 10 representative prompts + error cases (count mismatch / empty input / special characters)
- Mobile testing: Verify widget rendering in ChatGPT iOS / Android apps (must pass both web + mobile)
5. AWS Deployment (Lightsail + Nginx + Let’s Encrypt)#
Multi-App Architecture#
A single Lightsail instance hosts multiple ChatGPT app MCP servers together.
┌─────────────────────────────────────────┐
│ Lightsail ($5/month) │
│ │
HTTPS 443 │ Nginx (reverse proxy + SSL) │
─────────────► │ /ladder-pick/* → localhost:8787 │
│ /other-app/* → localhost:8788 │
│ /next-app/* → localhost:8789 │
│ ... │
│ │
│ PM2 (process management) │
│ ├─ ladder-pick :8787 │
│ ├─ other-app :8788 │
│ └─ next-app :8789 │
└─────────────────────────────────────────┘- Path-based routing: Each app has a unique path prefix (e.g.,
/ladder-pick/mcp) - Port separation: Each app runs independently on a separate port via PM2
- Shared SSL: A single Let’s Encrypt certificate is shared across all apps
- Independent deployment: Each app can be deployed independently via
git pull → build → pm2 restart - When adding a new app: Deploy code → Register with PM2 → Add Nginx location block →
nginx -s reload
5-1. Instance Creation#
- Go to the Lightsail console
- Click Create instance
- Settings:
- Region:
us-east-1(or preferred region) - Platform: Linux / Unix
- Blueprint: OS Only → Amazon Linux 2023 (or Node.js blueprint)
- Plan: $5 / month (1GB RAM; upgrade to the $10 plan as the number of apps grows)
- Instance name:
ted-mcp-servers
- Region:
- Click Create instance
5-2. Static IP Attachment#
Lightsail console → Instance → Networking tab:
- Click Attach static IP → Create and attach a static IP
- Use this IP for domain DNS
5-3. Firewall Configuration#
Lightsail console → Instance → Networking tab → IPv4 Firewall:
| Rule | Protocol | Port |
|---|---|---|
| SSH | TCP | 22 (default) |
| HTTP | TCP | 80 (default) |
| HTTPS | TCP | 443 ← Must be added |
+ Add rule → Add HTTPS (443)
5-4. Domain and DNS Configuration#
Example domain: apps.yourdomain.com (shared by all apps)
DNS settings (Lightsail DNS or external DNS):
A record → Lightsail static IP addressTo use Lightsail’s free DNS zone:
- Lightsail console → Networking → Create DNS zone
- Enter domain → Set nameservers at your domain registrar
- Add A record:
apps.yourdomain.com→ Static IP
Since this uses path-based routing rather than per-app subdomains, only one domain is needed.
5-5. Server Environment Setup (After SSH Access)#
# ─── Common environment installation (one-time) ───
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo yum install -y nodejs git
sudo npm install -g pnpm pm2
# ─── Clone repo ───
cd ~
git clone https://github.com/<your-repo>/ted-mcp-servers.git
cd ted-mcp-servers
# ─── Deploy Ladder Pick ───
cd apps/ladder-pick
echo "optional=false" > .npmrc
pnpm install --frozen-lockfile
pnpm build
PORT=8787 pm2 start dist/server.js --name ladder-pick
cd ../..
# ─── Adding a new app (example) ───
# cd apps/other-app
# pnpm install --frozen-lockfile && pnpm build
# PORT=8788 pm2 start dist/server.js --name other-app
# cd ../..
# ─── Register PM2 auto-start ───
pm2 save
pm2 startup # Run the sudo command from the output to register auto-start on reboot5-6. Nginx Reverse Proxy Configuration#
# ─── Install Nginx ───
sudo yum install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginxCreate configuration file (/etc/nginx/conf.d/ted-mcp-servers.conf):
server {
listen 80;
server_name apps.yourdomain.com;
# ─── Ladder Pick (port 8787) ───
location /ladder-pick/ {
proxy_pass http://127.0.0.1:8787/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_buffering off;
proxy_read_timeout 3600s;
}
# ─── Add new app: copy location block ───
# location /other-app/ {
# proxy_pass http://127.0.0.1:8788/;
# ... (same proxy settings)
# }
# ─── Domain verification (OpenAI Apps) ───
location = /.well-known/openai-apps-challenge {
return 200 '_c3oL6tq5igjgS91lqyRaccPmNuIakZeclCMpFMJSyI';
add_header Content-Type text/plain;
}
# ─── Health check ───
location = / {
return 200 'ted-mcp-servers server is running';
add_header Content-Type text/plain;
}
}Path mapping:
https://apps.yourdomain.com/ladder-pick/mcp→http://127.0.0.1:8787/mcpThe trailing/inproxy_passstrips the location prefix.
# Test and restart Nginx configuration
sudo nginx -t
sudo systemctl restart nginx
# Verify HTTP is working
curl http://apps.yourdomain.com/ladder-pick/
# → "Ladder Pick MCP server is running"When adding a new app: Add a location block to ted-mcp-servers.conf → sudo nginx -s reload
5-7. Let’s Encrypt HTTPS Setup#
# ─── Install Certbot ───
sudo yum install -y certbot python3-certbot-nginx
# ─── Issue certificate (includes automatic nginx configuration) ───
sudo certbot --nginx -d apps.yourdomain.com
# At the prompts:
# - Enter email
# - Agree to terms (Y)
# - HTTP→HTTPS redirect setup (recommended: Yes)
# ─── Test auto-renewal ───
sudo certbot renew --dry-run
# Certbot registers auto-renewal via systemd timer (every 90 days)After completion, the nginx configuration is automatically updated to its final state:
server {
server_name apps.yourdomain.com;
# ─── Ladder Pick ───
location /ladder-pick/ {
proxy_pass http://127.0.0.1:8787/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_buffering off;
proxy_read_timeout 3600s;
}
# ─── Add location blocks when adding new apps ───
# ─── Domain verification (OpenAI Apps) ───
location = /.well-known/openai-apps-challenge {
return 200 '_c3oL6tq5igjgS91lqyRaccPmNuIakZeclCMpFMJSyI';
add_header Content-Type text/plain;
}
location = / {
return 200 'ted-mcp-servers server is running';
add_header Content-Type text/plain;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/apps.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/apps.yourdomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
server_name apps.yourdomain.com;
return 301 https://$host$request_uri;
}A single certificate covers all app paths. No certificate reissuance is needed when adding apps.
5-8. Post-Deployment Verification#
# Overall server health check
curl https://apps.yourdomain.com/
# → "ted-mcp-servers server is running"
# Ladder Pick health check
curl https://apps.yourdomain.com/ladder-pick/
# → "Ladder Pick MCP server is running"
# Verify Ladder Pick MCP tool listing
curl -X POST https://apps.yourdomain.com/ladder-pick/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# → Confirm 4 tools are returned
# Verify SSL certificate
curl -vI https://apps.yourdomain.com/ 2>&1 | grep "SSL certificate"5-9. Code Update Procedure#
# After SSH access — update only a specific app (no impact on other apps)
cd ~/ted-mcp-servers/apps/ladder-pick
git pull
pnpm install --frozen-lockfile
pnpm build
pm2 restart ladder-pick
# Check status of all apps
pm2 status5-10. Deployment Checklist#
Common Infrastructure (One-Time):
- Create Lightsail instance (
ted-mcp-servers) and attach static IP - Add HTTPS (443) port to firewall
- Set DNS A record (
apps.yourdomain.com) → Static IP - Install Node.js + pnpm + PM2
- Install and configure Nginx
- Issue Let’s Encrypt certificate and verify auto-renewal
Per-App (For Ladder Pick and each subsequent app):
- Deploy code (
git clone → pnpm install → pnpm build) - Register app process with PM2 (
PORT=<port> pm2 start) - Add Nginx location block →
sudo nginx -s reload - Verify
GET /<app-path>/health check returns 200 - Verify
POST /<app-path>/mcpreturns tools/list correctly - Verify CORS headers are included (
Access-Control-Allow-Origin: *) - Verify CSP header configuration
- Set ChatGPT connector URL (e.g.,
https://apps.yourdomain.com/ladder-pick/mcp) - Re-test on deployed server via ChatGPT Developer Mode (web + mobile)
6. Directory Submission Preparation#
- Individual verification: Confirm Identity verification is complete at
platform.openai.com/settings/organization/general - Owner role: Owner role in the organization is required for app submission
- Privacy policy written (
docs/privacy-policy.md) → Host on web and prepare URL - Test prompts / expected results documented (
docs/test-prompts.md) - App icon prepared (
assets/ladder-pick-icon.png, 2048×2048px PNG) - Submission materials (English):
- App name: Ladder Pick
- Logo / icon:
assets/ladder-pick-icon.png - Description (one-liner + detailed)
- Privacy policy URL (web hosting required)
- MCP server URL:
https://apps.yourdomain.com/ladder-pick/mcp - Tool annotation justification (readOnlyHint / destructiveHint / openWorldHint for each tool)
- Screenshots (web / mobile)
- Test prompts + expected responses (refer to
docs/test-prompts.md) - Supported country settings
- EU data residency constraint: Cannot submit from EU data residency projects; must use a global data residency project
7. Submission → Review Response → Publish#
- Click “Submit for review” on the OpenAI Platform Dashboard (
platform.openai.com/apps-manage) - Check review status: Dashboard + email notifications
- Prepare for common rejection reasons:
- MCP server unreachable (URL errors, etc.; no credential issues since no authentication)
- Test case result mismatches (verify both web / mobile)
- Tool hint annotation mismatches
- User data returned that is not disclosed in the privacy policy
- Unnecessary PII / internal identifiers included in responses
- After approval, click Publish on the dashboard → Listed on the App Directory
- For subsequent updates: Create a new version draft → Re-submit / re-review
Test Plan#
Functional Tests#
- Reproducibility: Same seed → same mapping
- Integrity: No duplicate matchings (1:1)
- Input validation (error returns):
- players < 2 → error
- items count ≠ players count → error (no auto-correction)
- items empty → error
- Duplicate names / whitespace / emoji / special characters → Process after normalization
- UX:
- In one-by-one mode, reveal_next correctly advances the state
- On reshuffle, verify that the seed and results change
Widget Tests#
- Standalone mode: Open
ladder-widget.htmldirectly in browser and verify basic functionality - Renders without console errors inside an iframe
- Widget state restoration (UI updates after receiving tool-result notification)
- Verify that the Input card’s Players / Items sync with server results when a game is created in ChatGPT
- Canvas ladder rendering correctness (vertical lines, horizontal rungs, color-coded paths, start / end badges)
- Inline error message display
- Mobile layout working correctly
Regression Checklist (Based on Official Documentation)#
- Correct tool selection / argument passing on golden prompts
- Does not trigger unnecessarily on negative prompts
- structuredContent matches the declared schema
- No unused prototype tools remain
Submission / Operations Checklist#
- Minimize permissions: No external data access, no authentication required
- Hint annotation accuracy: Re-verify that hints match actual behavior (top rejection reason)
- Data minimization: Process only user input (players / items); do not include PII / internal identifiers / tokens in responses
- CSP definition: Set Content Security Policy on the MCP server (required for submission)
- Privacy policy: Specify all collected / processed data categories in English
- Region / plan issues: Prepare guidance text assuming the Connect button may be disabled
- Pre-launch press coordination: Contact press@openai.com in advance for any public announcements related to launch
Backlog (Optional Enhancements)#
- “Constrained matching” (e.g., A cannot be matched with B)
- Multi-language support (Korean, etc.)
- Team / workspace shared links
- Various game modes (penalties / roles, etc.)
- Switch to React-based widget (as complexity grows)
- Monetization (Agentic Commerce Protocol integration)
- Ladder animation speed control option
- Expand maximum player count (currently capped at 20)