Examples
Learn PancakeJS through working examples. Clone the repo and explore these patterns.
Example Projects
Basic Widget
Interactive widget with state, tool calls, and follow-ups
Basic Tools
Tools-only server without UI components
Basic Widget Example
A complete widget demonstrating all core features:
Server Setup
// server.ts
import { z } from 'zod';
import { createUniversalServerApp, textResult, renderWidget } from '@pancakeapps/server';
import { createMcpAppsServerAdapter } from '@pancakeapps/adapter-mcp-apps';
import { createChatGptServerAdapter } from '@pancakeapps/adapter-chatgpt-apps';
const app = createUniversalServerApp({
name: 'basic-widget',
version: '1.0.0',
});
// Simple text tool
app.tool(
{
name: 'greet',
description: 'Greet someone by name',
inputSchema: z.object({
name: z.string(),
}),
},
async ({ name }) => textResult(`Hello, ${name}! 🥞`)
);
// Widget with initial data
app.widget(
{
name: 'hello-widget',
description: 'A friendly greeting widget',
inputSchema: z.object({
name: z.string(),
}),
ui: {
entry: 'src/widgets/hello.tsx',
},
},
async ({ name }) => {
const items = await fetchInitialItems();
return renderWidget('hello-widget', {
data: {
greeting: `Hello, ${name}!`,
items
},
});
}
);
// Tool that widgets can call
app.tool(
{
name: 'fetchItems',
description: 'Fetch more items',
inputSchema: z.object({
page: z.number().default(1),
}),
},
async ({ page }) => {
const items = await fetchItems(page);
return withData(
textResult(`Fetched ${items.length} items`),
{ items, page }
);
}
);
app.adapter(createMcpAppsServerAdapter());
app.adapter(createChatGptServerAdapter());
app.listen({ port: 3000, dev: true });Widget Component
// src/widgets/hello.tsx
import React, { useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import {
UniversalAppProvider,
useToolInvocation,
useCallTool,
useWidgetState,
useHost,
useFollowUp,
} from '@pancakeapps/react';
interface Item {
id: string;
name: string;
status: 'active' | 'pending';
}
function HelloWidget() {
// Get initial data
const { data } = useToolInvocation<{
greeting: string;
items: Item[];
}>();
// Persist state
const [items, setItems] = useWidgetState('items', data?.items ?? []);
const [loading, setLoading] = useWidgetState('loading', false);
// Call other tools
const fetchItems = useCallTool('fetchItems');
// Access host info
const host = useHost();
const followUp = useFollowUp();
const handleLoadMore = async () => {
setLoading(true);
try {
const result = await fetchItems({ page: Math.ceil(items.length / 10) + 1 });
setItems([...items, ...result.data.items]);
} finally {
setLoading(false);
}
};
const handleAskMore = () => {
if (followUp.supported) {
followUp.send('Tell me more about these items');
}
};
return (
<div style={{
padding: '1.5rem',
background: host.context.theme === 'dark'
? 'linear-gradient(135deg, #1C1917 0%, #292524 100%)'
: 'linear-gradient(135deg, #FFFBEB 0%, #FEF3C7 100%)',
borderRadius: '12px',
fontFamily: 'system-ui',
color: host.context.theme === 'dark' ? '#FAFAF9' : '#1C1917',
}}>
<h1 style={{
margin: 0,
background: 'linear-gradient(135deg, #FCD34D 0%, #D97706 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
{data?.greeting ?? 'Hello!'}
</h1>
<div style={{ marginTop: '1rem' }}>
{items.map(item => (
<div key={item.id} style={{
padding: '0.75rem',
marginBottom: '0.5rem',
background: 'rgba(255,255,255,0.05)',
borderRadius: '8px',
display: 'flex',
justifyContent: 'space-between',
}}>
<span>{item.name}</span>
<span style={{
padding: '0.25rem 0.5rem',
borderRadius: '4px',
fontSize: '0.75rem',
background: item.status === 'active'
? 'rgba(52, 211, 153, 0.2)'
: 'rgba(251, 191, 36, 0.2)',
color: item.status === 'active' ? '#34D399' : '#FBBF24',
}}>
{item.status}
</span>
</div>
))}
</div>
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem' }}>
<button
onClick={handleLoadMore}
disabled={loading}
style={{
padding: '0.5rem 1rem',
background: '#D97706',
color: '#1C1917',
border: 'none',
borderRadius: '6px',
cursor: loading ? 'wait' : 'pointer',
fontWeight: 500,
}}
>
{loading ? 'Loading...' : 'Load More'}
</button>
{followUp.supported && (
<button
onClick={handleAskMore}
style={{
padding: '0.5rem 1rem',
background: 'transparent',
color: '#D97706',
border: '1px solid #D97706',
borderRadius: '6px',
cursor: 'pointer',
}}
>
Ask AI
</button>
)}
</div>
</div>
);
}
function App() {
return (
<UniversalAppProvider fallback={<div>Loading...</div>}>
<HelloWidget />
</UniversalAppProvider>
);
}
createRoot(document.getElementById('root')!).render(<App />);Basic Tools Example
Tools-only server without widgets:
// server.ts
import { z } from 'zod';
import { createUniversalServerApp, textResult, withData } from '@pancakeapps/server';
import { createMcpAppsServerAdapter } from '@pancakeapps/adapter-mcp-apps';
const app = createUniversalServerApp({
name: 'basic-tools',
version: '1.0.0',
});
// Calculator tool
app.tool(
{
name: 'calculate',
description: 'Perform basic math operations',
inputSchema: z.object({
operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
a: z.number(),
b: z.number(),
}),
},
async ({ operation, a, b }) => {
let result: number;
switch (operation) {
case 'add': result = a + b; break;
case 'subtract': result = a - b; break;
case 'multiply': result = a * b; break;
case 'divide': result = a / b; break;
}
return withData(
textResult(`${a} ${operation} ${b} = ${result}`),
{ result }
);
}
);
// Weather tool (mock)
app.tool(
{
name: 'getWeather',
description: 'Get current weather for a city',
inputSchema: z.object({
city: z.string().describe('City name'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
},
async ({ city, units }) => {
// Mock weather data
const temp = Math.floor(Math.random() * 30) + 10;
const conditions = ['Sunny', 'Cloudy', 'Rainy', 'Windy'][Math.floor(Math.random() * 4)];
const displayTemp = units === 'fahrenheit'
? Math.floor(temp * 9/5 + 32)
: temp;
const unit = units === 'fahrenheit' ? '°F' : '°C';
return withData(
textResult(`${city}: ${displayTemp}${unit}, ${conditions}`),
{ city, temp: displayTemp, unit, conditions }
);
}
);
// Random fact tool
app.tool(
{
name: 'getRandomFact',
description: 'Get a random interesting fact',
inputSchema: z.object({}),
},
async () => {
const facts = [
'Honey never spoils.',
'Octopuses have three hearts.',
'A group of flamingos is called a flamboyance.',
'Bananas are berries, but strawberries are not.',
];
const fact = facts[Math.floor(Math.random() * facts.length)];
return textResult(`🧠 Did you know? ${fact}`);
}
);
app.adapter(createMcpAppsServerAdapter());
app.listen({ port: 3001, dev: true });Running Examples
# Clone the repo
git clone https://github.com/pancakeapps/pancake.git
cd pancake
# Install dependencies
pnpm install
# Run basic widget example
cd examples/basic-widget
pnpm dev
# Open http://localhost:3000/_inspector
# Run basic tools example
cd examples/basic-tools
pnpm dev
# Open http://localhost:3001/_inspectorTesting Tips
Use the built-in inspector to test your tools and widgets in both MCP and ChatGPT environments.
- Open the inspector at
http://localhost:PORT/_inspector - Select a tool or widget from the list
- Switch between "MCP Harness" and "ChatGPT Harness" tabs
- The same code runs in both environments!
Patterns
Loading States
const [loading, setLoading] = useWidgetState('loading', false);
const handleAction = async () => {
setLoading(true);
try {
await doSomething();
} finally {
setLoading(false);
}
};Error Handling
const [error, setError] = useWidgetState<string | null>('error', null);
const handleAction = async () => {
try {
await doSomething();
setError(null);
} catch (e) {
setError(e.message);
}
};
if (error) {
return <ErrorMessage message={error} onRetry={handleAction} />;
}Capability Checks
const host = useHost();
const followUp = useFollowUp();
return (
<div>
<MainContent />
{followUp.supported && (
<button onClick={() => followUp.send('Continue')}>
Ask More
</button>
)}
{!followUp.supported && (
<CopyButton text={response} />
)}
</div>
);Next Steps
- Getting Started — Build your own app
- Tools vs Widgets — Understand the concepts
- API Reference — Detailed API docs