Note that "sunpeak", except where required in URLs or code, is always lowercase.
sunpeak is a framework for building MCP Apps with interactive UIs that run inside AI chat hosts (ChatGPT, Claude, and future major hosts). Built on top of the MCP Apps SDK (@modelcontextprotocol/ext-apps).
The value proposition of the sunpeak framework is to help developers and their agents:
- Test MCP Apps locally and automatically (in CI/CD) using a replica of the ChatGPT and Claude runtimes.
- Save time manually testing all possible host, server, app, ui, and backend states.
- Protect developers from 4-click manual refreshes on every code change in each host.
- Cancel all the $20 per person per host per month testing accounts.
- Avoid burning host credits on every test and code change.
- Verify MCP tools work across multiple LLM models via the eval framework. Evals connect to the MCP server, discover tools, and send prompts to multiple models (GPT-4o, GPT-4o-mini, o4-mini, Claude Sonnet, Gemini 2.0 Flash) via the Vercel AI SDK. Cases can include App Context for follow-up turns that depend on model-visible UI state. Each case runs N times per model and reports pass/fail counts, so developers can confirm that tool descriptions, schemas, and App Context work reliably on smaller and cheaper models. Opt-in via
sunpeak test --evalbecause evals cost money. - Automate the real-host testing loop with live tests (
sunpeak test --live): scripts that drive a real browser into ChatGPT (and other hosts as they're supported), send prompts that trigger MCP tool calls against the developer's server, and assert against the actually-rendered app via Playwright. Live tests catch what the inspector can't (real MCP connection behavior, real LLM tool invocation, host-specific iframe rendering, production resource loading) and replace the manual prompt-and-click loop. Opt-in because they hit real accounts. - Build multi-platform MCP Apps in a structured way that's easy to understand and get started.
- Test their MCPs in ChatGPT with HMR and Claude with automatic rebuilds and refresh notifications.
- Embed the Inspector as a React component inside any third-party React app to render arbitrary MCP Apps. The
<Inspector app={...} onCallTool={...} />shape exposes the same double-iframe runtime, host shells, and conversation chrome as the CLI inspector, but without any sunpeak-owned servers or/__sunpeak/*runtime dependencies. Tool calls flow through a callback the embedder owns; resource HTML is passed as a string. Embedders host the static sandbox proxy (dist/sandbox-proxy.html) on a separate origin from their own app — or fall back to the same-originsrcdocvariant for zero deployment. This is a first-class use case; treat it as load-bearing when designing changes.
Maintain these properties when changing the Inspector or its supporting code:
- Public hierarchical API: the
appprop on<Inspector />is the public input shape for embedders (InspectorApp→ resources + tools → simulations). Flatten internally; never require embedders to learn the legacy flatsimulationsmap. - No
/__sunpeak/*runtime dependencies in the React component: the bundled Inspector must not assume any sunpeak-owned HTTP endpoints exist in the embedder's origin. Health checks, tool calls, simulation discovery, and OAuth all flow through props (onCallTool,app.resources[].html) or are gated off in embedded mode. - Two CSS entries — keep them aligned:
sunpeak/style.cssships full Tailwind preflight (used by resources loading into iframe documents —pnpm devand the CLI / template path).sunpeak/embed.cssships the same Tailwind utilities + theme, but with preflight scoped under.sunpeak-inspector-root(used by embedders hosting the Inspector inside another React app). When changing the scoped preflight rules, update both files —src/inspector/globals.css(for style.css, full preflight via@import "tailwindcss") andsrc/embed.css(scoped preflight, theme + utilities only). Embed mode invariant: the host page's<button>,<input>, typography, anddata-themeattribute must be untouched. The one acknowledged document-level injection is host@font-face(inert to host pages). - Theme + host-context application stays root-scoped: variables, color-scheme,
data-themego on the Inspector's root ref, notdocument.documentElement. TheThemeProviderdefault still targets the document for callers using it outside the bundled Inspector — the Inspector overrides with a no-op. - Sizing follows the parent in embedded mode: when
appis provided, the root usesh-full w-full(fills the container the embedder gives it) instead ofh-screen w-screen(fills the viewport). Don't reintroduce viewport-relative sizing in the embedded path. - Sandbox proxy is delivered three ways: hosted static file (
dist/sandbox-proxy.html, ~7KB, runtime-configurable via URL params), CLI dev server (startSandboxServer), andsrcdocfallback (same-origin, lower fidelity). All three must stay behaviorally identical — seesrc/inspector/sandbox-static.test.tsfor the drift guard. - Mutually exclusive props:
appandsimulationsare mutually exclusive;appwins and emits a one-timeconsole.warn. Don't add a third way in. - Simulation
namevsdisplayName:Simulation.nameis the unique internal key (<toolName>__<userSimName>for the embed path);Simulation.displayNameis the user-facing sidebar label (the embedder's rawsim.name). Don't collapse them —nameis what URL deep-links and state indexing rely on;displayNameis purely cosmetic. Sidebar code falls back tonamewhendisplayNameis absent. - Backend-only tools are first-class: tools without UI resources stay visible in the tool picker and can be called from the sidebar or model chat. Show
Tool does not render a UIonly when a backend-only tool is selected and no model conversation is rendering; once a model conversation starts, show the transcript and tool-call metadata. If the model later calls a UI tool, render that UI as usual. - Security invariants (don't regress these):
- The inline helper script (
src/inspector/inline-helper-script.ts) must keep itsev.source === window.parentguard on incoming messages. Sibling iframes and extension content scripts could otherwise forgeui/notifications/tool-resultand drive the embedder's callbacks with attacker-controlled data. sandboxUrlvalidation rejects non-http(s) URLs — match this precedent if adding any new URL-accepting prop.- The Inspector does no sanitization of
app.resources[].html. Don't add accidental innerHTML paths that read from resources, tool results, or other embedder-supplied strings — React's text-node escaping is the only XSS defense. - Tool result JSON is escaped (
<→<) when injected into the<script id="__tool-result">data island. Don't switch to a less-escaped serialization. - The mock
openExternalruntime inmock-openai-runtime.tsrejects non-http(s) URLs and usesnoopener,noreferrer. Preserve both when changing the runtime. - The
srcdocfallback is documented as not safe for untrusted HTML (runs in the host's origin viaallow-same-origin+ same-origin srcdoc). Keep the separate-origin sandbox file as the recommended production path.
- The inline helper script (
- Data-source agnostic: the same
app+onCallToolAPI covers three patterns and the Inspector renders identically in all of them. (a) Live MCP server —app.resources[].htmlcomes frommcpClient.readResource(...)andonCallToolforwards tomcpClient.callTool(...). (b) Static / no-server — HTML is a string literal andonCallToolreturns canned responses (or simulations carry pre-cannedtoolResults so it's never called). (c) Hybrid — live MCP discovery plus saved simulations per tool, letting users flip between "None" (live call) and saved states (canned result). Don't add branches that assume one mode over the others; the Inspector should never know which it's in. Embedders proxying live MCP through their own backend (typical:/api/mcp/...) is the embedder's concern, not sunpeak's — the React component never makes outbound MCP calls itself.
pnpm --filter sunpeak test -- --run # Unit tests (vitest)
pnpm --filter sunpeak lint # ESLint
pnpm --filter sunpeak typecheck # tsc --noEmit
pnpm --filter sunpeak build # Vite build
pnpm --filter sunpeak validate # Full validation (lint + build + test + examples)
pnpm --filter sunpeak generate-examples # Regenerate examples/ from templateAll resource content renders inside iframes — never directly in the host page. This matches how AI chat hosts (ChatGPT, Claude) display apps and enables direct re-export of SDK hooks.
The inspector supports multiple host platforms via a HostShell abstraction. Each host provides:
- Conversation chrome — the visual shell (message bubbles, headers, input areas)
- Theme — host-specific CSS variables and theme application
- Host info & capabilities — reported to the app via MCP protocol
Switching hosts in the sidebar changes the conversation chrome, theming, and reported host info/capabilities. The sidebar controls, iframe infrastructure, and state management are shared.
Inspector(host page) →HostShell.Conversation→IframeResourceIframeResourcecreates an outer<iframe>containing a sandbox proxy that relays PostMessage between the host and an inner iframe holding the actual app. This two-level architecture matches how production hosts (ChatGPT, Claude) isolate app iframes on a separate origin (e.g.,web-sandbox.oaiusercontent.com).- Outer iframe: Loads the sandbox proxy from a separate-origin server (port 24680) or via
srcdoc(fallback for unit tests). - Inner iframe: Created by the proxy, loads the app HTML via
src(dev: Vite HMR URL) ordocument.write()(prod: generated HTML).
- Outer iframe: Loads the sandbox proxy from a separate-origin server (port 24680) or via
McpAppHostwraps the SDK'sAppBridgefor host-side communication. Messages flow: host ↔ outer iframe (proxy) ↔ inner iframe (app), all via PostMessage relay.- Inside the inner iframe, the resource component uses
useApp()which connects viaPostMessageTransporttowindow.parent(the proxy), which relays to the host.
The cross-origin relationship between iframes is intentional and must be preserved. The outer iframe (sandbox proxy, port 24680), inner iframe (resource HTML, proxied through the inspector on port 3000), and the Vite dev server (port 8000) are deliberately on different origins. This replicates production CSP behavior where the host, sandbox, and app content live on separate origins. Do not "fix" cross-origin issues by collapsing these onto the same origin — that would make the inspector less representative of production and mask real CSP bugs.
Safari is incompatible with sunpeak dev. Safari upgrades cross-origin HTTP requests to HTTPS, which breaks the multi-port localhost architecture. This is a known limitation with no workaround. Use Chrome for development. Production deploys (sunpeak start) work in all browsers because the app is a single bundled page without cross-origin localhost dependencies.
Tests use page.frameLocator('iframe').frameLocator('iframe') to access resource content inside the double-iframe. Elements on the inspector chrome (header, #root) use page.locator() directly. Console error tests filter expected MCP handshake errors.
Automated tests against real ChatGPT using Playwright. Uses the same ChatGPTPage class for selectors, message sending, and iframe handling. Auth flow: saved session → manual login in the opened browser window. Sessions typically last only a few hours because Cloudflare's HttpOnly cf_clearance cookie cannot be persisted by storageState(). The global-setup.mjs handles auth + MCP server refresh in the same browser session (refresh must happen before the browser closes while cf_clearance is still valid).
packages/sunpeak/
├── src/
│ ├── index.ts # Main barrel: SDK re-exports + hooks + types
│ ├── inspector/ # Generic multi-host inspector core
│ │ ├── inspector.tsx # Inspector component (host picker, sidebar, delegates to shell)
│ │ ├── use-inspector-state.ts # All inspector state management
│ │ ├── hosts.ts # HostShell interface + registry
│ │ ├── mcp-app-host.ts # MCP Apps bridge wrapper (generic, supports streaming partials)
│ │ ├── iframe-resource.tsx # Iframe rendering + double-iframe sandbox proxy
│ │ ├── sandbox-proxy.ts # Sandbox proxy HTML generation (srcdoc fallback)
│ │ ├── simple-sidebar.tsx # Dev control panel
│ │ └── theme-provider.tsx # Pluggable theme provider
│ ├── chatgpt/ # ChatGPT host shell
│ │ ├── chatgpt-conversation.tsx # ChatGPT conversation chrome
│ │ └── chatgpt-host.ts # Host registration (theme, capabilities)
│ ├── claude/ # Claude host shell
│ │ ├── claude-conversation.tsx # Claude conversation chrome
│ │ └── claude-host.ts # Host registration (theme, capabilities)
│ ├── hooks/ # React hooks (useApp, useHostContext, useToolData, useAppState, useUpdateModelContext, useAppTools, useCreateSamplingMessage, useRegisterTool, useSendToolListChanged, etc.)
│ ├── mcp/ # MCP server (runMCPServer, production-server, resource registration)
│ ├── host/ # Host detection (detectHost, isChatGPT, isClaude)
│ │ └── chatgpt/ # ChatGPT-specific: useUploadFile, useRequestModal, useRequestCheckout
│ ├── eval/ # Eval framework tests (implementation in bin/lib/eval/)
│ ├── lib/ # Utilities (discovery, cn(), media queries)
│ ├── types/ # Type definitions (Simulation, runtime types)
│ └── cli/ # CLI commands
├── template/ # Scaffolded app template (also a workspace package)
│ ├── src/resources/ # Example resource components (albums, carousel, map, review)
│ ├── src/tools/ # Tool files with handlers and metadata
│ ├── src/server.ts # Optional server entry (auth, config)
│ └── tests/ # Unit tests, E2E tests, simulations, live tests, evals
└── scripts/
├── validate.mjs # Full validation pipeline
└── generate-examples.mjs # Generate examples/ from template resources
sunpeak— Hooks, types, SDK re-exports (App,RESOURCE_MIME_TYPE,LATEST_PROTOCOL_VERSION, etc.),inspector+chatgptnamespacessunpeak/inspector— Generic Inspector, host shell system, infrastructuresunpeak/chatgpt— ChatGPT host shell registration + Inspector re-exportsunpeak/claude— Claude host shell registration + Inspector re-exportsunpeak/mcp— Server utilities (runMCPServer,createMcpHandler,createHandler,createProductionMcpServer,startProductionHttpServer,setJsonLogging,detectClientFromHeaders), tool types (AppToolConfig,ToolHandlerExtra,CallToolResult,AuthInfo), server config (ServerConfig,MCPServerConfig,MCPServerHandle), production types (ProductionTool,ProductionResource,ProductionServerConfig,HttpServerOptions,AuthFunction,WebAuthFunction,WebHandlerConfig), domain resolution (resolveDomain,computeClaudeDomain,computeChatGPTDomain,injectResolvedDomain,injectDefaultDomain,DomainConfig), favicon (FAVICON_BASE64,FAVICON_DATA_URI,FAVICON_BUFFER), SDK server helpers (registerAppTool,registerAppResource,getUiCapability,EXTENSION_ID,RESOURCE_URI_META_KEY,RESOURCE_MIME_TYPE,McpUiAppToolConfig,McpUiAppResourceConfig,ToolConfig,ToolCallback,ReadResourceCallback,ResourceMetadata)sunpeak/host— Host detectionsunpeak/host/chatgpt— ChatGPT-specific hooks (file upload, modals, checkout)sunpeak/test— MCP-first Playwright fixtures (testwithmcpfixture for protocol methods:callTool(name, input?),listTools(),listResources(),readResource(uri); andinspectorfixture for rendering:renderTool(name, input?, options?)returningInspectorResultwithapp(),source,screenshot(); plusinspector.host,inspector.page;expectwith MCP-native matchers)sunpeak/test/config— Playwright config factory (defineConfig— auto-detects sunpeak projects, or acceptsserveroption for external MCP servers;serversupportscommand,args,url,env,cwd; top-leveltimeoutfor server startup;visualoption for visual regression config)sunpeak/test/live— Host-agnostic Playwright fixtures for live testing (testwithlivefixture,expect,setColorScheme)sunpeak/test/live/config— Live test config factory (defineLiveConfigwithhostsarray and optionalserverfor external MCP servers)sunpeak/test/live/chatgpt— ChatGPT-specific Playwright fixtures (testwithchatgptfixture)sunpeak/eval— Eval framework (defineEval,defineEvalConfig,checkExpectations,createMcpConnection,discoverAndConvertTools,normalizeEvalAppContext,formatEvalAppContextForModel,runEvalCaseAggregate)sunpeak/test/live/chatgpt/config— ChatGPT-specific Playwright config factorysunpeak/test/inspect/config— Inspect config factory for external MCP servers (defineInspectConfig; supportsenv,cwd,timeout,visualoptions)sunpeak/inspect— Programmatic inspector server (inspectServer— start the inspector from code instead of CLI; acceptsserver,port,env,cwd, etc.)sunpeak/style.css— Full-preflight stylesheet for resources rendered inside iframe documents (CLI dev / template path)sunpeak/embed.css— Scoped-preflight stylesheet for embedders hosting the Inspector inside their own React app
// Tool file export (src/tools/{name}.ts)
interface AppToolConfig extends ToolConfig {
resource?: string; // Resource name (derived from directory: src/resources/{name}/). Omit for tools without a UI.
}
// Simulation fixture (tests/simulations/*.json)
interface SimulationJson {
tool: string; // References tool filename (e.g., "show-albums")
userMessage?: string;
toolInput?: Record<string, unknown>;
toolResult?: { structuredContent?: unknown };
serverTools?: Record<string, ServerToolMock>; // Mock responses for callServerTool calls
}
// ServerToolMock: simple (single result) or conditional (when/result array)
type ServerToolMock =
| CallToolResult
| Array<{ when: Record<string, unknown>; result: CallToolResult }>;
// Internal simulation (dev server runtime)
interface Simulation {
name: string;
resourceUrl?: string; // Dev: HTML page URL (Vite HMR)
resourceScript?: string; // Prod: JS bundle URL
tool: Tool;
resource?: Resource; // Undefined for tools without a UI
toolInput?: Record<string, unknown>;
toolResult?: { content?: [...]; structuredContent?: unknown };
serverTools?: Record<string, ServerToolMock>; // Mock responses for callServerTool
}
interface HostShell {
id: string; // 'chatgpt' | 'claude'
label: string; // Display name in sidebar
Conversation: ComponentType<HostConversationProps>;
applyTheme: (theme: 'light' | 'dark') => void;
hostInfo: { name: string; version: string };
hostCapabilities: McpUiHostCapabilities;
userAgent?: string; // e.g. 'chatgpt', 'claude'
}sunpeak dev starts the local MCP server (with Vite HMR for resources) and then launches the inspector pointed at it. This means sunpeak dev and sunpeak inspect share the same inspector UI codepath — the inspector is the single entry point for all inspector use cases.
Architecture:
- MCP server — Started via
runMCPServer()withviteMode: true. Serves tools, resources (with Vite HMR scripts inreadResourceHTML), and simulation data via a customsunpeak/simulationsMCP method. - Inspector —
inspectServer()frominspect.mjsconnects to the MCP server URL, discovers tools/resources via MCP protocol, and serves the inspector UI with HMR. - sandboxServer — A minimal HTTP server on a separate port (default 24680) for cross-origin iframe isolation.
Port management: The MCP server prefers port 8000 (configurable via SUNPEAK_MCP_PORT, users typically have an ngrok tunnel on this port). The inspector's Vite dev server uses port 24679 for its HMR WebSocket. The sandboxServer uses port 24680 (configurable via SUNPEAK_SANDBOX_PORT). The main dev server listens on the user-facing port (default 3000). All ports use getPort() to find free alternatives if the preferred port is taken.
Two orthogonal flags that toggle real tool handlers and production resource bundles independently:
| Flags | UI | Tools | Use case |
|---|---|---|---|
| (none) | HMR | Mocked | Day-to-day dev |
--prod-tools |
HMR | Real handlers | Integration testing |
--prod-resources |
Built | Mocked | CI/E2E, catch build regressions |
--prod-tools --prod-resources |
Built | Real handlers | Final smoke test |
Implementation: Tool calls flow through MCP protocol to the local server (no Vite middleware). --prod-tools sets the initial state of the Prod Tools sidebar checkbox. --prod-resources runs sunpeak build before starting and sets the initial Prod Resources checkbox state. Both are runtime-toggleable in the sidebar. The Inspector component accepts mcpServerUrl, defaultProdResources, hideInspectorModes, demoMode, and sandboxUrl props.
In development mode (viteMode), a small overlay appears in the bottom-right corner of every resource showing:
- Resource: HH:MM:SS timestamp when the resource HTML was generated (detects stale cached resources in ChatGPT)
- Tool: millisecond duration of the most recent tool call
The overlay is injected at readResource time in getDevOverlayScript() (server.ts). Both values are baked into the HTML: servedAt from Date.now(), toolMs from the module-level lastToolTimingMs (set by tool handlers on completion). For inspector re-runs (same iframe, new tool call), a PostMessage listener updates the timing via _meta._sunpeak.requestTimeMs.
The overlay is never present in production builds (only injected when viteMode is true). It can be disabled via:
devOverlay=falseURL param on the inspector (strips via regex in/__sunpeak/read-resource)SUNPEAK_DEV_OVERLAY=falseenvironment variable (disables server-side injection)defineLiveConfig({ devOverlay: false })in live test configs (passes the env var to the dev server)
The inspector reads URL params (parsed in use-inspector-state.ts):
sidebar=false— hides the sidebar, renders only conversation content (useful for headless testing or embedding)devOverlay=false— strips the dev overlay from resource HTML (for e2e tests where the overlay could interfere with assertions)toolInput=<json>— JSON-encoded tool arguments that override the simulation fixture's default toolInput (used by theinspectorfixture whenrenderToolreceives aninputargument)autoRun=true— call the tool on load when no fixture data exists (set by test fixtures to get results without clicking Run; not set during interactive use)
Template e2e tests use the mcp and inspector fixtures from sunpeak/test, which set devOverlay: false automatically. The dev-overlay.spec.ts test files (excluded from sunpeak new via dev- prefix filter in new.mjs) test the overlay explicitly and import createInspectorUrl directly from sunpeak/chatgpt.
Each layer works independently. The inspector and testing framework work with any MCP server in any language and can be embedded within other frameworks.
Convention-over-configuration for building MCP Apps. The inspector and testing are built in. defineConfig() auto-detects sunpeak projects and starts sunpeak dev as the backend. Template tests use the testing framework's fixtures and configs. For evals, the dev server auto-starts with --prod-tools.
Automated testing powered by the inspector. Works with any MCP server in any language. sunpeak's long-term goal is to be the generic testing framework for all MCP servers, not just MCP Apps. MCP Apps (interactive UIs in chat) are the current specialty, but the testing framework should work for any server that implements the MCP protocol. Keep MCP protocol primitives as a clean, 1:1 layer that can evolve with the spec. Layer sunpeak-specific functionality (inspector rendering, visual regression, simulations, MCP Apps features) on top without mixing it into the protocol layer.
The framework covers two distinct testing needs: (1) runtime correctness via e2e, visual, and live tests that exercise the replicated ChatGPT and Claude runtimes, and (2) model compatibility via the eval framework (sunpeak/eval), which connects to your MCP server, discovers tools via MCP protocol, and sends prompts to multiple models (GPT-4o, GPT-4o-mini, o4-mini, Claude Sonnet, Gemini 2.0 Flash) via the Vercel AI SDK. Eval cases can include appContext to test follow-up prompts that rely on state shared through updateModelContext or useAppState. Each case runs N times per model and reports pass/fail counts so developers can confirm tool descriptions, schemas, and App Context work reliably on smaller and cheaper models. Evals are opt-in (--eval) because they cost money.
mcp fixture (sunpeak/test) — protocol-only methods:
callTool(name, input?)— call a tool, return the MCP resultlistTools()— list tool definitionslistResources()— list resource definitionsreadResource(uri)— read resource content
inspector fixture (sunpeak/test) — rendering in the inspector:
renderTool(name, input?, options?)— call a tool, render in the inspector, returnInspectorResultwithapp()locator,sourcefield ('fixture'or'server'), andscreenshot(name?, options?)methodhost— current host ID ('chatgpt'or'claude')page— raw PlaywrightPagefor chrome-level assertions
renderTool with input navigates via ?tool=X&toolInput=JSON&autoRun=true (real server call, bypasses fixtures). Without input, it uses ?simulation=X&autoRun=true (uses fixture data when available, falls back to real call). autoRun tells the inspector to call the tool on load when no fixture result exists; interactive users don't set this flag so browsing tools doesn't trigger server calls. The fixture reads the tool result from a <script id="__tool-result"> data element so MCP-native matchers (toBeError, toHaveTextContent, toHaveStructuredContent, toHaveContentType) operate on real data. Fixture timeouts are configurable via use: { mcpTimeout } in Playwright config or per-call { timeout } option.
Server configuration: defineConfig({ server: { command, args, url, env, cwd }, timeout }) connects to any MCP server. For non-JS projects, sunpeak test init scaffolds a self-contained tests/sunpeak/ directory with its own package.json (version-pinned), auto-installs dependencies, and is ready to run. sunpeak test auto-discovers this directory from the project root.
Embedding: Other frameworks import defineConfig from sunpeak/test/config to generate Playwright configs programmatically. Binary resolution checks local node_modules/.bin first so sunpeak doesn't need to be installed globally.
Test types: E2E (sunpeak/test), Visual regression (result.screenshot() on InspectorResult), Live tests (sunpeak/test/live for real ChatGPT/Claude), Evals (sunpeak/eval for multi-model tool calling via Vercel AI SDK).
CLI: sunpeak test runs unit + e2e tests by default (unit tests only run if vitest is configured, i.e. app framework projects), sunpeak test --e2e runs only Playwright e2e tests, sunpeak test --visual runs e2e with visual regression comparison, sunpeak test --visual --update updates visual baselines, sunpeak test --live runs live tests against real hosts, sunpeak test --eval runs multi-model evals, sunpeak test --unit runs vitest unit tests (app framework projects only, not scaffolded by test init), sunpeak test init scaffolds test infrastructure (e2e, visual, live, eval — no unit tests). Flags are additive. --update implies --visual. --eval and --live are never included in the default run (they cost money).
Test any MCP server in replicated ChatGPT and Claude runtimes. No sunpeak project required.
- CLI:
sunpeak inspect --server <url>orsunpeak inspect --server "python server.py". Supports--env KEY=VALUE(repeatable) and--cwd <path>for stdio servers. - Programmatic:
inspectServer()fromsunpeak/inspectlets other frameworks start the inspector from their own CLI. - OAuth: Auto-negotiates MCP OAuth when servers return 401. Handles anonymous/auto-approved OAuth without user interaction. For interactive OAuth, opens the authorization URL in the user's browser and waits for the callback. Uses the MCP SDK's standard
OAuthClientProviderinterface. - Built into
sunpeak devfor app framework users.
Docs are built with Mintlify. Structure:
docs/docs.json— Navigation config. Three tabs: Documentation, API Reference, MCP Apps.docs/api-reference/hooks/— One.mdxfile per sunpeak hook. Badge:<Badge color="yellow">sunpeak API</Badge>.docs/mcp-apps/— MCP Apps SDK documentation (protocol-level, not sunpeak-specific). Badge:<Badge color="green">MCP Apps SDK</Badge>.docs/mcp-apps/types/protocol-reference.mdx— Complete protocol type/schema reference.
When adding new hooks or features, you must: create the hook doc page, add it to docs.json navigation (alphabetical within its group), and update cross-references (e.g., the <Tip> in mcp-apps/app/requests.mdx that lists convenience hooks).
Path consistency: File paths, docs.json group names, and resulting URL paths must stay consistent. When creating or moving doc pages, the file's directory should match the nav group it belongs to (e.g., a page in the "Server" group lives under mcp-apps/server/). If a file move changes a URL, add a Mintlify redirect in docs.json "redirects" to preserve SEO and update all internal links to the new path.
When sunpeak package APIs change (new hooks, new features, deprecations, etc.), these locations may need updating:
docs/— Mintlify docs pages (hook docs, MCP Apps SDK docs, cross-references)- READMEs —
README.mdfiles throughout the monorepo (packages/sunpeak/README.md, rootREADME.md, templateREADME.md) skills/create-sunpeak-app/SKILL.md— Agent skill for building MCP Apps (hooks, patterns, simulations)skills/test-mcp-server/SKILL.md— Agent skill for testing MCP servers (e2e, visual, live, evals)bin/lib/eval/eval-providers.mjs— Single source of truth for eval provider packages, model IDs, and CLI labels. Bothsunpeak newandsunpeak test initimport from here. When adding/removing models or providers, update this file andtemplate/tests/evals/eval.config.ts(must use the same spacing format so the uncomment regex works).- This file
- Update
packages/sunpeak/package.jsonandpackages/sunpeak/template/package.json - Run
pnpm installfrom monorepo root - Verify:
pnpm --filter sunpeak typecheck && pnpm --filter sunpeak lint && pnpm --filter sunpeak test -- --run && pnpm --filter sunpeak build - Regenerate examples:
pnpm --filter sunpeak generate-examples
This is the upstream SDK that sunpeak wraps. Upgrades often introduce new App methods, types, schemas, and capabilities that sunpeak must surface. Follow this checklist:
- Find the exact diff — Check the SDK's changelog or diff the installed package (
node_modules/@modelcontextprotocol/ext-apps/dist/) to identify new exports: methods onApp, types, Zod schemas, method constants, and host capabilities. - New
Appmethods → new hooks — For each new method on theAppclass (e.g.,app.downloadFile(),app.readServerResource()):- Create a hook in
src/hooks/following theuseCallServerTool/useOpenLinkpattern:useCallback+useApp()null check +console.warnfallback. - Export from
src/hooks/index.ts(alphabetical within the "Action hooks" section). - Create a doc page in
docs/api-reference/hooks/and add todocs.json.
- Create a hook in
- New types/schemas/constants → re-exports — Add to
src/index.tsin the appropriate section (method constants, Zod schemas, or protocol types). Updatedocs/mcp-apps/types/protocol-reference.mdx. - New host capabilities — Add to
DEFAULT_HOST_CAPABILITIESinsrc/inspector/mcp-app-host.tsand add the correspondingbridge.on*handler. - Update docs version note — Bump the SDK version in
docs/mcp-apps/introduction.mdxanddocs/mcp-apps/types/protocol-reference.mdx. - Check for deprecations — If new generic APIs supersede platform-specific hooks, remove the old hook and its docs.
- Update
requests.mdx— Add sections for newAppmethods indocs/mcp-apps/app/requests.mdxand update the<Note>listing convenience hooks.
The SDK's main entry (app.d.ts) uses export * from "./types" to re-export all types, schemas, and constants. To discover available exports, check:
node_modules/@modelcontextprotocol/ext-apps/dist/types.d.ts— All type definitionsnode_modules/@modelcontextprotocol/ext-apps/dist/app.d.ts—Appclass methods
Never generate code that imports from absolute paths to the CLI install. sunpeak is installed globally but also as a project dependency. When the CLI generates config files, vitest configs, or transformed code that will run in the project's context, all imports must use package names (sunpeak/eval, sunpeak/eval/plugin) not absolute paths (resolve(__dirname, ...)). Otherwise import('ai') and other project-local deps resolve from the global pnpm store instead of the project's node_modules. Tests in commands.test.ts enforce this.
- pnpm workspace with packages at
packages/*andpackages/sunpeak/template - ESM-first (
"type": "module") - Tailwind CSS with MCP standard variables via arbitrary values (
text-[var(--color-text-primary)],bg-[var(--color-background-primary)],border-[var(--color-border-primary)]) - Resources discovered from
src/resources/{name}/{name}.tsx - Tools discovered from
src/tools/{name}.ts(each exportstool: AppToolConfig,schema, optionaloutputSchema,defaulthandler) - Simulations discovered from
tests/simulations/*.json(flat directory,"tool"string field references tool filename) - Optional server entry at
src/server.ts(exportsserver: ServerConfigfor identity/icons/instructions,auth()for request authentication).instructionsis a server-wide string sent in the MCPinitializeresponse that hosts may inject into the model's system prompt — for cross-tool workflows and constraints. - Hook file naming:
use-{kebab-name}.ts→ exportuse{PascalName}(e.g.,use-download-file.ts→useDownloadFile) - SDK re-exports in
src/index.tsare organized into five sections: core classes/functions, app options/tool registration/Standard Schema types, method constants, Zod schemas, protocol types