A starter template for building ChatGPT apps with interactive widgets using MCP (Model Context Protocol) on Val Town.
Before using the OpenCloset functionality, add these environment variables in Val Town:
CLOUDINARY_CLOUD_NAME - Your Cloudinary cloud nameCLOUDINARY_API_KEY - Your Cloudinary API keyCLOUDINARY_API_SECRET - Your Cloudinary API secretCLOUDINARY_UPLOAD_PRESET - (Optional) Unsigned upload preset for easier setupVisit your deployment URL to get the MCP endpoint
Add to ChatGPT:
Test in ChatGPT:
list_messages - Shows all messages in an interactive widgetadd_message - Adds a new message and shows updated listget_message - Shows details for a specific messageshow_counter - Display an interactive counter with increment/decrement buttonsincrement_counter - Increment the counter by the step amountdecrement_counter - Decrement the counter by the step amountreset_counter - Reset the counter to zeroshow_todos - Display an interactive todo list with progress trackingshow_weather - Display current weather information for a locationshow_item_card - Display a single wardrobe item cardshow_multi_item_card - Display multiple wardrobe items in a gridshow_outfit_card - Display a complete outfit with top and bottom piecesopencloset.capture_items - Upload and categorize clothing photos to your closetopencloset.suggest_outfit - Get outfit suggestions from your saved itemsopencloset.list_items - View all items in your closet (with optional category filter)The project includes several React widget components:
/counter) - Interactive counter with +/- buttons and reset/todo) - Todo list with completion tracking and progress bar/weather) - Weather display with temperature, conditions, and stats/demo) - Showcase page displaying all three widgets togetherTools return discriminated unions that drive widget navigation:
{ kind: "message_list", messages: [...] } // → /list route
{ kind: "message_detail", id: 1, ... } // → /detail/:id route
{ kind: "counter", count: 5, step: 1 } // → /counter route
{ kind: "todo_list", todos: [...] } // → /todo route
{ kind: "weather", location: "NYC", ... } // → /weather route
{ kind: "item_card", item: {...} } // → Individual ItemCard component
{ kind: "multi_item_card", items: [...] } // → Individual MultiItemCard component
{ kind: "outfit_card", top: {...}, bottom: {...} } // → Individual OutfitCard component
The widget automatically navigates based on the kind field.
Messages are automatically scoped using the openai/subject field that ChatGPT includes in request metadata. This provides authless data isolation - each subject gets its own message board.
The scoping happens in tool handlers:
const subject = ctx.request.params._meta?.["openai/subject"];
const messages = await getMessages(subject);
The exact semantics of openai/subject are determined by ChatGPT.
backend/mcp/server.tsshared/types.tsfrontend/widgets/components/frontend/widgets/routes.tsxNavigationSync.tsx to handle new kindFor detailed information about the OpenCloset digital wardrobe functionality, see: