mcpField
C
create-sunpeak-app

create-sunpeak-app

github.com/alignbase开发21853 次安装1 天前更新
代码仓库

关于 create-sunpeak-app

create-sunpeak-app 是用于开发 MCP Apps 的技能,面向使用 React 构建交互式界面的开发者。它依据 Sunpeak 的项目约定组织资源和工具,并涵盖组件、服务端工具、模拟数据及开发检查流程。

主要功能

  1. 01按约定组织资源与工具
  2. 02使用 React 构建交互界面
  3. 03提供 MCP App 专用 Hooks
  4. 04通过 CLI 开发、构建和启动
  5. 05使用模拟数据进行检查

适用场景

  1. 01创建嵌入 AI 聊天宿主的交互式应用
  2. 02将 MCP 工具与界面资源关联
  3. 03构建需要用户确认的操作流程
  4. 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" 字符串引用工具文件名)

资源组件模式

每个资源文件都导出两项:

  1. resource — 一个 ResourceConfig 对象,包含 MCP 资源元数据(名称根据目录自动推导)
  2. 一个具名 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()stringIANA 时区(回退到浏览器时区)
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 服务后:

  1. 打开 OpenAI Platform 插件提交门户:https://platform.openai.com/plugins。
  2. 为仅含应用的插件或应用加 skills 插件创建 With MCP 提交。
  3. 直接提交生产 MCP 服务 URL。不要提交现有的 ChatGPT 应用 ID。
  4. 完成发布者身份、上架信息、身份验证、CSP、域名验证、工具元数据、提示词、五项正向测试、三项负向测试、可用性和发行说明。
  5. 提交审核,获批后发布。

每个 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
      },
    },
  },
};

常见错误

  1. 提前返回之前调用 Hook — 所有 Hook 都必须无条件运行。将 useMemo/useEffect 移到任何 if (...) return 代码块之前。
  2. 缺少 <SafeArea> — 始终将内容包裹在 <SafeArea> 中,以遵循宿主的安全区域内边距。
  3. 硬编码颜色 — 请通过 Tailwind 任意值使用 MCP 标准 CSS 变量(text-[var(--color-text-primary)]、bg-[var(--color-background-primary)]),而不是使用原始颜色。
  4. 模拟工具不匹配 — 模拟 JSON 中的 "tool" 字段必须与 src/tools/ 中的工具文件名匹配(例如,"tool": "show-weather" 对应 src/tools/show-weather.ts)。
  5. 修改 Hook 参数 — 对于 app.onteardown = ...(类 setter,而非修改操作),请使用 eslint-disable-next-line react-hooks/immutability。
  6. 忘记文本回退 — 在模拟中包含 toolResult.content[],以供非 UI 宿主使用。

故障排查:应用未在 ChatGPT/Claude 中呈现

如果调用工具后应用没有显示,请按以下步骤操作:

  1. 检查隧道——确认 ngrok(或同类工具)正在运行、指向正确的端口,并且上游使用 http 而非 https(ngrok http 8000)。
  2. 检查开发服务器——确保 sunpeak dev 正在运行,并且 MCP 服务已在预期端口启动(留意“port was in use”消息)。
  3. 重启 sunpeak dev——停止开发服务器(Ctrl+C),然后重新启动。这会清除过期连接。
  4. 刷新或重新添加 MCP 服务。 在 ChatGPT 中,打开 Plugins 下的开发者模式应用,并在插件详情中刷新。在其他宿主平台中,前往其应用或连接器管理页面。如果刷新失败,请移除并使用隧道 URL 重新添加。
  5. 强制刷新宿主页面——Cmd+Shift+R / Ctrl+Shift+R 会清除缓存的 MCP 连接。
  6. 新建聊天——两个宿主平台都会按对话缓存 iframe 内容。新建聊天可强制建立新连接。

完整故障排查指南:https://sunpeak.ai/docs/app-framework/guides/troubleshooting

导出路径

导入项内容
sunpeakHooks、类型、SDK 再导出项、SafeArea、inspector + chatgpt 命名空间
sunpeak/mcp服务端实用工具(runMCPServer、createMcpHandler、createProductionMcpServer)、工具类型(AppToolConfig、ToolHandlerExtra)、服务端配置(ServerConfig)
sunpeak/inspector通用 Inspector、宿主外壳系统、基础设施
sunpeak/chatgptChatGPT 宿主外壳 + Inspector 再导出项
sunpeak/claudeClaude 宿主外壳 + Inspector 再导出项
sunpeak/host宿主检测(isChatGPT、isClaude、detectHost)
sunpeak/host/chatgptChatGPT 专用 Hooks(useUploadFile、useRequestModal、useRequestCheckout)
sunpeak/style.css主样式表

有关测试导出路径(sunpeak/test、sunpeak/eval 等),请参阅 test-mcp-server 技能。

参考资料

评论

支持 Markdown