create-sunpeak-app について
create-sunpeak-appは、SunpeakとReactでインタラクティブなMCP Appsを開発するためのスキルです。リソースやツールの構成規約に加え、UI、サーバー側ツール呼び出し、シミュレーションを扱います。
主な機能
- 01規約に沿ったリソースとツールの配置
- 02ReactによるインタラクティブUIの構築
- 03MCP App向けHooksの利用
- 04CLIによる開発、ビルド、起動
- 05シミュレーションデータでの検査
活用シーン
- 01AIチャットホストに埋め込むアプリの作成
- 02MCPツールとUIリソースの連携
- 03ユーザー確認を伴う操作フローの構築
- 04シミュレーションによるツールとUIのテスト
MCP 対応
Sunpeakは@modelcontextprotocol/ext-appsを基盤にMCP Appsを構築します。このスキルでは、MCPツール、リソース、ランタイムの定義と、Hooksを介したホストやMCPサーバーとの連携を扱います。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ alignbase/ sunpeak --skill create-sunpeak-appインストール後、Claude Code で/create-sunpeak-appを入力して読み込まれたか確認します。
詳細
Sunpeak アプリの作成
Sunpeak は、@modelcontextprotocol/ext-apps を基盤に構築された React フレームワークです。AI チャットホスト(ChatGPT、Claude)内で動作する、インタラクティブな UI を備えた MCP Apps を作成できます。React フック、開発用インスペクター、CLI(sunpeak dev / sunpeak build / sunpeak start)、構造化されたプロジェクト規約を提供します。ChatGPT アプリは現在、プラグインとして申請・公開されますが、アプリ部分は引き続き MCP を基盤とするアプリであるため、sunpeak のサーバー、ツール、リソース、ランタイムは変わりません。
参考コードの取得
作業用のサンプルを確認するには、sunpeak リポジトリをクローンします。
git clone --depth 1 https://github.com/Alignbase/sunpeak /tmp/sunpeak
テンプレートアプリは /tmp/sunpeak/packages/sunpeak/template/ にあります。これは標準となるプロジェクト構成です。まずこちらを読んでください。
プロジェクト構成
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)
検出は規約に基づいています。
- リソース:
src/resources/{name}/{name}.tsx(名前はディレクトリから派生) - ツール:
src/tools/{name}.ts(名前はファイル名から派生) - シミュレーション:
tests/simulations/*.json(フラットなディレクトリで、"tool"文字列がツールのファイル名を参照)
リソースコンポーネントのパターン
すべてのリソースファイルは、次の2つをエクスポートします。
resource— MCP リソースのメタデータを含むResourceConfigオブジェクト(名前はディレクトリから自動的に派生)- 名前付き React コンポーネント — UI(
{Name}Resource)
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>
);
}
ルール:
- ホストのインセットを尊重するため、必ず
<SafeArea>で囲みます - Tailwind の任意値を使って、MCP 標準の CSS 変数を使用します:
text-[var(--color-text-primary)]、text-[var(--color-text-secondary)]、bg-[var(--color-background-primary)]、border-[var(--color-border-tertiary)] useToolData<TInput, TOutput>()— 入力と出力の両方に型を指定します- 早期の
returnより前に、すべてのフックを呼び出す必要があります(React のフックのルール) - フック内で
appを直接変更しないでください — クラスのセッターにはeslint-disable-next-line react-hooks/immutabilityを使用します
ツールファイル
各ツールの .ts ファイルは、メタデータ、Zod スキーマ、任意の出力スキーマ、ハンドラーをエクスポートします。resource フィールドはツールを UI に関連付けます。データ専用のツールでは省略してください。
// 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 は MCP SDK 2.0 に準拠しています。キャンセルには extra.mcpReq.signal、リクエスト ID には extra.mcpReq.id、検証済み HTTP 認証情報には extra.http?.authInfo を使用します。
バックエンド専用ツール(確認ループ)
よくあるパターンとして、レビュー用の UI ツールと、実行用のバックエンド専用ツールを組み合わせます。UI ツールの structuredContent には reviewTool フィールドが含まれます。リソースコンポーネントはこれを読み取り、ユーザーが確認したときに useCallServerTool を使ってバックエンドツールを呼び出します。
// 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.' },
};
}
UI ツールはレスポンスで reviewTool を返し、リソースは承認または拒否時に useCallServerTool を呼び出します。ツールは content(ホストモデル向けの人間が読めるテキスト)と structuredContent(UI 向けの status と message を含む)の両方を返します。リソースは structuredContent.status を読み取って成功・エラーのスタイルを判断し、structuredContent.message を表示します。1つの review ツールで、action フィールドを介して購入、差分、投稿など、すべてのレビューの種類に対応します。インスペクターは callServerTool の呼び出しに対して、実際のホストの動作に合わせたモックシミュレーションデータを返します。実装の全体については、テンプレートの review リソースを参照してください。
シミュレーションファイル
シミュレーションは開発用インスペクターを動かす JSON フィクスチャです。フラットな JSON ファイルとして tests/simulations/ に配置します。
{
"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
}
}
}
主なフィールド:
tool—src/tools/内のツールファイルを参照する文字列(.tsを除く)userMessage— インスペクターに表示される装飾用テキスト(機能上の目的はありません)toolInput— ツールに渡される引数(useToolDataへの入力として表示)toolResult.structuredContent—useToolData().outputによってレンダリングされるデータtoolResult.content[]— UI をサポートしないホスト向けのテキストフォールバックserverTools—callServerTool呼び出しに対するモック応答。キーはツール名です。値には、常に返される単一のCallToolResult、または呼び出し引数との条件付き照合に使用する{ when, result }エントリの配列を指定できます。
(バックエンド専用ツールを呼び出すリソース向けの)serverTools を使った例:
{
"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." } } }
]
}
}
1 つのツールに対して複数のシミュレーションを設定できます。たとえば、review-diff.json と review-post.json が、同じツールの resource フィールドを介して同じリソースを共有できます。
コアフックリファレンス
すべてのフックは sunpeak からインポートします。
| フック | 戻り値 | 説明 |
|---|---|---|
useToolData<TIn, TOut>() | { input, inputPartial, output, isLoading, isError, isCancelled } | ホストからのリアクティブなツールデータ |
useHostContext() | McpUiHostContext | null | ホストのコンテキスト(テーマ、ロケール、機能など) |
useTheme() | 'light' | 'dark' | undefined | 現在のテーマ |
useDisplayMode() | 'inline' | 'pip' | 'fullscreen' | 現在の表示モード(デフォルトは 'inline') |
useLocale() | string | ホストのロケール(例:'en-US'、デフォルトは 'en-US') |
useTimeZone() | string | IANA タイムゾーン(ブラウザーのタイムゾーンをフォールバックとして使用します) |
usePlatform() | 'web' | 'desktop' | 'mobile' | undefined | ホストが報告するプラットフォームの種類 |
useDeviceCapabilities() | { touch?, hover? } | デバイスの入力機能 |
useUserAgent() | string | undefined | ホストアプリケーションの識別子 |
useStyles() | McpUiHostStyles | undefined | ホストのスタイル設定(CSS 変数、フォント) |
useToolInfo() | { id?, tool } | undefined | このアプリを作成したツール呼び出しに関するメタデータ |
useSafeArea() | { top, right, bottom, left } | セーフエリアのインセット(px) |
useViewport() | { width, height, maxWidth, maxHeight } | コンテナーの寸法(px) |
useIsMobile() | boolean | ビューポートがモバイルサイズの場合は True |
useApp() | App | null | SDK を直接呼び出すための、生の MCP App インスタンス |
useCallServerTool() | (params) => Promise<result> | 名前を指定してサーバー側のツールを呼び出す関数を返します |
useCreateSamplingMessage() | (params) => Promise<result> | sampling/createMessage を介して、ホストに LLM の補完をリクエストします |
useRegisterTool() | (name, config, cb) => handle | ホストが呼び出せるアプリ側のツールを登録します。有効化/無効化/削除用のハンドルを返します |
useSendMessage() | (params) => Promise<void> | 会話にメッセージを送信する関数を返します |
useOpenLink() | (params) => Promise<void> | ホスト経由で URL を開く関数を返します |
useRequestDisplayMode() | { requestDisplayMode, availableModes } | 'inline'、'pip'、または 'fullscreen' をリクエストします。まず availableModes を確認してください |
useDownloadFile() | (params) => Promise<result> | ホスト経由でファイルをダウンロードします(クロスプラットフォームで動作します) |
useReadServerResource() | (params) => Promise<result> | URI を指定して MCP サーバーからリソースを読み取ります |
useListServerResources() | (params?) => Promise<result> | MCP サーバーで利用可能なリソースを一覧表示します |
useUpdateModelContext() | (params) => Promise<void> | 状態をホストのモデルコンテキストに直接送信します |
useSendLog() | (params) => Promise<void> | デバッグログをホストに送信します |
useSendToolListChanged() | () => Promise<void> | アプリのツール一覧が変更されたことをホストに通知します(登録/削除/有効化/無効化の後) |
useHostInfo() | { hostVersion, hostCapabilities } | ホスト名、バージョン、サポートされている機能 |
useTeardown(fn) | void | ティアダウンハンドラーを登録します |
useAppTools(config) | void | アプリがホストに提供するツールを登録します(双方向のツール呼び出し) |
useRequestTeardown() | () => Promise<void> | このアプリインスタンスをティアダウンするようホストにリクエストします |
useAppState(initial) | [state, setState] | updateModelContext() を介してホストのモデルコンテキストと自動同期する React の状態 |
useRequestDisplayMode の詳細
const { requestDisplayMode, availableModes } = useRequestDisplayMode();
// Always check availability before requesting
if (availableModes?.includes('fullscreen')) {
await requestDisplayMode('fullscreen');
}
if (availableModes?.includes('pip')) {
await requestDisplayMode('pip');
}
useCallServerTool の詳細
const callTool = useCallServerTool();
const result = await callTool({ name: 'get-weather', arguments: { city: 'Austin' } });
// result: { content?: [...], isError?: boolean }
useSendMessage の詳細
const sendMessage = useSendMessage();
await sendMessage({
role: 'user',
content: [{ type: 'text', text: 'Please refresh the data.' }],
});
useAppState の詳細
状態は React 内で保持され、更新のたびに updateModelContext() を介してホストに自動送信されるため、LLM はコンテキストウィンドウ内で現在の UI の状態を確認できます。モデル評価では、評価ケースの appContext フィールドに同じ状態を設定してください。そうすることで、「これを予約して」のような後続のプロンプトを、選択されたアプリの状態に対してテストできます。
const [state, setState] = useAppState<{ decision: 'accepted' | 'rejected' | null }>({
decision: null,
});
// setState triggers a re-render AND pushes state to the model context
setState({ decision: 'accepted' });
useToolDataの詳細
const {
input, // TInput | null — final tool input arguments
inputPartial, // TInput | null — partial (streaming) input as it generates
output, // TOutput | null — structuredContent when present, otherwise content
isLoading, // boolean — true until first toolResult arrives
isError, // boolean — true if tool returned an error
isCancelled, // boolean — true if tool was cancelled
cancelReason, // string | null
} = useToolData<MyInput, MyOutput>(defaultInput, defaultOutput);
LLMの生成中に段階的にレンダリングするには、inputPartialを使用します。最終データにはoutputを使用します。
useDownloadFileの詳細
const downloadFile = useDownloadFile();
// Download embedded text content
await downloadFile({
contents: [{
type: 'resource',
resource: {
uri: 'file:///export.json',
mimeType: 'application/json',
text: JSON.stringify(data, null, 2),
},
}],
});
// Download embedded binary content
await downloadFile({
contents: [{
type: 'resource',
resource: {
uri: 'file:///image.png',
mimeType: 'image/png',
blob: base64EncodedPng,
},
}],
});
useReadServerResource/useListServerResourcesの詳細
const readResource = useReadServerResource();
const listResources = useListServerResources();
// List available resources
const result = await listResources();
for (const resource of result?.resources ?? []) {
console.log(resource.name, resource.uri);
}
// Read a specific resource by URI
const content = await readResource({ uri: 'videos://bunny-1mb' });
useAppToolsの詳細
双方向のツール呼び出しに対応するため、アプリがホストに提供するツールを登録します。ツールのメタデータはtools[]に設定し、すべての呼び出しを単一のonCallToolコールバックでディスパッチします。
import { useAppTools } from 'sunpeak';
function MyResource() {
useAppTools({
tools: [
{
name: 'get-selection',
description: 'Get current user selection',
inputSchema: { type: 'object', properties: {} },
},
],
onCallTool: async ({ name, arguments: args }) => {
if (name === 'get-selection') {
return { content: [{ type: 'text', text: selectedText }] };
}
return { content: [], isError: true };
},
});
}
コマンド
sunpeak new # Scaffold a new sunpeak app project
sunpeak dev # Start dev server (Vite + MCP server, port 3000 web / 8000 MCP)
sunpeak build # Build resources + compile tools to dist/
sunpeak start # Start production MCP server (real handlers, auth, Zod validation)
sunpeak upgrade # Upgrade sunpeak to the latest version
sunpeak devコマンドを実行すると、Vite開発サーバーとMCPサーバーが同時に起動します。インスペクターはhttp://localhost:3000で動作します。ChatGPTをhttp://localhost:8000/mcpに接続するか、リモートテストにはngrokを使用します。
実際のChatGPTでテストするには、開発者モードを有効にする必要があります。必要に応じて、左下のユーザーメニューをクリックし、Settings > Security and login > Developer modeを選択します。その後、ChatGPTのホームページに戻り、サイドバーのPluginsをクリックします。すでにアプリがある場合は開き、ない場合は+を選択して、トンネル経由の/mcp URLを指定して作成します。
シミュレーション用フィクスチャではなく実際のハンドラーを使って、本番環境での動作をローカルでテストするには、sunpeak build && sunpeak startを使用します。
sunpeak devコマンドでは、異なる組み合わせをテストするための、互いに独立した2つのフラグを使用できます。
--prod-tools—callServerToolをシミュレーション用モックではなく、実際のツールハンドラーにルーティングします--prod-resources— Vite HMRではなく、dist/の本番ビルド済みHTMLを配信します--prod-tools --prod-resources— 完全なスモークテスト:本番用バンドルと実際のハンドラーを使用します
本番サーバーのオプション
sunpeak start # Default: port 8000, all interfaces
sunpeak start --port 3000 # Custom port
sunpeak start --host 127.0.0.1 # Bind to localhost only
sunpeak start --json-logs # Structured JSON logging
sunpeak start --sse # SSE streaming with 15s keep-alives
sunpeak start --sse-keep-alive-ms 0 # Disable SSE keep-alives
PORT=3000 HOST=127.0.0.1 sunpeak start # Via environment variables
本番サーバーでは、以下を提供します。
/health— ロードバランサーのプローブと監視向けのヘルスチェックエンドポイント({"status":"ok","uptime":N})/mcp— MCP Streamable HTTPエンドポイント- SIGTERM/SIGINTを受信した際のグレースフルシャットダウン(5秒間のドレイン)
- ログ集約(Datadog、CloudWatchなど)向けの構造化JSONログ(
--json-logs) --sseが有効な場合のSSEキープアライブコメント(調整には--sse-keep-alive-msまたはSUNPEAK_SSE_KEEP_ALIVE_MSを使用)
本番ビルドの出力
sunpeak buildは、最適化されたバンドルをdist/に生成します。
dist/
├── weather/
│ ├── weather.html # Self-contained bundle (JS + CSS inlined)
│ └── weather.json # ResourceConfig with generated uri for cache-busting
├── tools/
│ ├── show-weather.js # Compiled tool handler + Zod schema
│ └── ...
├── server.js # Compiled server entry (if src/server.ts exists)
└── ...
sunpeak startはすべてをdist/から読み込み、実際のツールハンドラー、Zodによる入力検証、src/server.tsからの任意の認証に対応した本番MCPサーバーを起動します。
ChatGPTへの公開
ChatGPTアプリはプラグインとして申請・公開します。本番MCPサーバーをデプロイした後、次の手順に従います。
- OpenAI Platformのプラグイン申請ポータル(
https://platform.openai.com/plugins)を開きます。 - アプリのみのプラグイン、またはアプリとスキルのプラグインとして、
With MCP申請を作成します。 - 本番MCPサーバーのURLを直接送信します。既存のChatGPTアプリIDは送信しないでください。
- 公開者の本人確認情報、掲載情報、認証、CSP、ドメインの検証、ツールのメタデータ、プロンプト、5件の肯定的テスト、3件の否定的テスト、提供状況、リリースノートを入力します。
- 審査に申請し、承認後に公開します。
すべてのMCPツールで、readOnlyHint、openWorldHint、destructiveHintを正確に設定する必要があります。審査担当者の認証情報は、MFA、SMS、メール確認、プライベートネットワークへのアクセスなしで使用できなければなりません。公開されたアプリは、独立したApps Directoryではなく、共通のPlugins Directoryに表示されます。
ローカルでのプラグインのパッケージ化と、任意のスキルのバンドルについては、https://learn.chatgpt.com/docs/build-pluginsを参照してください。現在の公開審査要件については、https://learn.chatgpt.com/docs/submit-pluginsを参照してください。
ホストの検出
import { isChatGPT, isClaude, detectHost } from 'sunpeak/host';
// In a resource component
function MyResource() {
const host = detectHost(); // 'chatgpt' | 'claude' | 'unknown'
if (isChatGPT()) {
// Safe to use ChatGPT-specific hooks
}
}
ChatGPT固有のフック
sunpeak/host/chatgptからインポートします。使用前に必ず機能を検出してください。
import { useUploadFile, useRequestModal, useRequestCheckout } from 'sunpeak/host/chatgpt';
import { isChatGPT } from 'sunpeak/host';
function MyResource() {
// Only call these when on ChatGPT
const uploadFile = useUploadFile();
const requestModal = useRequestModal();
const requestCheckout = useRequestCheckout();
}
| フック | 説明 |
|---|---|
useUploadFile() | ChatGPTにファイルをアップロードするための(file: File) => Promise<{ fileId }>を返します |
useRequestModal() | ホストネイティブのモーダルダイアログを開くための(params) => Promise<void>を返します |
useRequestCheckout() | ChatGPTのインスタントチェックアウトを開始するための(session) => Promise<...>を返します |
SafeArea コンポーネント
ホストのインセットを尊重するため、リソースのコンテンツは常に<SafeArea>で囲んでください。
import { SafeArea } from 'sunpeak';
export function MyResource() {
return (
<SafeArea>
{/* your content */}
</SafeArea>
);
}
SafeAreaはuseSafeArea()のインセットと同じ値のpaddingを自動的に適用します。
MCP 標準変数を使ったスタイリング
生の色値ではなく、Tailwindの任意値を介してMCP標準CSS変数を使用してください。これらの変数は、各ホスト(ChatGPT、Claude)のテーマに合わせて自動的に調整されます。
| Tailwindクラス | CSS変数 | 用途 |
|---|---|---|
text-[var(--color-text-primary)] | --color-text-primary | 主要テキスト |
text-[var(--color-text-secondary)] | --color-text-secondary | 補助テキスト/控えめなテキスト |
bg-[var(--color-background-primary)] | --color-background-primary | カード/サーフェスの背景 |
bg-[var(--color-background-secondary)] | --color-background-secondary | セカンダリ/ネストされたサーフェスの背景 |
bg-[var(--color-background-tertiary)] | --color-background-tertiary | ターシャリ背景 |
bg-[var(--color-ring-primary)] | --color-ring-primary | 主要アクションの色(例:バッジの塗りつぶし) |
border-[var(--color-border-tertiary)] | --color-border-tertiary | 控えめな境界線 |
border-[var(--color-border-primary)] | --color-border-primary | デフォルトの境界線 |
dark:バリアント | — | [data-theme="dark"]によるダークモード |
これらの変数はCSSのlight-dark()を使用しているため、テーマの変更に自動的に対応します。Tailwindのdark:バリアントも[data-theme="dark"]を介して機能します。
テスト
すべてのテスト機能(E2Eテスト、ビジュアルリグレッション、実際のChatGPTに対するライブテスト、マルチモデル評価、Playwrightの設定)を利用するには、test-mcp-serverスキルをインストールしてください。
pnpm dlx skills add Alignbase/sunpeak@test-mcp-server
テストスキルは、sunpeakプロジェクトに限らず、どのMCPサーバーでも使用できます。シミュレーション(上記)は開発ワークフローの一部であり、ここで定義されています。テストではmcpフィクスチャを介してシミュレーションを使用します。
テストコマンドについては、test-mcp-serverスキルを参照してください。クイックリファレンス:sunpeak test(ユニットテスト+E2E)、sunpeak test --visual(ビジュアルリグレッション)、sunpeak test --live(実際のChatGPT)、sunpeak test --eval(マルチモデル評価)。
ResourceConfigのフィールド
import type { ResourceConfig } from 'sunpeak';
// name is auto-derived from the directory (src/resources/my-resource/)
export const resource: ResourceConfig = {
title: 'My Resource', // Human-readable title
description: 'What it shows', // Description for MCP hosts
mimeType: 'text/html;profile=mcp-app', // Required for MCP App resources
_meta: {
ui: {
csp: {
resourceDomains: ['https://cdn.example.com'], // Image/script CDNs
connectDomains: ['https://api.example.com'], // API fetch targets
},
},
},
};
よくある間違い
- 早期リターンより前にフックを呼び出す — すべてのフックは無条件に実行する必要があります。
useMemo/useEffectを、if (...) returnブロックより前に移動してください。 <SafeArea>の不足 — ホストのセーフエリアインセットを尊重するため、コンテンツは常に<SafeArea>で囲んでください。- 色のハードコード — 生の色値ではなく、Tailwindの任意値を介してMCP標準CSS変数(
text-[var(--color-text-primary)]、bg-[var(--color-background-primary)])を使用してください。 - シミュレーションツールの不一致 — シミュレーションJSONの
"tool"フィールドは、src/tools/内のツールファイル名と一致している必要があります(例:"tool": "show-weather"はsrc/tools/show-weather.tsに対応します)。 - フックのパラメーターの変更 —
app.onteardown = ...にはeslint-disable-next-line react-hooks/immutabilityを使用してください(これはクラスのセッターであり、変更ではありません)。 - テキストのフォールバックの忘れ — UIを表示しないホスト向けに、シミュレーションに
toolResult.content[]を含めてください。
トラブルシューティング:ChatGPT/Claude でアプリが表示されない
ツールの呼び出し後にアプリが表示されない場合は、次の手順に従ってください。
- トンネルを確認する — ngrok(または同等のツール)が実行中で、正しいポートを指定し、上流で
httpsではなくhttpを使用していることを確認します(ngrok http 8000)。 - 開発サーバーを確認する —
sunpeak devが実行中で、MCP サーバーが想定されたポートで起動していることを確認します(「port was in use」というメッセージに注意してください)。 sunpeak devを再起動する — 開発サーバーを停止し(Ctrl+C)、再起動します。これにより、古い接続がクリアされます。- MCP サーバーを更新するか、再追加する。 ChatGPT では、
Pluginsの開発者モードのアプリを開き、プラグインの詳細から更新します。他のホストでは、それぞれのアプリまたはコネクター管理ページを使用します。更新に失敗した場合は、削除してトンネル URL を使って再追加してください。 - ホストのページを強制的に再読み込みする —
Cmd+Shift+R/Ctrl+Shift+Rを押すと、キャッシュされた MCP 接続がクリアされます。 - 新しいチャットを開く — どちらのホストも会話ごとに iframe のコンテンツをキャッシュします。新しいチャットを開くと、新しい接続が確立されます。
トラブルシューティングの完全なガイド:https://sunpeak.ai/docs/app-framework/guides/troubleshooting
エクスポートパス
| インポート | 内容 |
|---|---|
sunpeak | フック、型、SDK の再エクスポート、SafeArea、inspector と chatgpt の名前空間 |
sunpeak/mcp | サーバーユーティリティ(runMCPServer、createMcpHandler、createProductionMcpServer)、ツールの型(AppToolConfig、ToolHandlerExtra)、サーバー設定(ServerConfig) |
sunpeak/inspector | 汎用 Inspector、ホストシェルシステム、インフラストラクチャ |
sunpeak/chatgpt | ChatGPT ホストシェルと Inspector の再エクスポート |
sunpeak/claude | Claude ホストシェルと Inspector の再エクスポート |
sunpeak/host | ホストの検出(isChatGPT、isClaude、detectHost) |
sunpeak/host/chatgpt | ChatGPT 専用フック(useUploadFile、useRequestModal、useRequestCheckout) |
sunpeak/style.css | メインスタイルシート |
テスト用エクスポートパス(sunpeak/test、sunpeak/eval など)については、test-mcp-server スキルを参照してください。