Public
Like
1
ChatAppSDKStarter
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.
Viewing readonly version of main branch: v13View latest version
Interactive widget for ChatGPT using the App SDK with MCP Lite.
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
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
// backend/mcp/server.ts
mcp.resource("ui://widget/index.html", {
mimeType: "text/html+skybridge",
// ...
});
mcp.tool("list_messages", {
handler: async () => ({
structuredContent: {
kind: "message_list", // Discriminator for routing
messages: [...]
}
})
});
switch (data.kind) {
case "message_list":
navigate({ to: "/messages" });
break;
case "greeting":
navigate({ to: "/greeting" });
break;
}
const data = useToolOutput<MessageListOutput>();
// Render data.messages
Lists all messages from the message board. Shows interactive message list widget.
Adds a new message and shows updated message list.
Greets a user by name with a personalized widget.
- Define output type in
backend/mcp/types.ts:
export const MyOutputSchema = z.object({
kind: z.literal("my_kind"),
data: z.string(),
});
- Create widget component in
widget/components/:
export function MyWidget() {
const data = useToolOutput<MyOutput>();
const theme = useTheme();
// Render UI
}
- Add route in
widget/routes.tsx:
const myRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/my-view",
component: MyWidget,
});
- Update NavigationSync in
widget/NavigationSync.tsx:
case "my_kind":
navigate({ to: "/my-view" });
break;
- Register tool in
backend/mcp/server.ts:
mcp.tool("my_tool", {
outputSchema: MyOutputSchema,
_meta: widgetMeta(),
handler: async () => ({
structuredContent: {
kind: "my_kind",
data: "..."
}
})
});
vt watch # Edit files locally - changes auto-deploy in ~100ms
- Go to val.town
- Open your project
- Edit files directly
- Changes deploy instantly
-
Get your Val URL:
https://yourname-projectname.val.run -
Add MCP server to ChatGPT:
MCP Server URL: https://yourname-projectname.val.run/mcp -
Invoke a tool:
User: "List all messages" ChatGPT calls list_messages tool Widget renders with message list
Val Town's serveFile automatically:
- Transpiles TSX β JS
- Sets correct Content-Type headers
- Handles CSS and other assets
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";
Every TSX file needs:
/** @jsxImportSource https://esm.sh/react@19 */
Widget HTML uses placeholder __VAL_TOWN_URL__ which is replaced at runtime with actual Val Town project URL.
Widget CSP allows:
connect_domains: Val Town project URL + esm.shresource_domains: Val Town project URL + esm.sh
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@19for packages with React peer dependencies
Routing doesn't work:
- Check
kinddiscriminator matches NavigationSync cases - Verify route paths are defined in routes.tsx
Styles not applied:
- Check
/widget-assets/index.cssis accessible - Verify CSS link in index.html uses
__VAL_TOWN_URL__placeholder