• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
laurynas

laurynas

ChatAppSDKStarter

A starter val for building ChatGPT Apps powered by React
Remix of laurynas/tanstackReactHonoExample
Public
Like
1
ChatAppSDKStarter
Home
Code
9
backend
4
frontend
7
shared
4
widget
3
widgets
8
.vtignore
AGENTS.md
README.md
deno.json
Branches
1
Pull requests
Remixes
5
History
Environment variables
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
widget
/
README.md
Code
/
widget
/
README.md
Search
10/10/2025
Viewing readonly version of main branch: v34
View latest version
README.md

ChatGPT Widget for Val Town

Interactive widget for ChatGPT using the App SDK with MCP Lite.

Architecture

This widget uses runtime transpilation - no build step required. Val Town automatically transpiles TSX to JS when serving files.

User → ChatGPT → MCP Server → Widget HTML Resource
                     ↓
               Widget iframe loads HTML
                     ↓
               Browser requests /widget-assets/index.tsx
                     ↓
               Val Town transpiles TSX → JS
                     ↓
               React app renders in iframe

Project Structure

widget/
├── index.html              # Widget HTML template
├── index.tsx               # React entry point
├── index.css               # Base styles
├── routes.tsx              # TanStack Router config
├── NavigationSync.tsx      # Syncs tool output to routes
├── openai-types.ts         # window.openai type definitions
├── hooks.ts                # useToolOutput, useTheme hooks
└── components/
    ├── LoadingWidget.tsx   # Loading state
    ├── MessageListWidget.tsx
    └── GreetingWidget.tsx

How It Works

1. MCP Server registers widget resource

// backend/mcp/server.ts mcp.resource("ui://widget/index.html", { mimeType: "text/html+skybridge", // ... });

2. Tools return structured content with discriminated types

mcp.tool("list_messages", { handler: async () => ({ structuredContent: { kind: "message_list", // Discriminator for routing messages: [...] } }) });

3. NavigationSync routes based on output kind

switch (data.kind) { case "message_list": navigate({ to: "/messages" }); break; case "greeting": navigate({ to: "/greeting" }); break; }

4. Components render tool output

const data = useToolOutput<MessageListOutput>(); // Render data.messages

Available Tools

list_messages

Lists all messages from the message board. Shows interactive message list widget.

add_message

Adds a new message and shows updated message list.

greet

Greets a user by name with a personalized widget.

Adding a New Tool

  1. Define output type in backend/mcp/types.ts:
export const MyOutputSchema = z.object({ kind: z.literal("my_kind"), data: z.string(), });
  1. Create widget component in widget/components/:
export function MyWidget() { const data = useToolOutput<MyOutput>(); const theme = useTheme(); // Render UI }
  1. Add route in widget/routes.tsx:
const myRoute = createRoute({ getParentRoute: () => rootRoute, path: "/my-view", component: MyWidget, });
  1. Update NavigationSync in widget/NavigationSync.tsx:
case "my_kind": navigate({ to: "/my-view" }); break;
  1. Register tool in backend/mcp/server.ts:
mcp.tool("my_tool", { outputSchema: MyOutputSchema, _meta: widgetMeta(), handler: async () => ({ structuredContent: { kind: "my_kind", data: "..." } }) });

Development Workflow

Using vt watch (Recommended)

vt watch # Edit files locally - changes auto-deploy in ~100ms

Using web IDE

  1. Go to val.town
  2. Open your project
  3. Edit files directly
  4. Changes deploy instantly

Testing with ChatGPT

  1. Get your Val URL:

    https://yourname-projectname.val.run
    
  2. Add MCP server to ChatGPT:

    MCP Server URL: https://yourname-projectname.val.run/mcp
    
  3. Invoke a tool:

    User: "List all messages"
    ChatGPT calls list_messages tool
    Widget renders with message list
    

Key Implementation Details

No Bundling Required

Val Town's serveFile automatically:

  • Transpiles TSX → JS
  • Sets correct Content-Type headers
  • Handles CSS and other assets

ESM Imports

All imports use full URLs:

import { useState } from "https://esm.sh/react@19"; import { Router } from "https://esm.sh/@tanstack/react-router@1?deps=react@19";

JSX Pragma Required

Every TSX file needs:

/** @jsxImportSource https://esm.sh/react@19 */

URL Replacement

Widget HTML uses placeholder __VAL_TOWN_URL__ which is replaced at runtime with actual Val Town project URL.

CSP Configuration

Widget CSP allows:

  • connect_domains: Val Town project URL + esm.sh
  • resource_domains: Val Town project URL + esm.sh

Troubleshooting

Widget doesn't load:

  • Check browser console for CSP errors
  • Verify /widget-assets/* endpoint returns JS (not TSX)
  • Ensure JSX pragma is at top of each TSX file

Import errors:

  • Use full esm.sh URLs with version pinning
  • Add ?deps=react@19 for packages with React peer dependencies

Routing doesn't work:

  • Check kind discriminator matches NavigationSync cases
  • Verify route paths are defined in routes.tsx

Styles not applied:

  • Check /widget-assets/index.css is accessible
  • Verify CSS link in index.html uses __VAL_TOWN_URL__ placeholder

Resources

  • ChatGPT Apps SDK
  • MCP Lite
  • Val Town Docs
  • TanStack Router
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2025 Val Town, Inc.