🥞PancakeJS

MCP Apps Architecture

Understanding how MCP (Model Context Protocol) apps work helps you build better integrations. This guide explains the architecture behind Pancake apps.

Core Components

A Pancake app has two main parts:

1. MCP Server

The server exposes tools that AI assistants can call. In Pancake, views, actions, and data endpoints are all exposed as tools.

import { createApp, defineView } from '@pancake-apps/server';

const app = createApp({
  name: 'my-app',
  version: '1.0.0',

  views: {
    search: defineView({
      description: 'Search for items',
      input: z.object({ query: z.string() }),
      handler: async ({ query }) => searchItems(query),
      ui: { html: './src/views/search.html' },
    }),
  },
});

app.start({ port: 3000 });

The server exposes:

  • POST /mcp - MCP JSON-RPC endpoint
  • GET /openapi.json - OpenAPI spec for ChatGPT
  • GET /.well-known/openai-plugin.json - OpenAI plugin manifest

2. View UI

Views are web components that render inside the AI host. They run in a sandboxed iframe and communicate with the host via postMessage.

function SearchView() {
  const { data } = useViewParams();

  return (
    <div>
      {data.items.map(item => (
        <ItemCard key={item.id} item={item} />
      ))}
    </div>
  );
}

Request Flow

Here's what happens when a user interacts with your app:

User Expresses Intent

The user types something like "Search for laptops under $1000" in Claude or ChatGPT.

AI Selects Your Tool

The AI analyzes the request and determines your search tool matches. It extracts parameters from the natural language.

MCP Server Executes

The AI sends a tool call to your server:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {
      "query": "laptops under $1000"
    }
  }
}

Handler Runs

Your handler fetches data and returns it with a UI reference:

handler: async ({ query }) => {
  const items = await searchItems(query);
  return { items };
}

Host Retrieves View

The AI host requests the view HTML:

{
  "method": "resources/read",
  "params": {
    "uri": "pancake://ui/view/search"
  }
}

View Renders

The host loads your view into a sandboxed iframe. The view receives the data from your handler.

Protocol Comparison

Pancake supports multiple protocols. Here's how they differ:

AspectMCP (Claude)OpenAI Actions (ChatGPT)
ProtocolJSON-RPC 2.0REST API
CommunicationpostMessagewindow.openai API
Tool Discoverytools/listOpenAPI spec
View Loadingresources/readOutput templates

Pancake handles these differences automatically. Your code stays the same.

Security Model

Views run in a sandboxed environment:

Security Model

This architecture provides:

  • Isolation: Your code can't access the host directly
  • Controlled communication: Only postMessage, no direct API access
  • CSP policies: Network requests restricted to declared domains

View Lifecycle

1. INITIALIZE
   └─→ Host creates iframe
   └─→ View script loads
   └─→ PancakeProvider establishes connection

2. HYDRATE
   └─→ View receives tool input/output data
   └─→ Initial render with server data

3. INTERACTIVE
   └─→ User interacts with view
   └─→ View calls actions, fetches data
   └─→ View may message the AI

4. CLEANUP
   └─→ Host notifies view of destruction
   └─→ Cleanup handlers run
   └─→ Iframe removed

Always wrap your view with PancakeProvider. It handles connection setup, data hydration, and lifecycle management.

MCP vs OpenAI Comparison

MCP uses JSON-RPC 2.0 over postMessage:

// Tool call
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": { "name": "search", "arguments": { "query": "test" } }
}

// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": { "items": [...] }
}

ChatGPT uses the window.openai API:

// Access tool output
const data = window.openai.toolOutput;

// Call another tool
await window.openai.callTool('search', { query: 'test' });

// Send follow-up message
window.openai.sendFollowUpMessage({ prompt: 'Show me more' });

Why Pancake Abstracts This

Without abstraction:

// Platform-specific code
if (typeof window.openai !== 'undefined') {
  const data = window.openai.toolOutput;
  window.openai.callTool('book', { id: itemId });
} else {
  const data = await waitForMcpData();
  postMessage({ method: 'tools/call', ... });
}

With Pancake:

// Universal code
const { data } = useViewParams();
const { dispatch } = useAction();
await dispatch('book', { id: itemId });

Next Steps

On this page