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)

Core Documents (Must-Read)#

Design#

Build#

Deployment / Testing / Submission#

Guides / References#

General References#


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 / modified
    • destructiveHint: false — No irreversible external impact
    • openWorldHint: false — No public internet state changes

User Experience (UX) Scenarios#

Basic Flow (Interactive Widget)#

  1. The user types @Ladder Pick or something like “play a ladder game.”
  2. ChatGPT calls the create_game tool, and the Ladder Pick widget is displayed in an iframe.
  3. 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
  4. 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)

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:

  1. MCP Server (required) — Defines the app’s functionality (tools) and exposes them via the /mcp endpoint to ChatGPT. Operates as an HTTP server using StreamableHTTPServerTransport.
  2. 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 over postMessage (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 / modified
  • destructiveHint: false — No irreversible impact on external systems
  • openWorldHint: false — No public internet state changes
  • Exception: export_result is a pure read, so readOnlyHint: 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.length for all mode, 0 for one-by-one mode
  • 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[], new mapping[], 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.html single 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/initializeui/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.html directly 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_result locally via the localCall() 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" in package.json
  • pnpm add @modelcontextprotocol/sdk @modelcontextprotocol/ext-apps zod
  • pnpm 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/updateGame API
  • Shared types (types.ts): GameState, Pairing, RevealMode

2. MCP Server Implementation#

  • src/server.ts: HTTP server + /mcp endpoint + CORS handling + GET / health check
  • Register public/ladder-widget.html as ui://widget/ladder.html via registerAppResource
  • Register 4 tools via registerAppTool (zod schemas + _meta.ui.resourceUri binding)
  • 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/initializeui/notifications/initialized, tools/call, ui/notifications/tool-result reception
  • 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 Pick in 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#

  1. Go to the Lightsail console
  2. Click Create instance
  3. 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
  4. Click Create instance

5-2. Static IP Attachment#

Lightsail console → Instance → Networking tab:

  1. Click Attach static IP → Create and attach a static IP
  2. Use this IP for domain DNS

5-3. Firewall Configuration#

Lightsail console → Instance → Networking tab → IPv4 Firewall:

RuleProtocolPort
SSHTCP22 (default)
HTTPTCP80 (default)
HTTPSTCP443 ← 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 address

To use Lightsail’s free DNS zone:

  1. Lightsail console → NetworkingCreate DNS zone
  2. Enter domain → Set nameservers at your domain registrar
  3. 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 reboot

5-6. Nginx Reverse Proxy Configuration#

# ─── Install Nginx ───
sudo yum install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx

Create 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/mcphttp://127.0.0.1:8787/mcp The trailing / in proxy_pass strips 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.confsudo 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 status

5-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>/mcp returns 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.html directly 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)
© 2026 Ted Kim. All Rights Reserved. | Email Contact