🥞PancakeJS

Examples

Learn PancakeJS through working examples. Clone the repo and explore these patterns.

Example Projects

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/_inspector

Testing Tips

Use the built-in inspector to test your tools and widgets in both MCP and ChatGPT environments.

  1. Open the inspector at http://localhost:PORT/_inspector
  2. Select a tool or widget from the list
  3. Switch between "MCP Harness" and "ChatGPT Harness" tabs
  4. 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

On this page