MCP App framework · ChatGPT Apps · Claude connectors

The MCP App framework.

Write an MCP tool and the UI it shows once. moonpeak renders it in ChatGPT, Claude, VS Code, Goose and every MCP Apps host, from the same code.

Documentation Source

Why use an MCP App framework?

An MCP App is an MCP tool plus a ui:// resource the host renders next to the conversation. Doing that by hand means a server, a bundler that inlines everything into one HTML file, host metadata for every client and a fake host to look at it. moonpeak does all of it from two folders.

  • Folders are the config: tools, Views and fixtures are discovered.
  • Return a plain object and it becomes structuredContent; throw and it becomes a tool error.
  • Each View builds to one self-contained HTML file, ready for strict CSPs.
  • Resources carry MCP Apps metadata and the older openai/* keys, so ChatGPT renders them too.
  • Hot reload for Views and tools while the inspector stays open.
my-app
my-app/
├─ src/
│  ├─ tools/show-moon.ts        # one MCP tool
│  ├─ resources/moon/moon.tsx   # its View
│  └─ lib/
└─ tests/
   ├─ simulations/show-moon.json
   ├─ e2e/moon.spec.ts
   └─ evals/moon.eval.json
src/tools/show-moon.ts
import { z } from "zod";

export const tool = {
  title: "Show moon phase",
  description: "Show the moon phase for a date.",
  resource: "moon",             // render src/resources/moon
};

export const schema = { date: z.string().optional() };

export default async function ({ date }) {
  return { date, phase: "Full Moon", illumination: 1 };
}

A tool is one file.

Export the tool's metadata, a zod schema and a handler. Point resource at a View folder to give it a UI, or leave it out for a text tool. Mark helper tools visibility: ["app"] and only your View can call them; the model never sees them.

A View is one React component.

Twenty-seven typed hooks cover everything a View needs: the tool's input and output, theme, locale, safe areas, container size, display mode requests, per-View state that survives reloads, calling other tools, posting into the chat and updating model context.

Every hook →
src/resources/moon/moon.tsx
import { useToolData, useTheme, useCallTool } from "moonpeak";

export default function Moon() {
  const { output, isPending } = useToolData();
  const theme = useTheme();
  const callTool = useCallTool();
  if (isPending) return <p>Reading the sky…</p>;
  return (
    <div className={theme}>
      <h1>{output.phase}</h1>
      <button onClick={() => callTool("show-moon", next)}>Next week</button>
    </div>
  );
}

Start from a pattern

npx moonpeak new asks which resources to start with. Each one is a working View with simulations, e2e tests and evals.

◐

moon

Detail card. A date's moon phase with a 7-day strip; buttons call the tool again through the host.

◑

calendar

Carousel. Swipeable cards of the upcoming phases; "load more" pages through the host, selection is shared with the model.

◒

review

Review and confirm. The model proposes a stargazing night, the user confirms it through an app-only tool.

npx moonpeak new my-app calendar,reviewpick resources up front
deploy
moonpeak build      # dist/resources/*.html + dist/server.mjs
moonpeak start      # http://localhost:8000/mcp

# or mount it yourself (Workers, Hono, Deno, Bun, Next.js)
import { createHandler } from "moonpeak/server";

Ship it anywhere.

moonpeak build inlines every View and bundles your tools. Serve the result with moonpeak start, or mount createHandler on any runtime that speaks Fetch, or createNodeHandler on Express and node:http. Then add the HTTPS URL as a Claude connector or a ChatGPT app.

Deploy docs →

Open source, MIT licensed.

Documentation → GitHub
Copied