alignbase/sunpeak/create-sunpeak-app

Claude Code and agent skill by alignbase/sunpeak

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Use when working with sunpeak, or when the user asks to "build an MCP App", "build a ChatGPT App", "add a UI to an MCP tool", "create an interactive resource for Claude Connector or ChatGPT", "build a React UI for an MCP server", or needs guidance on MCP App resources, tool-to-UI data flow, simulation files, host context, platform-specific ChatGPT/Claude features, or production builds. For testing (e2e, visual regression, live tests, evals), see the test-mcp-server skill.

Install

npx vskill@latest install alignbase/sunpeak/create-sunpeak-app

SKILL.md

Create Sunpeak App

Sunpeak is a React framework built on @modelcontextprotocol/ext-apps for building MCP Apps with interactive UIs that run inside AI chat hosts (ChatGPT, Claude). It provides React hooks, a dev inspector, a CLI (sunpeak dev / sunpeak build / sunpeak start), and a structured project convention. ChatGPT apps are now submitted and published as plugins, but the app portion remains an MCP-backed app, so sunpeak's server, tools, resources, and runtime stay the same.

Getting Reference Code

Clone the sunpeak repo for working examples:

bash
git clone --depth 1 https://github.com/Alignbase/sunpeak /tmp/sunpeak

Template app lives at /tmp/sunpeak/packages/sunpeak/template/. This is the canonical project structure — read it first.

Project Structure

sunpeak-app/
├── src/
│ ├── resources/
│ │ └── {name}/
│ │ └── {name}.tsx # Resource component + ResourceConfig export
│ ├── tools/
│ │ └── {name}.ts # Tool metadata, Zod schema, handler
│ ├── server.ts # Optional server entry (auth, identity, icons, instructions)
│ └── styles/
│ └── globals.css # Tailwind imports
├── tests/
│ ├── simulations/
│ │ └── *.json # Simulation fixture files (flat directory)
│ ├── e2e/
│ │ └── {name}.spec.ts # Playwright inspector tests
│ ├── evals/
│ │ ├── eval.config.ts # Eval config (models, runs, defaults)
│ │ ├── .env # API keys (gitignored)
│ │ └── {name}.eval.ts # Eval specs (one per resource or tool)
│ └── live/
│ ├── playwright.config.ts # Live test config (long timeouts, single worker)
│ └── {name}.spec.ts # Live tests against real ChatGPT (one per resource)
├── package.json
└── (vite.config.ts, tsconfig.json, etc. managed by sunpeak CLI)

Discovery is convention-based:

  • Resources: src/resources/{name}/{name}.tsx (name derived from directory)
  • Tools: src/tools/{name}.ts (name derived from filename)
  • Simulations: tests/simulations/*.json (flat directory, "tool" string references tool filename)

Resource Component Pattern

Every resource file exports two things:

  1. `resource` — A ResourceConfig object with MCP resource metadata (name is auto-derived from directory)
  2. A named React component — The UI ({Name}Resource)
tsx
import { useToolData, useHostContext, useDisplayMode, SafeArea } from 'sunpeak';
import type { ResourceConfig } from 'sunpeak';
// MCP resource metadata (name auto-derived from directory: src/resources/weather/)
export const resource: ResourceConfig = {
title: 'Weather',
description: 'Show current weather conditions',
mimeType: 'text/html;profile=mcp-app',
_meta: {
ui: {
csp: {
resourceDomains: ['https://cdn.example.com'],
},
},
},
};
// Type definitions
interface WeatherInput {
city: string;
units?: 'metric' | 'imperial';
}
interface WeatherOutput {
temperature: number;
condition: string;
humidity: number;
}
// React component
export function WeatherResource() {
// All hooks must be called before any early return
const { input, output, isLoading } = useToolData<WeatherInput, WeatherOutput>();
const context = useHostContext();
const displayMode = useDisplayMode();
if (isLoading) return <div className="p-4 text-[var(--color-text-secondary)]">Loading...</div>;
const isFullscreen = displayMode === 'fullscreen';
const hasTouch = context?.deviceCapabilities?.touch ?? false;
return (
<SafeArea className={isFullscreen ? 'flex flex-col h-screen' : undefined}>
<div className="p-4">
<h1 className="text-[var(--color-text-primary)] font-semibold">{input?.city}</h1>
<p className={`${hasTouch ? 'text-base' : 'text-sm'} text-[var(--color-text-secondary)]`}>
{output?.temperature}° — {output?.condition}
</p>
</div>
</SafeArea>
);
}

Rules:

  • Always wrap in <SafeArea> to respect host insets
  • Use MCP standard CSS variables via Tailwind arbitrary values: text-[var(--color-text-primary)], text-[var(--color-text-secondary)], bg-[var(--color-background-primary)], border-[var(--color-border-tertiary)]
  • useToolData<TInput, TOutput>() — provide types for both input and output
  • All hooks must be called before any early return (React rules of hooks)
  • Do NOT mutate app directly inside hooks — use eslint-disable-next-line react-hooks/immutability for class setters

Tool Files

Each tool .ts file exports metadata, a Zod schema, an optional output schema, and a handler. The resource field links a tool to its UI — omit it for data-only tools:

ts
// src/tools/show-weather.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
// 1. Tool metadata (resource links to src/resources/weather/ — omit for tools without a UI)
export const tool: AppToolConfig = {
resource: 'weather',
title: 'Show Weather',
description: 'Show current weather conditions',
annotations: { readOnlyHint: true },
_meta: { ui: { visibility: ['model', 'app'] } },
};
// 2. Zod schema (auto-converted to JSON Schema for MCP)
export const schema = {
city: z.string().describe('City name'),
units: z.enum(['metric', 'imperial']).describe('Temperature units'),
};
// 3. Optional output schema (enables structured output validation)
export const outputSchema = {
temperature: z.number(),
condition: z.string(),
humidity: z.number(),
};
// 4. Handler — return structured data for the UI
export default async function (args: { city: string; units?: string }, extra: ToolHandlerExtra) {
return {
structuredContent: {
temperature: 72,
condition: 'Partly Cloudy',
humidity: 55,
},
};
}

ToolHandlerExtra follows MCP SDK 2.0: use extra.mcpReq.signal for cancellation, extra.mcpReq.id for the request ID, and extra.http?.authInfo for validated HTTP auth.

With MCP SDK 2.4, keep schema IDs valid as URIs (named_output, not named_output:schema). Named object output schemas return their object directly in structuredContent on legacy connections too. Do not unwrap a result property unless your tool actually returns that field. For modern HTTP x-mcp-header parameters, use strings, z.int(), or booleans, not z.number().

Backend-Only Tools (Confirmation Loop)

A common pattern pairs a UI tool (for review) with a backend-only tool (for execution). The UI tool's structuredContent includes a reviewTool field. The resource component reads it and calls the backend tool via useCallServerTool when the user confirms:

ts
// src/tools/review.ts — no resource field, shared by all review variants
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
title: 'Confirm Review',
description: 'Execute or cancel a reviewed action after user approval',
annotations: { readOnlyHint: false },
_meta: { ui: { visibility: ['model', 'app'] } },
};
export const schema = {
action: z.string().describe('Action identifier (e.g., "place_order", "apply_changes")'),
confirmed: z.boolean().describe('Whether the user confirmed'),
decidedAt: z.string().describe('ISO timestamp of decision'),
payload: z.record(z.unknown()).optional().describe('Domain-specific data'),
};
type Args = z.infer<z.ZodObject<typeof schema>>;
export default async function (args: Args, _extra: ToolHandlerExtra) {
if (!args.confirmed) {
return {
content: [{ type: 'text' as const, text: 'Cancelled.' }],
structuredContent: { status: 'cancelled', message: 'Cancelled.' },
};
}
return {
content: [{ type: 'text' as const, text: 'Completed.' }],
structuredContent: { status: 'success', message: 'Completed.' },
};
}

The UI tool returns reviewTool in its response, and the resource calls useCallServerTool on accept/reject. The tool returns both content (human-readable text for the host model) and structuredContent (with status and message for the UI). The resource reads structuredContent.status to determine success/error styling and displays structuredContent.message. One review tool handles all review variants (purchases, diffs, posts) via the action field. The inspector returns mock simulation data for callServerTool calls, matching real host behavior. See the template's review resource for the full implementation.

Simulation Files

Simulations are JSON fixtures that power the dev inspector. Place them in tests/simulations/ as flat JSON files:

json
{
"tool": "show-weather",
"userMessage": "Show me the weather in Austin, TX.",
"toolInput": {
"city": "Austin",
"units": "imperial"
},
"toolResult": {
"structuredContent": {
"temperature": 72,
"condition": "Partly Cloudy",
"humidity": 55
}
}
}

Key fields:

  • tool — String referencing a tool filename in src/tools/ (without .ts)
  • userMessage — Decorative text shown in inspector (no functional purpose)
  • toolInput — Arguments sent to the tool (shown as input to useToolData)
  • toolResult.structuredContent — The data rendered by useToolData().output
  • toolResult.content[] — Text fallback for non-UI hosts
  • serverTools — Mock responses for callServerTool calls. Keys are tool names. Values are either a single CallToolResult (always returned) or an array of { when, result } entries for conditional matching against call arguments.

Example with serverTools (for resources that call backend-only tools):

json
{
"tool": "review-purchase",
"toolResult": { "structuredContent": { "..." } },
"serverTools": {
"review": [
{ "when": { "confirmed": true }, "result": { "content": [{ "type": "text", "text": "Completed." }], "structuredContent": { "status": "success", "message": "Completed." } } },
{ "when": { "confirmed": false }, "result": { "content": [{ "type": "text", "text": "Cancelled." }], "structuredContent": { "status": "cancelled", "message": "Cancelled." } } }
]
}
}

Multiple simulations per tool are supported: review-diff.json, review-post.json sharing the same resource via the same tool's resource field.

Core Hooks Reference

All hooks are imported from sunpeak:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
218
Repository forks
12
7-day trend score
0

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/alignbase/sunpeak/create-sunpeak-app/badge)](https://verified-skill.com/skills/alignbase/sunpeak/create-sunpeak-app)

Something wrong with this skill or its page? Tell us.