关于 create-sunpeak-app
create-sunpeak-app 是用于开发 MCP Apps 的技能,面向使用 React 构建交互式界面的开发者。它依据 Sunpeak 的项目约定组织资源和工具,并涵盖组件、服务端工具、模拟数据及开发检查流程。
主要功能
- 01按约定组织资源与工具
- 02使用 React 构建交互界面
- 03提供 MCP App 专用 Hooks
- 04通过 CLI 开发、构建和启动
- 05使用模拟数据进行检查
适用场景
- 01创建嵌入 AI 聊天宿主的交互式应用
- 02将 MCP 工具与界面资源关联
- 03构建需要用户确认的操作流程
- 04用模拟数据检查工具和界面
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 框架,用于构建带有交互式 UI 的 MCP 应用,这些 UI 可在 AI 聊天宿主(ChatGPT、Claude)中运行。它提供 React Hooks、开发检查器、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"字符串引用工具文件名)
资源组件模式
每个资源文件都导出两项:
resource— 一个ResourceConfig对象,包含 MCP 资源元数据(名称根据目录自动推导)- 一个具名 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>()— 为输入和输出都提供类型- 所有 Hook 都必须在任何提前
return之前调用(React Hook 规则) - 不要在 Hook 内直接修改
app— 对类设置器使用eslint-disable-next-line react-hooks/immutability
工具文件
每个工具 .ts 文件都导出元数据、一个 Zod schema、一个可选的输出 schema,以及一个处理器。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 进行取消,
使用 extra.mcpReq.id 获取请求 ID,并使用 extra.http?.authInfo 获取经过验证的 HTTP 身份验证信息。
仅限后端的工具(确认循环)
一种常见模式是将 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。一个 review 工具通过 action 字段处理所有审核变体(购买、差异、帖子)。对于 callServerTool 调用,检查器会返回模拟数据,以匹配真实宿主的行为。完整实现请参阅模板中的 review 资源。
模拟文件
模拟是为开发检查器提供支持的 JSON fixture。将它们作为扁平 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." } } }
]
}
}
支持每个工具对应多个模拟:review-diff.json 和 review-post.json 可通过同一工具的 resource 字段共享同一资源。
核心 Hook 参考
所有 Hook 均从 sunpeak 导入:
| Hook | 返回值 | 说明 |
|---|---|---|
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 | 原始 MCP App 实例,可直接调用 SDK |
useCallServerTool() | (params) => Promise<result> | 返回一个按名称调用服务端工具的函数 |
useCreateSamplingMessage() | (params) => Promise<result> | 通过 sampling/createMessage 请求主机生成大模型补全 |
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] | React 状态;会通过 updateModelContext() 自动同步到主机模型上下文 |
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() 自动发送到主机,因此大模型可以在其上下文窗口中看到当前 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);
使用 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 命令支持两个互不相关的标志,可用于测试不同组合:
--prod-tools— 将callServerTool路由到真实工具处理程序,而非模拟对象--prod-resources— 提供dist/中生产构建的 HTML,而非 Vite HMR--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 秒排空时间)
- 结构化 JSON 日志(
--json-logs),用于日志聚合(Datadog、CloudWatch 等) - 启用
--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/ 加载所有内容,并启动生产 MCP 服务,其中包含真实工具处理程序、Zod 输入验证,以及来自 src/server.ts 的可选身份验证。
发布到 ChatGPT
ChatGPT 应用以插件形式提交和发布。部署生产 MCP 服务后:
- 打开 OpenAI Platform 插件提交门户:
https://platform.openai.com/plugins。 - 为仅含应用的插件或应用加 skills 插件创建
With MCP提交。 - 直接提交生产 MCP 服务 URL。不要提交现有的 ChatGPT 应用 ID。
- 完成发布者身份、上架信息、身份验证、CSP、域名验证、工具元数据、提示词、五项正向测试、三项负向测试、可用性和发行说明。
- 提交审核,获批后发布。
每个 MCP 工具都需要准确设置 readOnlyHint、openWorldHint 和 destructiveHint。审核人员的凭据必须可在不使用 MFA、短信、电子邮件确认或专用网络访问的情况下正常使用。已发布的应用会显示在共享的 Plugins Directory 中,而非单独的 Apps Directory。
请使用 https://learn.chatgpt.com/docs/build-plugins 了解本地插件打包和可选的捆绑 skills。请使用 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 专用 Hook
从 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();
}
| Hook | 描述 |
|---|---|
useUploadFile() | 返回 (file: File) => Promise<{ fileId }>,用于将文件上传到 ChatGPT |
useRequestModal() | 返回 (params) => Promise<void>,用于打开宿主原生模态对话框 |
useRequestCheckout() | 返回 (session) => Promise<...>,用于触发 ChatGPT 即时结账 |
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"] 使用。
测试
如需使用所有测试功能(端到端测试、视觉回归、针对真实 ChatGPT 的实时测试、多模型评估、Playwright 配置),请安装 test-mcp-server 技能:
pnpm dlx skills add Alignbase/sunpeak@test-mcp-server
测试技能适用于任何 MCP 服务(不仅限于 sunpeak 项目)。上文的模拟属于开发工作流,并在此处定义。测试通过 mcp fixture 使用这些模拟。
测试命令请参阅 test-mcp-server 技能。快速参考:sunpeak test(单元测试 + 端到端测试)、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
},
},
},
};
常见错误
- 提前返回之前调用 Hook — 所有 Hook 都必须无条件运行。将
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)。 - 修改 Hook 参数 — 对于
app.onteardown = ...(类 setter,而非修改操作),请使用eslint-disable-next-line react-hooks/immutability。 - 忘记文本回退 — 在模拟中包含
toolResult.content[],以供非 UI 宿主使用。
故障排查:应用未在 ChatGPT/Claude 中呈现
如果调用工具后应用没有显示,请按以下步骤操作:
- 检查隧道——确认 ngrok(或同类工具)正在运行、指向正确的端口,并且上游使用
http而非https(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 | Hooks、类型、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 专用 Hooks(useUploadFile、useRequestModal、useRequestCheckout) |
sunpeak/style.css | 主样式表 |
有关测试导出路径(sunpeak/test、sunpeak/eval 等),请参阅 test-mcp-server 技能。