@coze/app-sdk
v0.0.1-alpha.b378f6
Published
MCP Apps View SDK with Coze Host integrations
Downloads
194
Readme
@coze/app-sdk
@coze/app-sdk 是基于官方 @modelcontextprotocol/ext-apps 的 MCP Apps View SDK。
它完整继承并重导出官方 View API,同时为 Coze Host 增加 Composer 草稿写入和 Panel 交互能力。
该包面向所有开发者发布到公网 npm,同时保留字节内部 BNPM 和仓库内 workspace 的使用方式。
安装
公网项目从 npm 安装:
npm install @coze/app-sdk字节内部项目仍可从 BNPM 安装:
bnpm install @coze/app-sdk包入口
业务 View 使用根入口:
Panel 与 Host 之间的消息协议统一收录在 Panel 消息协议。
import { App, PostMessageTransport } from '@coze/app-sdk';包提供七个职责分离的入口:
import { App } from '@coze/app-sdk';
import type { AppApi, LaunchContext } from '@coze/app-sdk/contracts';
import { parsePluginDevelopmentPackage } from '@coze/app-sdk/development';
import { App as BusinessApp } from '@coze/app-sdk/business';
import {
definePanel,
definePluginApp,
usePanelRuntime,
} from '@coze/app-sdk/react';
import { PanelRuntimeProvider } from '@coze/app-sdk/react/host';
import type { RuntimeContext } from '@coze/app-sdk/internal';- 根入口:第三方 MCP Apps View 与 Iframe Transport。
/contracts:不加载 React 的 AppApi、LaunchContext 与 Panel Runtime 合同。/development:标准 Plugin Manifest / UI Resources 的开发态解析与校验工具。/business:受信 Host 自定义扩展使用的类型化 request 与错误模型;普通业务 HTTP 不走此入口。/react:一方 React Panel 的定义、Runtime Hook 与definePluginApp。/react/host:Web、桌面和移动端 Host 共用的 Provider 与入口 marker guard。/internal:统一 Bootstrap 与受信 Host 使用的 RuntimeContext、容器 Adapter 和内部方法;不属于业务 API。
API 稳定性与权限
以上七个入口都会随 npm 包发布。面向开发者的公共 API 遵循 SemVer;/business 和 /internal
仅供受信业务或 Host 集成使用。包可以被下载或导入不代表获得对应的 Host 权限,运行时仍以 Host
声明的 capability 和信任校验为准。/internal 不提供 SemVer 兼容性承诺。
Panel 开发
一方 Panel 默认导出零 Host props 组件,并从 Runtime Hook 读取 Host 注入:
import { definePanel, usePanelRuntime } from '@coze/app-sdk/react';
function FilesPanel() {
const { app, launchContext, activation, hostContext, signal, close } =
usePanelRuntime();
// launchContext also exposes the current userId and accountId when provided by Host.
// 业务只依赖统一 Runtime,不判断 iframe、Electron 或 Capacitor。
}
export default definePanel(FilesPanel);PC Web 的 default Native Panel 可以声明当前激活周期的默认工作区布局。该配置只表达初始偏好, Host 仍会保护对话区最小宽度;用户本次拖动或重新展开左栏不会写入全局布局偏好:
export default definePanel(FilesPanel, {
layout: {
collapseLeftPanel: true,
width: { type: 'ratio', value: 0.5, min: 360, max: 960 },
},
});fixed 的 value/min/max 使用 CSS 像素,ratio.value 取值为 (0, 1]。iframe、Sandbox、
Composer、Dialog 和移动端不会消费该配置。
close() 精确返回 Host teardown Promise;signal.aborted 才表示当前实例已经失效。公共 Runtime
不暴露容器类型;业务不应按 iframe、WebView 或 in-process 分叉。平台响应式信息由
hostContext.platform 表达。
一个业务产物可以用 definePluginApp() 声明 Host 可选择的全部 Panel。Panel ID 由 Host 的
内部 RuntimeContext 下发,不从 URL 推导;Dialog 只是 Host 展示 Panel 的一种方式,不需要第二套组件定义:
export default definePluginApp({
panels: {
main: definePanel(FilesPanel),
picker: definePanel(FilePickerPanel),
},
});开发态标准 Manifest
本地调试 Host 直接消费 agent-plugins.org v1.0.0 的 plugin.json,Panel 声明位于
extensions["cn.coze.plugin"].panels;Sandbox Panel 还必须通过 resourceUri 精确引用
cn.coze.plugin/resources.json。开发态只保留这两份与 Panel 有关的标准 JSON,不再定义一套
私有 Catalog。
最小 plugin.json:
{
"$schema": "https://coze.cn/schemas/1.0.0/plugin.schema.json",
"name": "coze-plugin-example",
"extensions": {
"cn.coze.plugin": {
"panels": [
{
"id": "main",
"name": "Main",
"runtime": "sandbox",
"resourceUri": "ui://coze-plugin-example/main"
},
{
"id": "composer",
"name": "Composer",
"runtime": "sandbox",
"placement": "composer",
"resourceUri": "ui://coze-plugin-example/composer"
},
{
"id": "message",
"name": "Message card",
"runtime": "sandbox",
"placement": "message",
"resourceUri": "ui://coze-plugin-example/message"
}
]
}
}
}对应的 cn.coze.plugin/resources.json:
{
"resources": [
{
"uri": "ui://coze-plugin-example/main",
"name": "main",
"path": "./dist/main.html"
},
{
"uri": "ui://coze-plugin-example/composer",
"name": "composer",
"path": "./dist/composer.html"
},
{
"uri": "ui://coze-plugin-example/message",
"name": "message",
"path": "./dist/message.html"
}
]
}parsePluginDevelopmentManifest()、parsePluginDevelopmentResources() 与
parsePluginDevelopmentPackage() 会校验 schema 版本、稳定 Plugin name、Panel ID、重复项、
Resource namespace、包内路径,以及 Sandbox Panel 到 UI Resource 的引用关系,并返回深冻结副本。
开发服务器在固定路径 /__coze_plugin_dev__/plugin.json 与
/__coze_plugin_dev__/cn.coze.plugin/resources.json 暴露它们;revision 使用两份规范化响应体的
共同 HTTP ETag,不得写入 Manifest JSON。
Manifest name 是稳定 Plugin name,不是 plg_* 内部安装 ID;内部 ID、开发 URL、信任级别、
Session/Agent/Project 归属与凭证均由 Host 管理。placement: "composer" 表示由输入框区域承载,
placement: "message" 表示只能由 Host 投影到消息流,placement: "dialog" 表示只能作为
Dialog 目标;这些 placement 都不进入普通 Panel 列表。
当前 Panel 可以通过 app.coze.dialog.open({ panelId, payload }),把同一插件内声明为
placement: "dialog" 的另一个 Panel 交给 Host 以 Modal 展示。目标组件仍使用
usePanelRuntime(),打开参数位于 activation?.payload。
受信的一方 Panel 可以通过
app.coze.commerce.openUpgradeDialog({ benefitType, targetLevel, contentScene, tracking })
请求 Web Host 展示统一的功能升级弹窗。调用前使用
app.coze.commerce.canOpenUpgradeDialog 判断当前 Host 是否开放该能力;该请求只确认 Host 接收,
不返回弹窗关闭或支付结果。
仓库内开发
仓库内使用方需要在自己的 package.json 中声明 workspace 依赖:
{
"dependencies": {
"@coze/app-sdk": "workspace:*"
}
}依赖变更按仓库 Rush 工作流处理,不要在仓库根目录或包目录直接执行 npm、pnpm 或 yarn 安装。
快速开始
import { App, PostMessageTransport } from '@coze/app-sdk';
const app = new App(
{ name: 'example-app', version: '1.0.0' },
{ availableDisplayModes: ['inline', 'fullscreen'] },
{ autoResize: true, strict: true },
);
// 建议在连接前订阅事件,避免遗漏初始化阶段的通知。
const offActivation = app.coze.panel.onActivation(activation => {
console.log('Panel activation:', activation.payload);
});
await app.connect(new PostMessageTransport(window.parent, window.parent));
// 不再需要事件时取消订阅。
offActivation();
// View 销毁时关闭连接。
await app.close();App 的构造参数、标准方法和生命周期与官方 MCP Apps App 保持一致。已有 View 通常只需要把
导入来源改为 @coze/app-sdk。
View 需要关闭自身时,使用与官方源码相同的 API。该通知只表达关闭意图,是否关闭由 Host 决定:
await app.requestTeardown();Host 接受后会继续发送标准 ui/resource-teardown,View 仍通过 app.onteardown 完成保存和清理。
一方 HTTP 与当前对话事件
一方 HTTP 业务直接调用生成 IDL Service,不通过 app.coze.requests 或 Host registrar。
当前对话消息 Event 是公共 App SDK 协议,一方与三方插件使用相同入口:
import { conversationStream } from '@coze/app-sdk';
import { clawApi } from '@coze-space/services/claw';
const result = await clawApi.ListMessage({
session_id: launchContext.sessionId,
});
const unsubscribe = await app.coze.events.subscribe(
conversationStream,
frame => consume(frame),
);
await unsubscribe();调用方必须从可信 LaunchContext 构造请求目标字段,并把现有 AbortSignal 传给 IDL
方法第二个参数。events 位于根入口,调用方不能传 sessionId 或 conversationId,
Host 始终绑定当前 App 实例的对话上下文。订阅前可通过
app.coze.events.canSubscribe(conversationStream) 检查当前 Host 是否支持。listener
直接接收 Base Agent StreamFrame;完整事件类型从 @coze/app-sdk/events
导出。Host 根据 App 信任级别决定可见事件集合:trusted App 接收全部合法 Frame,
third-party App 只接收 agent.message。业务方在 listener 中自行过滤所需事件类型。
app.coze.requests 仅供确实需要 Host 自定义实现的受信扩展使用,不作为业务 HTTP
代理,也不作为普通 Panel 的默认接入方式。
Drive 云盘
Drive 在 App SDK 中提供一等 API。已迁移的受信一方 Panel 直接调用并按方法检查 capability:
const capabilities = await app.coze.drive.getCapabilities();
if (capabilities.available && app.coze.drive.canListEntries) {
const { tree } = await app.coze.drive.listEntries({
path: capabilities.workspace_path,
depth: 2,
});
}完整的路径作用域、11 个方法、二进制传输和统一错误约定见 Drive 协议。
Web、Desktop 与 Mobile 统一使用该入口,不再维护 Business Drive Definition 或并行链路。
下载文件
下载使用 MCP Apps 标准 ui/download-file。连接完成后先做 capability 检查;不支持时应隐藏或禁用入口,
不要自行增加私有下载协议:
if (app.getHostCapabilities()?.downloadFile) {
await app.downloadFile({
contents: [
{
type: 'resource_link',
uri: 'https://example.com/report.pdf',
name: 'report.pdf',
mimeType: 'application/pdf',
},
],
});
}Coze Host 的业务策略只接受标准 ResourceLink 或 EmbeddedResource。链接资源由 Host 在当前插件身份
下解析;第三方 HTTP(S) 请求不携带 Coze Cookie 或 token。多个资源可能由 Coze Host 打包为 ZIP,
但这是 Host 增强行为,View 不能假设其他 MCP Apps Host 也会自动打包。
如果下载对象是当前 Agent / 项目 Workspace 内的云盘文件,并且需要遵循 Drive 的本地优先选路,
应使用 app.coze.drive.downloadFile({ path }),参见 Drive 协议。
它与这里的标准 ui/download-file 是两项独立能力:前者只接受 Workspace 文件路径,后者接受标准 Resource。
Desktop 本地文件上传
app.coze.desktop.uploadFile() 是受信任一方 Panel 的 Desktop-only 能力。当前只提供接口,尚未实现桌面端本地上传逻辑;完整定义见 Desktop 消息协议。
发送消息
立即发送
app.sendMessage() 直接使用标准 MCP Apps ui/message:
const result = await app.sendMessage({
role: 'user',
content: [{ type: 'text', text: '立即发送到会话' }],
});
if (result.isError) {
// Host 已接收请求,但消息发送失败。
}参数类型与 MCP Apps 官方 McpUiMessageRequest['params'] 完全一致,不接受 Coze 私有消息字段、
Coze Skill 或通用 Composer Reference。结构化草稿统一使用 Composer API。
Composer 共享草稿
Composer 是当前会话的共享可见草稿。调用前逐叶检查 capability;每个操作都可以独立开放:
if (app.coze.composer.canReplace) {
await app.coze.composer.replace({
text: '请分析',
focus: true,
});
}
if (app.coze.composer.canAddReferences) {
await app.coze.composer.addReferences({
references: [
{
kind: 'selection',
title: '已选 29 个元素',
details: {
targetTitle: '已选 29 个元素',
},
locator: { selectionId: 'selection-1' },
},
],
});
}
if (app.coze.composer.canUpdateReference) {
await app.coze.composer.updateReference({
referenceId: 'reference-1',
reference: {
kind: 'selection',
title: '已选 30 个元素',
details: {
targetTitle: '已选 30 个元素',
},
locator: { selectionId: 'selection-1' },
},
});
}
if (app.coze.composer.canInsertChip) {
await app.coze.composer.insertChip({
chip: {
entityId: 'asset-1',
kind: 'video-asset',
label: '城市黄昏素材',
metadata: {
coverUrl: 'https://example.com/asset-cover.png',
fileUri: 'file:///assets/city-sunset.mp4',
},
},
focus: true,
});
}
const snapshot = app.coze.composer.canGet
? await app.coze.composer.get()
: undefined;
const deviceInfo = app.coze.composer.canGetDeviceInfo
? await app.coze.composer.getDeviceInfo()
: undefined;
const subscription = app.coze.composer.canSubscribe
? await app.coze.composer.subscribe(nextSnapshot => {
renderComposerMirror(nextSnapshot);
})
: undefined;
renderComposerMirror(subscription?.snapshot ?? snapshot);
await subscription?.unsubscribe();
const removeSubscription = app.coze.composer.canSubscribeReferenceRemove
? await app.coze.composer.subscribeReferenceRemove(event => {
handleRemovedReferences(event.references, event.reason);
})
: undefined;
const submitSubscription = app.coze.composer.canSubscribeSubmit
? await app.coze.composer.subscribeSubmit(event => {
handleSubmitOutcome(event.outcome);
})
: undefined;
await removeSubscription?.unsubscribe();
await submitSubscription?.unsubscribe();
if (app.coze.composer.canSubmit) {
await app.coze.composer.submit();
}compose(insert) 在当前光标插入有序 content,并追加 references;compose(replace) 完整重建文本、
技能、文件、通用引用和回复引用。get/subscribe 始终返回完整快照,不返回差量或公开节点 ID。
技能展示名与图标、引用 provenance 都由 Host 注入,调用方只提供 skillId 和引用业务字段。
subscribeSubmit 的 failed outcome 表示已进入业务提交逻辑但提交失败。
Panel API
所有 Coze 扩展集中在稳定的 app.coze 命名空间下:
await app.coze.panel.open({
target: { panelId: 'detail' },
history: 'push',
payload: { itemId: '123' },
});
await app.coze.panel.open({
target: { pluginId: 'plg-builtin-files', panelId: 'main' },
history: 'replace',
});
await app.coze.panel.back();
await app.coze.panel.forward();省略 target.pluginId 表示当前插件;传入时按目标插件的稳定 ID 路由。default Panel 的 history 可选 push 或 replace,省略时等价于 push;back() / forward() 在边界返回 { moved: false }。一方与三方插件使用同一合同和权限,目标必须已存在于当前 Session 的可用 Panel 目录中。canOpen 只表示 Host 提供导航能力,不保证某个具体目标存在。
订阅 Panel 事件:
const offActivation = app.coze.panel.onActivation(activation => {
console.log(activation.payload);
console.log(activation.navigation.action);
});
offActivation();onActivation 会保留尚未消费的最新一次激活。业务加载、错误、只读和重试状态由 Panel 应用内部管理,
不通过 Host 协议上报。
Panel Header
移动端等 App Host 会在 Panel 之外渲染 Header。标题和显隐沿用既有 setHeader patch;传
title: null 恢复 Manifest 中的 Panel 名称:
if (app.coze.panel.canSetHeader) {
await app.coze.panel.setHeader({ visible: true, title: '文件详情' });
await app.coze.panel.setHeader({ title: null });
}自定义按钮也归入同一个 Panel Header API。按钮由 Host 渲染,点击通知不缓冲,因此先监听再展示:
const offHeaderAction = app.coze.panel.onHeaderAction(({ actionId }) => {
if (actionId === 'refresh') {
void reload();
}
});
if (app.coze.panel.canSetHeaderActions) {
await app.coze.panel.setHeaderActions({
actions: [
{ id: 'refresh', label: '刷新', icon: 'custom-refresh' },
{ id: 'more', label: '更多' },
],
});
}setHeaderActions 是全量替换,传 { actions: [] } 清空。每项只包含唯一 id、label 和可选的
非空字符串 icon;SDK 不限制按钮数量、文本长度或 icon 取值。Panel 销毁或不再需要监听时调用
offHeaderAction()。
安全区
安全区直接使用 MCP Apps 标准 hostContext.safeAreaInsets,不经过 Coze 自定义请求:
const insets = app.getHostContext()?.safeAreaInsets;
const offHostContext = app.subscribeHostContext(context => {
applySafeArea(context?.safeAreaInsets);
});Panel 销毁或不再需要监听时调用 offHostContext()。
Composer 显隐
移动端会话输入框就是 Composer。隐藏它只影响可见性,不清空草稿:
if (app.coze.composer.canSetVisibility) {
await app.coze.composer.setVisibility({ visible: false });
}Capability 检查
Coze 扩展是渐进增强能力。连接成功后,可以读取当前 Host 实际声明的细粒度 capability:
app.coze.composer.canInsert;
app.coze.composer.canInsertChip;
app.coze.composer.canReplace;
app.coze.composer.canAddReferences;
app.coze.composer.canUpdateReference;
app.coze.composer.canRemoveReferences;
app.coze.composer.canFocus;
app.coze.composer.canGet;
app.coze.composer.canSubscribe;
app.coze.composer.canSubscribeReferenceRemove;
app.coze.composer.canSubscribeSubmit;
app.coze.composer.canSubmit;
app.coze.composer.canSetVisibility;
app.coze.panel.canOpen;
app.coze.panel.canSetHeader;
app.coze.panel.canSetHeaderActions;
app.coze.desktop.canUploadFile;
app.getHostCapabilities()?.downloadFile;
app.getHostCapabilities()?.openLinks;这些值在连接前和连接关闭后均为 false。调用未声明的 Coze 能力不会自动降级,而是抛出
CozeCapabilityUnavailableError。
错误处理
import {
CozeCapabilityUnavailableError,
CozeHostError,
CozeInvalidArgumentError,
} from '@coze/app-sdk';
try {
await app.coze.composer.compose({
mode: 'insert',
content: [{ type: 'text', text: '写入草稿' }],
});
} catch (error) {
if (error instanceof CozeCapabilityUnavailableError) {
console.log('当前 Host 不支持该能力:', error.capability);
} else if (error instanceof CozeInvalidArgumentError) {
console.log('本地参数不合法:', error.message);
} else if (error instanceof CozeHostError) {
console.log('Host 请求失败:', error.method, error.code, error.data);
}
}标准 MCP Apps API 的返回值和异常保持官方语义;只有 Coze 扩展请求会映射为上述 Coze 错误。
所有请求方法均支持传入官方 RequestOptions,SDK 不会自动重试或静默切换到其他发送方式。
License
本项目使用 MIT License。第三方组件及许可证见 THIRD-PARTY-LICENSES.md。
