Your First App
This guide walks you through creating a complete Pancake app with views, actions, and data endpoints. By the end, you'll have a working app that runs in Claude, ChatGPT, and any MCP-compatible client.
Prerequisites
- Node.js 18+ installed
- pnpm, npm, or yarn package manager
- Basic familiarity with TypeScript
Create a New Project
The CLI is the fastest way to get started:
npx create-pancake my-app
cd my-app
pnpm installOr set up manually by following the Installation guide.
Understanding the Project Structure
The CLI creates a project with this structure:
my-app/
├── src/
│ ├── index.ts # Server entry point
│ └── views/
│ └── hello/
│ ├── index.html # HTML entry for Vite
│ ├── index.tsx # React component
│ └── metadata.json # View metadata
├── scripts/
│ └── dev.ts # Development server script
├── package.json
├── tsconfig.json
└── vite.config.tsViews: Interactive UIs
Views are the core of Pancake apps. They combine a server-side handler (for data fetching) with a client-side UI (HTML or React).
Define a View
In src/index.ts, views are defined in the views object:
import { createApp, defineView } from '@pancake-apps/server';
import { z } from 'zod';
const app = createApp({
name: 'my-app',
version: '0.1.0',
views: {
weather: defineView({
description: 'Show weather for a city',
input: z.object({
city: z.string().describe('City name'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
data: z.object({
city: z.string(),
temp: z.number(),
conditions: z.string(),
}),
handler: async ({ city, units }) => {
const weather = await fetchWeatherAPI(city);
const temp = units === 'fahrenheit'
? weather.temp * 9/5 + 32
: weather.temp;
return { city, temp, conditions: weather.conditions };
},
ui: { html: './src/views/weather/index.html' },
}),
},
});
app.start({ port: 3000 });The view has:
- description: What the AI sees when listing available tools
- input: Zod schema for parameters (from AI or URL)
- data: Zod schema for what your handler returns
- handler: Server-side function that fetches data
- ui: Path to the HTML or React component
Create the UI
Create src/views/weather/index.tsx:
import React from 'react';
import { createRoot } from 'react-dom/client';
import {
PancakeProvider,
useViewParams,
useTheme,
} from '@pancake-apps/web';
function WeatherView() {
const { data } = useViewParams<
{ city: string; units?: string },
{ city: string; temp: number; conditions: string }
>();
const theme = useTheme();
return (
<div style={{
padding: '2rem',
fontFamily: 'system-ui',
background: theme === 'dark' ? '#1a1a1a' : '#fff',
color: theme === 'dark' ? '#fff' : '#000',
minHeight: '100vh',
}}>
<h1>{data?.city}</h1>
<p style={{ fontSize: '3rem' }}>{data?.temp}°</p>
<p>{data?.conditions}</p>
</div>
);
}
function App() {
return (
<PancakeProvider>
<WeatherView />
</PancakeProvider>
);
}
createRoot(document.getElementById('root')!).render(<App />);Create src/views/weather/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Weather</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>Create src/views/weather.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Weather</title>
<style>
body { font-family: system-ui; padding: 2rem; }
.temp { font-size: 3rem; margin: 1rem 0; }
</style>
</head>
<body>
<h1 id="city">Loading...</h1>
<p class="temp" id="temp"></p>
<p id="conditions"></p>
<script type="module">
const waitForPancake = () => new Promise((resolve) => {
if (window.pancake) return resolve(window.pancake);
const check = setInterval(() => {
if (window.pancake) {
clearInterval(check);
resolve(window.pancake);
}
}, 50);
});
const pancake = await waitForPancake();
const data = pancake.adaptor.getToolOutput();
document.getElementById('city').textContent = data?.city || 'Unknown';
document.getElementById('temp').textContent = `${data?.temp || '--'}°`;
document.getElementById('conditions').textContent = data?.conditions || '';
</script>
</body>
</html>Update your view definition:
ui: { html: './src/views/weather.html' }Actions: Write Operations
Actions handle mutations and side effects. Unlike views, they don't render UI.
import { defineAction } from '@pancake-apps/server';
const app = createApp({
name: 'my-app',
version: '0.1.0',
actions: {
submitFeedback: defineAction({
description: 'Submit user feedback',
input: z.object({
rating: z.number().min(1).max(5),
comment: z.string().optional(),
}),
output: z.object({
success: z.boolean(),
id: z.string(),
}),
handler: async ({ rating, comment }) => {
const id = await saveFeedback({ rating, comment });
return { success: true, id };
},
}),
},
views: { /* ... */ },
});Calling Actions from Views
In React views, use the useAction hook:
import { useAction } from '@pancake-apps/web';
function FeedbackForm() {
const { dispatch } = useAction();
const [rating, setRating] = useState(5);
const handleSubmit = async () => {
const result = await dispatch<{ success: boolean; id: string }>(
'submitFeedback',
{ rating, comment: 'Great app!' }
);
console.log('Submitted:', result.id);
};
return (
<div>
<input
type="range"
min="1"
max="5"
value={rating}
onChange={(e) => setRating(Number(e.target.value))}
/>
<button onClick={handleSubmit}>Submit</button>
</div>
);
}In vanilla HTML, use pancake.unified.action():
const result = await pancake.unified.action('submitFeedback', {
rating: 5,
comment: 'Great app!',
});Data: Read Operations
Data endpoints are for queries and lookups. They return structured data without UI.
import { defineData } from '@pancake-apps/server';
const app = createApp({
name: 'my-app',
version: '0.1.0',
data: {
searchProducts: defineData({
description: 'Search the product catalog',
input: z.object({
query: z.string(),
category: z.string().optional(),
limit: z.number().default(10),
}),
output: z.array(z.object({
id: z.string(),
name: z.string(),
price: z.number(),
})),
handler: async ({ query, category, limit }) => {
return searchProducts({ query, category, limit });
},
}),
},
views: { /* ... */ },
actions: { /* ... */ },
});Fetching Data from Views
In React:
import { useData } from '@pancake-apps/web';
function ProductSearch() {
const { getData } = useData();
const [products, setProducts] = useState([]);
const handleSearch = async (query: string) => {
const results = await getData<Product[]>('searchProducts', { query });
setProducts(results);
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}In vanilla HTML:
const products = await pancake.unified.data('searchProducts', {
query: 'laptop',
limit: 20,
});View Discovery
Instead of defining views inline, you can auto-discover them from the filesystem:
import { createApp, discoverViews } from '@pancake-apps/server';
const app = createApp({
name: 'my-app',
version: '0.1.0',
views: discoverViews('./src/views'),
});Drop files in src/views/ and they become available:
src/views/
├── dashboard.html # → view: "dashboard"
├── dashboard.json # → metadata for dashboard
├── settings/
│ ├── index.html # → view: "settings"
│ └── metadata.json # → metadata for settings
└── weather.tsx # → view: "weather"Each view can have a metadata file (metadata.json or viewname.json):
{
"description": "User dashboard with analytics",
"visibility": "both"
}Configuration Options
const app = createApp({
name: 'my-app',
version: '0.1.0',
views: { /* ... */ },
actions: { /* ... */ },
data: { /* ... */ },
config: {
// Enable debug logging
debug: true,
// Target protocol (mcp, openai, or auto-detect)
protocol: 'both',
// Vite dev server port for hot reload
devServer: {
vitePort: 5173,
},
// Automatic tunneling
tunnel: {
provider: 'cloudflare', // or 'ngrok'
},
// CORS settings
cors: {
origin: ['https://example.com'],
credentials: true,
},
},
});React Hooks Reference
When using React views, these hooks are available:
| Hook | Returns | Description |
|---|---|---|
useViewParams<I, D>() | { inputs, data } | View inputs and server data |
useViewState(initial) | [state, setState] | Persistent state across re-renders |
useNavigation() | { navigate, say } | Navigate views and message the AI |
useAction() | { dispatch } | Call actions (write operations) |
useData() | { getData } | Fetch data (read operations) |
useTheme() | 'light' | 'dark' | Current theme from host |
useDisplayMode() | 'embedded' | 'standalone' | How the view is displayed |
Next Steps
You now understand the core concepts of Pancake apps. Explore further:
- Local Development: Configure tunnels and test with AI clients
- Building Widgets: Advanced UI patterns and interactions
- Deployment: Ship to production
- API Reference: Complete API documentation