MCP App framework. MCP testing framework. A local inspector with ChatGPT and Claude style hosts.
Build interactive apps that render inside ChatGPT, Claude and every other MCP Apps host, then test them in CI without host accounts, tunnels or AI credits.
npx moonpeak new my-app
cd my-app && npm install
npm run dev # inspector at http://localhost:3000, MCP server at /mcp
Open source, MIT licensed. Built on the official MCP Apps SDK and the MCP TypeScript SDK v2, so what works in the inspector works in real hosts.
Quickstart
npx moonpeak new my-app
cd my-app && npm install
npm run dev
Open http://localhost:3000. Edit src/resources/moon/moon.tsx and the View reloads. Edit src/tools/show-moon.ts and the next call uses the new code.
To try it in Claude or ChatGPT, run moonpeak build && moonpeak start, expose port 8000 over HTTPS, and add https://your-host/mcp as a custom connector (Claude) or app (ChatGPT developer mode).
What you get
| App framework | One file per tool, one folder per View. React hooks for tool data, theme, display mode, state, calling tools and posting messages. |
| Inspector | ChatGPT-style and Claude-style hosts on localhost. Switch host, theme, device width and display mode (inline, PiP, fullscreen). Edit tool input and output live. Every setting lives in the URL. |
| Testing | moonpeak test checks schemas, Views and simulations in-process. --e2e runs Playwright against the inspector. --eval checks real models pick the right tool. --server tests any MCP server in any language. |
| Production | moonpeak build inlines each View into one HTML file and bundles your tools. Serve with moonpeak start, or mount createHandler on Workers, Hono, Express, Deno or Bun. |
Project layout
Everything is discovered from the folders. No config file needed.
src/
tools/show-moon.ts one MCP tool per file
resources/moon/moon.tsx one View per folder (+ optional moon.css)
tests/
simulations/show-moon.json fixtures for the inspector and `moonpeak test`
e2e/moon.spec.ts Playwright tests (optional)
evals/moon.eval.json model evals (optional)
A tool
// 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 as the UI
};
export const schema = { date: z.string().optional() };
export default async function ({ date }: { date?: string }) {
return { date, phase: "Full Moon", illumination: 1 }; // becomes structuredContent
}
Return a plain object (structured content), a string (text), or a full CallToolResult. Throwing returns isError: true with your message. Leave out resource for a plain text tool.
A View
// src/resources/moon/moon.tsx
import { useToolData, useTheme, useCallTool, useRequestDisplayMode } from "moonpeak";
export const resource = { title: "Moon phase", prefersBorder: true };
export default function Moon() {
const { output, isPending } = useToolData<{ date?: string }, { phase: string }>();
const theme = useTheme();
if (isPending) return <p>Loading…</p>;
return <h1 className={theme}>{output?.phase}</h1>;
}
Hooks
Portable, from moonpeak (work in every host):
| Hook | Returns |
|---|---|
useToolData() |
{ input, output, content, isPending, isError, isStreaming, isCancelled, tool, result } |
useToolInput() · useToolOutput() · useToolResult() |
the parts on their own |
useTheme() |
"light" | "dark" |
useDisplayMode() · useRequestDisplayMode() |
current mode, and a function to ask for inline, pip or fullscreen |
useAppState(initial, { share }) |
one state object per View, persisted in ChatGPT, optionally shared with the model |
useResultState(initial) |
local state that resets on every new tool result |
useCallTool() |
call another tool on your server through the host |
useSendMessage() |
post a message into the chat as the user |
useUpdateModelContext() |
tell the model what happened without posting |
useOpenLink() · useDownloadFile() · useLog() |
host actions |
useHost() · useHostContext() · useHostCapabilities() · useConnection() · useApp() |
host info and the raw App |
useLocale() · useTimeZone() · usePlatform() · useDeviceCapabilities() · useSafeArea() · useContainerSize() · useMaxHeight() |
environment |
Host extras, safe to call anywhere: moonpeak/chatgpt (useWidgetState, useUploadFile, useRequestModal, useOpenAiGlobal, useIsChatGPT) and moonpeak/claude (useIsClaude, useClaudeStyles).
Simulations
[
{ "name": "full moon", "prompt": "Moon on Nov 24?", "input": { "date": "2026-11-24" },
"expect": { "output": { "phase": "Full Moon", "week": { "$length": 7 } } } },
{ "name": "mocked", "input": {}, "output": { "phase": "Waxing Crescent" }, "host": { "theme": "dark" } },
{ "name": "bad date", "input": { "date": "soon" }, "expect": { "isError": true, "text": "not a date" } }
]
output/resultmock the tool result in the inspector, so you can design a View without a backend.expectis checked bymoonpeak test: partial match onoutput,"re:..."for regexes,"*"for "any value",$length/$minLengthfor arrays.hostsets theme, display mode, device, locale or host for that scenario.
Testing
moonpeak test # schema + View + simulation checks, in-process, ~1s
moonpeak test --e2e # Playwright against the inspector (starts it for you)
moonpeak test --visual # screenshot tests tagged @visual (first run records baselines)
moonpeak test --visual --update # re-record baselines after an intended change
moonpeak test --eval # model evals, needs ANTHROPIC_API_KEY / OPENAI_API_KEY
moonpeak test init --server URL # write simulations for an existing server
moonpeak test --server URL # test any MCP server, any language
Run npx playwright install chromium once before the first e2e run. E2E tests get two fixtures:
import { test, expect } from "moonpeak/test";
test("renders in Claude dark mode", async ({ inspector }) => {
const view = await inspector.open({ tool: "show-moon", sim: "show-moon#full-moon", host: "claude", theme: "dark" });
await expect(view.getByRole("heading", { name: "Full Moon" })).toBeVisible();
});
test("tool output", async ({ mcp }) => {
expect(await mcp.call("show-moon", { date: "2026-11-24" })).toHaveStructuredContent({ phase: "Full Moon" });
});
Inspect any server
moonpeak inspect --server http://localhost:8000/mcp
moonpeak inspect --server https://api.example.com/mcp --header "authorization:Bearer $TOKEN"
Works with MCP servers written in Python, Go, Rust, Java or TypeScript. Views are loaded with resources/read and rendered in a sandboxed frame, the same way real hosts do it.
Deploy
moonpeak build && moonpeak start # http://localhost:8000/mcp
Or mount it yourself:
// Cloudflare Workers, Hono, Deno, Bun, Next.js route handlers
import { createHandler } from "moonpeak/server";
import * as built from "./dist/server.mjs";
// Express / node:http
import { createNodeHandler } from "moonpeak/server";
app.all("/mcp", await createNodeHandler(opts));
Then add the URL as a connector in Claude, or as an app in ChatGPT developer mode.
CLI
| Command | |
|---|---|
moonpeak new [dir] |
scaffold a project |
moonpeak dev |
MCP server + inspector with hot reload |
moonpeak build |
dist/resources/*.html + dist/server.mjs |
moonpeak start |
serve the build at /mcp |
moonpeak inspect --server <url> |
inspector for any MCP server |
moonpeak test [init] [--e2e] [--visual] [--eval] [--server <url>] |
tests |
moonpeak upgrade |
update moonpeak |
License
MIT