@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.1b5c337
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/steering/design-html/skills/charts/SKILL.md +4 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +71 -26
- package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +26 -14
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2831 -293
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +544 -22
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +19 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +991 -165
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +22 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +20 -12
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +364 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +76 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +528 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1814 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +600 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +315 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +421 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
- package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
- package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +37 -113
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +13 -2
- package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +7 -7
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +149 -24
- package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
- package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +265 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +158 -543
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +15 -1
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
- package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +77 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +76 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +182 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +197 -0
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
- package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md
CHANGED
|
@@ -68,7 +68,9 @@
|
|
|
68
68
|
```markdown
|
|
69
69
|
[Schema 摘录卡]
|
|
70
70
|
- pluginInstanceId / actionKey / outputMode
|
|
71
|
-
- input.required
|
|
71
|
+
- input.required: [字段名: 类型, ...]
|
|
72
|
+
- output.fields: [字段名: 类型, ...](每个字段必须在代码中被消费,未消费需注释说明原因)
|
|
73
|
+
- readme.constraints
|
|
72
74
|
- 调用侧决策: Client | Server
|
|
73
75
|
```
|
|
74
76
|
|
|
@@ -339,6 +341,18 @@ this.somePluginInstanceSideEffect(input).catch(error => {
|
|
|
339
341
|
|
|
340
342
|
---
|
|
341
343
|
|
|
344
|
+
### 调用错误分类与应对
|
|
345
|
+
|
|
346
|
+
| 错误类型 | 含义 | 应对策略 |
|
|
347
|
+
|----------|------|---------|
|
|
348
|
+
| `InputValidationError` | 入参不符合 schema | 修复参数后重试,不应出现在生产环境 |
|
|
349
|
+
| `RateLimitError` | 触发限流 | 指数退避重试(1s/2s/4s),最多 3 次 |
|
|
350
|
+
| `ExecutionError` | 插件执行失败 | 记录日志 + 降级方案(如规则计算)+ 通知用户 |
|
|
351
|
+
| `OutputValidationError` | 返回值不符合 schema | 记录异常返回 + 使用默认值或降级 |
|
|
352
|
+
| 网络超时 | 请求超时 | 重试 + 超时后降级 |
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
342
356
|
### outputMode 与调用侧选择
|
|
343
357
|
|
|
344
358
|
先通过 `get_plugin_ai_json(pluginInstanceId)` 获取 `actions[].outputMode`:
|
|
@@ -146,6 +146,20 @@
|
|
|
146
146
|
❌ searchRecords 拉全量 → find 找单条 → 必须用 getRecord
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
## 数据架构选择(应用读取多维表格数据时)
|
|
150
|
+
|
|
151
|
+
按**查询模式**选择架构:
|
|
152
|
+
|
|
153
|
+
| 查询模式 | 架构选择 | 实现方式 |
|
|
154
|
+
|---------|---------|---------|
|
|
155
|
+
| 展示/编辑单条记录 | 纯插件 | `getRecord` / `batchUpdateRecords` |
|
|
156
|
+
| 列表分页浏览 | 纯插件 | `searchRecords` + `pageToken` 游标分页 |
|
|
157
|
+
| 统计聚合(计数/求和/平均) | **纯插件 + aggregateQuery** | 禁止 searchRecords 全量拉取后内存计算 |
|
|
158
|
+
| 统计 + 明细下钻 | 纯插件 | 聚合用 `aggregateQuery`,下钻用 `searchRecords` + filter |
|
|
159
|
+
| 排行榜 / TOP N | 纯插件 | `searchRecords` + sort + pageSize=N |
|
|
160
|
+
| 复杂排序/多表关联/全文搜索 | 插件同步 + 本地数据库 | 定时/Webhook 同步到 postgres,复杂查询走数据库 |
|
|
161
|
+
| 高频写入 + 读取 | 本地数据库为主 | 多维表格仅作展示/备份 |
|
|
162
|
+
|
|
149
163
|
## 数据分析类Action补充说明
|
|
150
164
|
|
|
151
165
|
`aggregateQuery` 用于分组聚合统计,支持 dimensions(分组维度)和 measures(聚合计算)。
|
|
@@ -301,23 +315,25 @@ const chartData =
|
|
|
301
315
|
|
|
302
316
|
## 推荐 UI 组件
|
|
303
317
|
|
|
304
|
-
| BizType | 展示
|
|
305
|
-
| ---------------------------- |
|
|
306
|
-
| `User` | `<UserDisplay
|
|
307
|
-
| `Url` | `<Hyperlink value={val} readOnly />`
|
|
308
|
-
| `SingleSelect`/`MultiSelect` | 文本/标签
|
|
309
|
-
| `DateTime` | `new Date(ts).toLocaleDateString()`
|
|
310
|
-
| `Checkbox` | `value ? '是' : '否'`
|
|
311
|
-
| `Currency` | `¥${value.toFixed(2)}`
|
|
312
|
-
| `Progress` | `${value}%` 或进度条
|
|
313
|
-
| `Rating` | `'★'.repeat(value) + '☆'.repeat(5-value)`
|
|
318
|
+
| BizType | 展示 | 录入 |
|
|
319
|
+
| ---------------------------- | -------------------------------------------------------------------- | ------------------------------------------- |
|
|
320
|
+
| `User` | `<UserDisplay value={ids.map(id => id.toString())} />` | `<UserSelect multiple />` |
|
|
321
|
+
| `Url` | `<Hyperlink value={val} readOnly />` | `<Hyperlink readOnly={false} />` |
|
|
322
|
+
| `SingleSelect`/`MultiSelect` | 文本/标签 | `<Select>` 选项来自 enumValues |
|
|
323
|
+
| `DateTime` | `new Date(ts).toLocaleDateString()` | `<Input type="date" />` |
|
|
324
|
+
| `Checkbox` | `value ? '是' : '否'` | `<Checkbox />` |
|
|
325
|
+
| `Currency` | `¥${value.toFixed(2)}` | `<Input type="number" />` |
|
|
326
|
+
| `Progress` | `${value}%` 或进度条 | `<Input type="number" min={0} max={100} />` |
|
|
327
|
+
| `Rating` | `'★'.repeat(value) + '☆'.repeat(5-value)` | `<Input type="number" min={0} max={5} />` |
|
|
328
|
+
|
|
329
|
+
BizType 为 `User` 的字段展示必须使用 business-ui `UserDisplay`;不要直接渲染、拼接或 `join` 用户 ID 文本。
|
|
314
330
|
|
|
315
331
|
### 组件代码示例
|
|
316
332
|
|
|
317
333
|
```tsx
|
|
318
|
-
//
|
|
334
|
+
// 人员展示(business-ui UserDisplay 使用 value;不要传 users)
|
|
319
335
|
<UserDisplay
|
|
320
|
-
|
|
336
|
+
value={record['人员字段']?.map(userId => userId.toString()) || []}
|
|
321
337
|
size="small"
|
|
322
338
|
showLabel={true}
|
|
323
339
|
/>
|
|
@@ -365,13 +381,13 @@ const chartData =
|
|
|
365
381
|
const columns = [
|
|
366
382
|
// 文本字段 - 注意读取格式是 { text: string }
|
|
367
383
|
{ title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' },
|
|
368
|
-
// 人员字段 - 使用 UserDisplay
|
|
384
|
+
// 人员字段 - 使用 business-ui UserDisplay 组件,传 value(用户 ID 字符串数组),不要传 users
|
|
369
385
|
{
|
|
370
386
|
title: '负责人',
|
|
371
387
|
key: 'user',
|
|
372
388
|
render: (_, record) => (
|
|
373
389
|
<UserDisplay
|
|
374
|
-
|
|
390
|
+
value={record.record['人员字段']?.map((id) => id.toString()) || []}
|
|
375
391
|
size="small"
|
|
376
392
|
/>
|
|
377
393
|
),
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: raw-sql-boundary-audit
|
|
3
|
+
description: >-
|
|
4
|
+
Use when editing or reviewing NestJS backend database code that contains Drizzle raw `sql` tagged templates,
|
|
5
|
+
db.execute(sql`...`), report/statistics/aggregation endpoints, filter (...), CASE WHEN, window functions,
|
|
6
|
+
or custom type / Date range parameters.
|
|
7
|
+
steering: true
|
|
8
|
+
steering-inclusion: always
|
|
9
|
+
steering-topic: raw_sql_boundary_audit
|
|
10
|
+
match-template-name: nestjs-react-fullstack
|
|
11
|
+
unavailable-agents:
|
|
12
|
+
- AppInit
|
|
13
|
+
- SpecDoc
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Drizzle raw SQL 参数边界审计
|
|
17
|
+
|
|
18
|
+
本 skill 只管一件事:改 NestJS 后端数据库代码时,raw `sql` 模板里的 `${...}` 参数必须是数据库 driver 可直接接收的标量。Drizzle helper 绑定 schema 列会按列 encoder 处理;raw `sql` 模板参数不会自动调用列的 `toDriver`。
|
|
19
|
+
|
|
20
|
+
## 何时必须执行审计
|
|
21
|
+
|
|
22
|
+
满足任一条件就执行:
|
|
23
|
+
|
|
24
|
+
- 正在修改 `server/**/*.service.ts` 或 repository 中的数据库查询;
|
|
25
|
+
- 文件里有 `sql\``、`db.execute(sql`、`filter (...)`、`CASE WHEN`、窗口函数或复杂 where;
|
|
26
|
+
- 需求涉及统计、报表、趋势、排行、周期范围、截止时间、创建/完成时间等后端汇总接口。
|
|
27
|
+
|
|
28
|
+
## 审计步骤(写代码前和提交前各做一次)
|
|
29
|
+
|
|
30
|
+
1. 先读 `server/database/schema.ts`,确认相关列类型和 custom type。
|
|
31
|
+
2. 枚举当前 service/repository 里的 raw SQL 模板;不要只看刚新增的查询。可用搜索辅助,但结论以读代码为准:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
rg -n 'sql`|db\.execute\(sql|filter \(|CASE WHEN|OVER \(' server
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. 对每个 raw SQL 模板逐个检查 `${...}` 参数:
|
|
38
|
+
- 已是 string/number/boolean、ISO string、普通 ID、`sql.join(...)` 中逐个参数化的标量:可以保留;
|
|
39
|
+
- `user_profile` 是特殊 custom type,不适用“普通 ID 标量即可”的泛化判断;raw SQL 遇到 `user_profile` 列必须按 coding-guide 的三侧写法处理:读 `(col).user_id`,写 `ROW(${userId})::user_profile`,过滤 `(col).user_id = ${userId}`;禁止裸列 SELECT、`col = ${userId}`、`col::text = ${userId}` 或 `col = ROW(...)::user_profile` 比较;
|
|
40
|
+
- 是 `Date`、date/timestamptz/custom type 的运行时对象,或变量来源不明但表示时间/自定义结构:先转 driver-safe 标量再传入 raw SQL;日期时间用 `.toISOString()`;
|
|
41
|
+
- 如果能用 `gte(col, Date)` / `lt(col, Date)` / `eq(col, value)` / `insert().values()` / `update().set()` 等绑定 schema 列的 helper 表达,就优先改回 helper。
|
|
42
|
+
4. 保留 helper 安全路径:不要把 `gte(col, Date)`、`lt(col, Date)` 这类绑定列的 Drizzle helper 当成坏边界。
|
|
43
|
+
5. 改完后调用受影响的真实 API,至少覆盖一个会触发该 raw SQL 的范围/筛选参数;HTTP 200 且后端无参数序列化错误才算完成。
|
|
44
|
+
|
|
45
|
+
## 写法示例
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const rangeStart: Date = getRangeStart(range);
|
|
49
|
+
const rangeStartIso: string = rangeStart.toISOString();
|
|
50
|
+
|
|
51
|
+
// helper 绑定 schema 列,可以直接传 Date
|
|
52
|
+
where: gte(tasks.createdAt, rangeStart)
|
|
53
|
+
|
|
54
|
+
// raw SQL 参数先转 driver-safe 标量
|
|
55
|
+
total: sql<number>`
|
|
56
|
+
count(*) filter (where ${tasks.createdAt} >= ${rangeStartIso})
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
// 不要把 Date/custom type 运行时对象直接放进 raw SQL 模板参数
|
|
60
|
+
total: sql<number>`
|
|
61
|
+
count(*) filter (where ${tasks.createdAt} >= ${rangeStart})
|
|
62
|
+
`
|
|
63
|
+
```
|
package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
11
11
|
在 NestJS 服务端代码中使用 `FileService` 进行文件上传、下载、删除和管理。
|
|
12
12
|
|
|
13
13
|
> **使用注意**: 仅用于服务端文件处理场景,非必要文件上传下载场景请使用前端 `client-builtins-file-storage-service` skill。
|
|
14
|
-
> CLI
|
|
14
|
+
> **入口边界**:本 SDK 是**服务端代码**读写应用存储的入口;Agent 在对话 / 开发中自己上传或调试文件,先加载应用文件存储操作 skill(按「应用文件存储 / 文件上传 / 文件下载」召回),用其 CLI 命令操作,具体命令一律以该 skill 为准。二者与本 SDK 操作同一个应用存储桶,但**该 CLI 命令只供 Agent 在沙箱终端使用,禁止写进服务端代码**。
|
|
15
15
|
|
|
16
16
|
## 使用场景
|
|
17
17
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp-guide
|
|
3
|
+
description: "Use when 用户要把应用能力开放给外部 Agent / 工作伙伴 / MCP 客户端调用,新增或修改 MCP 工具,给工具结果加界面(MCP Apps / ui:// 资源),维护应用 Skill(server/mcp/SKILL.md),或要在开发态核对、调用、调试 MCP 工具。触发词:MCP, MCP Server, MCP 工具, MCP Apps, ui://, @McpTools, @McpTool, @McpUiResource, McpToolError, readMcpUiTemplate, miaoda-mcp validate, .spark/mcp/manifest.json, tools/list, tools/call, resources/read, mcp_call, 开放给 Agent, 工作伙伴调用, Agent 调用应用, 应用 Skill, server/mcp。不负责 HTTP 对外开放接口(走 openapi-guide)、自动化触发器(走 trigger-guide)。"
|
|
4
|
+
steering: true
|
|
5
|
+
steering-topic: mcp_guide
|
|
6
|
+
match-template-name: nestjs-react-fullstack
|
|
7
|
+
gate-tools:
|
|
8
|
+
- tool: mcp_call
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# MCP 开发与调试
|
|
12
|
+
|
|
13
|
+
应用的 MCP Server 是全栈工程里多出来的一层出口:同仓、同域名、同版本。SDK(`@lark-apaas/fullstack-nestjs-core` 重导出 `@lark-apaas/nestjs-mcp`)已经内置端点 `POST /__innerapi__/mcp`、Streamable HTTP 协议、zod 校验、身份读取、清单导出与 `ui://` 资源下发。你只写三种文件:工具类、界面入口、应用 Skill。
|
|
14
|
+
|
|
15
|
+
**真值顺序**:工程内 `node_modules/@lark-apaas/nestjs-mcp/` 的 `README.md` 与 `dist/index.d.ts` > 本文。装饰器选项、类型名、返回值形状以 `index.d.ts` 为准;本文与它冲突时按包改,不按本文改。
|
|
16
|
+
|
|
17
|
+
## 固定路径(不探测)
|
|
18
|
+
|
|
19
|
+
| 路径 | 内容 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `server/mcp/tools/<域>.tools.ts` | 工具类:一个业务域一个文件,`@McpTools()` 类 + 多个 `@McpTool()` 方法 |
|
|
22
|
+
| `client/mcp-ui/<entry>/index.html`(+ `main.tsx`) | MCP Apps 界面源码,Vite 预设自动打成 `dist/mcp-ui/<entry>.html` |
|
|
23
|
+
| `server/mcp/SKILL.md` | 应用 Skill:给消费方 Agent 看的用法说明,SDK 注册为 `skill://app/SKILL.md` |
|
|
24
|
+
| `.spark/mcp/manifest.json` | SDK 生成的离线清单,随代码提交,不手改 |
|
|
25
|
+
|
|
26
|
+
`server/mcp/` 不存在即为首次开通;开通只需建上述文件并把工具类注册为所属业务模块的 provider。`McpModule` 已由 `PlatformModule.forRoot()` 引入,SDK 启动时自动发现工具类——`app.module.ts` 不动,不写注册表,不写 `tools/index.ts`。
|
|
27
|
+
|
|
28
|
+
三份 Markdown 别混:本文(教你写代码)、`server/mcp/SKILL.md`(应用产物,教下游 Agent 用工具)、SDK README(真值)。不把本文复制成应用 Skill,应用 Skill 不加 `steering: true`。
|
|
29
|
+
|
|
30
|
+
## 前置检查(一条命令)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
ls -d node_modules/@lark-apaas/nestjs-mcp node_modules/@lark-apaas/coding-preset-vite-react/lib/plugins/mcp-ui.js
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- 缺 `@lark-apaas/nestjs-mcp` → 当前 `@lark-apaas/fullstack-nestjs-core` 不含 MCP 层(`1.1.62` 起含)。告知用户升级 core,停止;不装 `@modelcontextprotocol/sdk`、不手写端点。
|
|
37
|
+
- 缺 `mcp-ui.js` → 只能做工具、不能做界面(`@lark-apaas/coding-preset-vite-react` `1.0.23` 起含)。要做界面先让用户升级该预设。Rspack 预设不支持 MCP Apps。
|
|
38
|
+
|
|
39
|
+
## 任务路由
|
|
40
|
+
|
|
41
|
+
| 任务 | 读 |
|
|
42
|
+
|---|---|
|
|
43
|
+
| 新增 / 修改工具、开通 MCP、维护应用 Skill | `references/tool-authoring.md` |
|
|
44
|
+
| 给工具结果加界面、改界面、界面与工具联动 | `references/mcp-apps.md` |
|
|
45
|
+
| 用 `mcp_call` 核对与调用、按错误定位、本地模式调试 | `references/debugging.md` |
|
|
46
|
+
|
|
47
|
+
改完必跑 `npx miaoda-mcp validate`,再按 `references/debugging.md` 的回路用 `mcp_call` 验证;三类任务共用。`mcp_call` 报「当前运行环境不支持」是本地 CLI 宿主,走该文件的本地模式。向用户汇报用业务表述(「找到 4 个演示航班」),不贴 JSON。
|
|
48
|
+
|
|
49
|
+
## 不变量
|
|
50
|
+
|
|
51
|
+
1. schema 用 zod,提成常量放类上方;方法签名固定 `(input: Infer<typeof In>, ctx: McpContext)`,返回 `McpToolResult<typeof Out>`。签名写错在 `npm run type:check:server` 暴露。
|
|
52
|
+
2. `description` 面向模型:用途、何时调用、参数来源、返回什么、失败怎样。每个 zod 字段 `.describe()`。消费方 Agent 只靠这些选工具、填参数。
|
|
53
|
+
3. 四项 `annotations` 显式写出。只读工具必须 `readOnlyHint: true`:缺省会被当成可能写数据,调试时每次都要 `safe: true`,消费方也会额外确认。
|
|
54
|
+
4. 声明了 `outputSchema` 就返回匹配的 `structuredContent`,对不上时 `mcp_call` 报「输出与其声明的 outputSchema 不符」;列表工具入参带 `limit` 与 `cursor`;写操作的幂等由 Service 保证,不引入幂等键字段。
|
|
55
|
+
5. 业务失败抛 `McpToolError(message, { code })`,Agent 原样收到;其他异常只落日志、返回通用提示,Agent 看不到原因。
|
|
56
|
+
6. 身份只从 `ctx.user` 读;权限与行级过滤留在 Service。工具类作为业务模块 provider 注册,单例。
|
|
57
|
+
7. 工具名 = `prefix + 方法名`,匹配 `^[A-Za-z0-9_.-]{1,128}$`,全应用唯一;用 `域_动作`。
|
|
58
|
+
8. 界面源码只放 `client/mcp-ui/<entry>/`,用 `@McpUiResource` + `readMcpUiTemplate('<entry>')` 下发,工具用 `ui: { resourceUri }` 关联。
|
|
59
|
+
9. 新增、改名、删除工具后同步改 `server/mcp/SKILL.md`。
|
|
60
|
+
|
|
61
|
+
## 禁止
|
|
62
|
+
|
|
63
|
+
- 从入参接收用户 ID、token、连接串——身份来自网关,入参里的身份可伪造。
|
|
64
|
+
- 把 `requireUser` 设为 `false` 来让调用通过——`MCP_USER_REQUIRED` 说明请求没经过带身份的入口,修入口不修配置。
|
|
65
|
+
- 手改 `.spark/mcp/manifest.json`——`validate --check` 会判不一致。
|
|
66
|
+
- 界面源码放 `server/` 下——被服务端 tsconfig 一起编译,JSX 报错。
|
|
67
|
+
- 界面里存凭证、直连业务系统——沙箱 iframe 是独立 origin,用户登录态带不进来;数据走宿主转发的工具调用。
|
|
68
|
+
- `McpToolError.data` 与 `structuredContent` 里放密钥、内部连接信息——两者原样进 Agent 上下文。
|
|
69
|
+
- 自己实现 MCP 协议、JSON-RPC、`/mcp` 路由、凭证校验——SDK 与网关已做,重复实现会与网关映射冲突。
|
|
70
|
+
|
|
71
|
+
## 交付前核对
|
|
72
|
+
|
|
73
|
+
1. `validate` 通过;`list_tools` 与源码里的工具、资源一一对应。
|
|
74
|
+
2. 每个工具有面向模型的 `description`、四项 `annotations`;写工具有 `outputSchema`。
|
|
75
|
+
3. 每个 `ui.resourceUri` 能 `read_resource` 读到,界面拿到 `structuredContent` 后能渲染。
|
|
76
|
+
4. `server/mcp/SKILL.md` 覆盖全部工具:何时调用、如何选工具、调用顺序、约束、失败处理、界面 URI。
|
|
77
|
+
5. `.spark/mcp/manifest.json` 随代码提交。
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 调试:mcp_call、错误定位、本地模式
|
|
2
|
+
|
|
3
|
+
## mcp_call
|
|
4
|
+
|
|
5
|
+
云端沙箱里的开发态 MCP 调试工具。它经 MCP 网关连当前应用的开发态 MCP Server,以当前用户身份发起,与预览页里操作应用的身份一致。
|
|
6
|
+
|
|
7
|
+
| action | 参数 | 返回 |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `list_tools` | 无 | server 名、每个工具的 name / title / description / annotations / `_meta.ui` / inputSchema(长 schema 截断)、资源清单 |
|
|
10
|
+
| `call` | `tool_name`(带 prefix 的完整名)、`arguments`(JSON 对象**字符串**,缺省 `{}`)、`safe` | 文本内容、`structuredContent`(截断)、`isError`、耗时 |
|
|
11
|
+
| `read_resource` | `resource_uri`(如 `ui://order/list`、`skill://app/SKILL.md`) | uri、mimeType、`_meta.ui`、正文前若干字符 |
|
|
12
|
+
|
|
13
|
+
- `safe`:工具未声明 `annotations.readOnlyHint: true` 时,`call` 在发起前被拒为 `requires_safe`。确认这次调用只影响你自建的测试数据后再传 `safe: true`;用户已有的真实数据未经明确要求不动。只读工具反复撞这条,是源码漏了 `readOnlyHint: true`,补注解而不是加 `safe`。
|
|
14
|
+
- 单个子请求 60s 无动静即超时,整次动作 600s 硬上限。server 是无状态 JSON 模式(不开 SSE),`ctx.extra.sendNotification` 发的 `notifications/progress` 到不了客户端、不会重置计时;超时只能靠缩小单次处理范围。
|
|
15
|
+
- 同一工具连续失败 4 次熔断 60s:先改代码或入参,不是等。`requires_safe`、`tool_not_found`、`aborted` 不计入熔断。
|
|
16
|
+
- 失败或 `isError: true` 时结果尾部附 Trace-ID 对应的 `server.log` / `trace.log` 片段。
|
|
17
|
+
|
|
18
|
+
## 回路
|
|
19
|
+
|
|
20
|
+
1. 改工具 / 资源 / 界面。
|
|
21
|
+
2. `npx miaoda-mcp validate`。失败即改,不进下一步。
|
|
22
|
+
3. 等 dev server 重启(`nest --watch` 会自动重启;界面改动由 `[mcp-ui]` 插件自动重建)。
|
|
23
|
+
4. `list_tools` 核对清单与源码一致(`visibility: ['app']` 的工具也在清单里,属正常)。
|
|
24
|
+
5. `call` 逐个工具跑一次真实入参:先只读,再写(`safe: true`)。写工具跑两次验证幂等语义。
|
|
25
|
+
6. 带界面的工具 `read_resource`。
|
|
26
|
+
7. 每个 `McpToolError` 分支构造一次触发(不存在的 ID、非法状态),确认 `isError: true` 且 `code` 与 `description` 预告一致。
|
|
27
|
+
8. 更新 `server/mcp/SKILL.md`,`read_resource` `skill://app/SKILL.md` 确认最新正文已生效。
|
|
28
|
+
|
|
29
|
+
## 错误分流
|
|
30
|
+
|
|
31
|
+
| 现象 | 原因 | 动作 |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| 端点 404「当前工程可能尚未接入 MCP 层,或 dev server 尚未就绪」 | `server/mcp` 不存在;dev server 没起来;零工具零资源无应用 Skill(SDK 此时返回 404) | 确认三者;有工具但 404 看启动日志是否被校验阻断 |
|
|
34
|
+
| `list_tools` 少了某个工具 | 类没进任何模块 `providers`,无任何日志提示 | 对照启动日志「已注册 N 个 MCP 工具:…」,缺的加进所属业务模块的 `providers` |
|
|
35
|
+
| dev server 起不来,报「MCP 工具必须使用单例及单例依赖」 | 工具类或其依赖是 request / transient 作用域 | 改回单例,身份从 `ctx.user` 取 |
|
|
36
|
+
| 启动日志「MCP 定义校验失败」 | 工具名不合法 / 重名、资源 URI 不以 `ui://` 开头 / 重名、`ui.resourceUri` 找不到 `@McpUiResource()`、同一方法同时标两个装饰器 | 按日志逐条修,`validate` 能提前查出 |
|
|
37
|
+
| `tool_not_found` | 名字没带 prefix,或用了 `title` | 用 `list_tools` 里的 name |
|
|
38
|
+
| `requires_safe` | 见上文 `safe` | 只读工具补注解;写工具确认测试数据后 `safe: true` |
|
|
39
|
+
| `isError` + `code: MCP_USER_REQUIRED` | 请求未携带用户身份 | 请求没经过网关 / 开发服务入口。`mcp_call` 自带身份,正常不会撞到;撞到说明链路有问题,不改 `requireUser` |
|
|
40
|
+
| `isError` + 业务 `code` | 工具按设计抛了 `McpToolError` | 对照 `description` 检查是否预告了该 code;入参是否真的该触发 |
|
|
41
|
+
| `isError` + 通用失败提示 | 方法抛了非 `McpToolError` 的异常 | 读结果尾部 Trace-ID 日志定位;该异常若是可预期的业务失败,改抛 `McpToolError` |
|
|
42
|
+
| zod 校验错误 | 入参不符 inputSchema | 对照 `list_tools` 里的 schema 改 `arguments`;schema 本身不合理就改源码 |
|
|
43
|
+
| 「工具已执行,但输出与其声明的 outputSchema 不符」 | `structuredContent` 与 `outputSchema` 对不上 | 工具已跑通:改返回值或改 schema,不重试同一调用 |
|
|
44
|
+
| `timeout` | 单个子请求 60s 无动静,或整次超 600s | 收窄入参 / 分页 / 拆成「发起 + 查状态」两个工具;加进度通知无效(JSON 模式发不出去) |
|
|
45
|
+
| `connect_failed` / `http_error` / `unauthorized` | 网关或预览环境问题 | 看 statusCode;不是应用代码问题时告知用户,不改代码绕 |
|
|
46
|
+
| 「已连续 4 次调用失败,已暂停对它的重复调用」 | 同一工具连续失败 4 次熔断 | 先修实现或入参,60s 后再验 |
|
|
47
|
+
| 「当前运行环境不支持开发态 MCP 调试」 | 本地 CLI 宿主没有 `mcp_call` 后端 | 走下节本地模式 |
|
|
48
|
+
| `read_resource` 报「读取 MCP Apps 界面产物失败」 | `dist/mcp-ui/<entry>.html` 不存在 | 入口名对齐 `readMcpUiTemplate('<entry>')`;确认 `client/mcp-ui/<entry>/index.html` 存在;看 `[mcp-ui]` 日志是否构建失败 |
|
|
49
|
+
|
|
50
|
+
同一失败不原样重试。改完代码再验,每轮只改一个变量。
|
|
51
|
+
|
|
52
|
+
## 本地模式(IDE Agent / 本地 CLI)
|
|
53
|
+
|
|
54
|
+
本地开发(`MIAODA_LOCAL_DEV=1`,`npm run dev` 走 `dev-local.js`)没有 `mcp_call`,MCP 客户端是你自己或 IDE 里的 Agent。
|
|
55
|
+
|
|
56
|
+
- 带身份的入口只有 dev server:`http://localhost:<CLIENT_DEV_PORT><CLIENT_BASE_PATH>/__innerapi__/mcp`(本地 `CLIENT_BASE_PATH` 通常为空)。dev server 代理把 `+env-pull` 拉到的 `SUDA_WEBUSER` 注入为身份头,工具里的 `ctx.user` 就是开发者本人。直连 NestJS 端口没有身份,会撞 `MCP_USER_REQUIRED`。
|
|
57
|
+
- 标准 MCP 客户端配置 Streamable HTTP、URL 填上面地址即可连接。
|
|
58
|
+
- 看原始请求响应:`npx @modelcontextprotocol/inspector`,指向同一地址。
|
|
59
|
+
- 快速验证:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
curl -s -X POST 'http://localhost:<port><base>/__innerapi__/mcp' \
|
|
63
|
+
-H 'content-type: application/json' \
|
|
64
|
+
-H 'accept: application/json, text/event-stream' \
|
|
65
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`accept` 必须同时含两种类型,否则 406。`GET` / `DELETE` 返回 405,端点只接受 `POST`。
|
|
69
|
+
|
|
70
|
+
- 平台目录接口 `GET <入口>/__innerapi__/mcp/manifest` 返回工具、资源、应用 Skill 正文与源码定位,是面板用的聚合接口,不是工具调用接口。
|
|
71
|
+
|
|
72
|
+
## 汇报
|
|
73
|
+
|
|
74
|
+
- 用业务表述:「`order_search` 返回 3 条待支付订单」「取消不存在的订单返回 ORDER_NOT_FOUND,符合预期」;不贴整段 JSON。
|
|
75
|
+
- 写操作说明动了哪些测试数据。
|
|
76
|
+
- 未验证的分支明说未验证,不写「已全部通过」。
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# MCP Apps:给工具结果加界面
|
|
2
|
+
|
|
3
|
+
一个 MCP App = 一个 `ui://` 资源(单文件 HTML)+ 至少一个用 `ui.resourceUri` 指向它的工具。宿主调用工具后把 HTML 放进沙箱 iframe 渲染,再把工具入参与结果通过通知推给界面;界面里的操作经宿主回调 server 的工具。
|
|
4
|
+
|
|
5
|
+
## 先决定要不要界面
|
|
6
|
+
|
|
7
|
+
| 信号 | 做法 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 结果是列表 / 详情 / 状态,文本 + `structuredContent` 已够 Agent 继续推理 | 不加界面 |
|
|
10
|
+
| 用户要从多条候选里挑一个(航班、订单、文件) | 选择型界面,选中后 `updateModelContext` 同步给模型 |
|
|
11
|
+
| 结果是图表、地图、对比等空间信息 | 展示型界面 |
|
|
12
|
+
| 写操作前要让用户在界面里核对、确认 | 确认型界面,确认动作经 `callServerTool` 调一个 `visibility: ['app']` 的工具 |
|
|
13
|
+
|
|
14
|
+
不需要的地方不加:文本更快,也没有构建与 CSP 负担。
|
|
15
|
+
|
|
16
|
+
## 目录与构建
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
client/mcp-ui/<entry>/
|
|
20
|
+
├── index.html # 入口骨架,见下
|
|
21
|
+
└── main.tsx # 或 main.ts;React 或原生 JS 都行
|
|
22
|
+
dist/mcp-ui/<entry>.html # 预设产物,@McpUiResource 经 readMcpUiTemplate 读取
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<!-- client/mcp-ui/<entry>/index.html -->
|
|
27
|
+
<!doctype html>
|
|
28
|
+
<html><head><meta charset="utf-8" /></head>
|
|
29
|
+
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`<div id="root">` 不能少:缺了 `createRoot(document.getElementById('root')!)` 拿到 null,iframe 全白,而 `validate` 与 `read_resource` 都不会报错。
|
|
33
|
+
|
|
34
|
+
- `<entry>` 只允许 `A-Z a-z 0-9 _ -`。
|
|
35
|
+
- Vite 预设的 `mcp-ui` 插件:检测到 `client/mcp-ui/` 就为每个入口跑一次独立子构建,JS / CSS / 资源全部内联成一个 HTML。开发态监听 `client/`、`shared/` 与入口目录,改完自动重建;生产在主构建之后构建,并同步到发布目录 `dist/server/dist/mcp-ui`。
|
|
36
|
+
- 子构建只带 React 插件与 `@shared` 别名:`@/`、`@client/*`、`@server/*` 解析不到,不支持 styled-jsx。样式写在入口自己的 CSS 里,不依赖主应用的全局样式与 Tailwind;颜色字体优先读宿主下发的 CSS 变量(`useHostStyles`)。
|
|
37
|
+
- 界面代码归 `tsconfig.app.json` 管(浏览器代码)。放到 `server/` 下会被服务端 tsconfig 编译报 JSX 错误。
|
|
38
|
+
- `@modelcontextprotocol/ext-apps` 随 SDK 安装,直接 import。解析失败时把它加进 `package.json` `dependencies`,版本与 `node_modules/@lark-apaas/nestjs-mcp/package.json` 里声明的一致(1.x),不装 2.x。
|
|
39
|
+
|
|
40
|
+
## server 侧:声明资源并关联工具
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { McpTools, McpTool, McpUiResource, readMcpUiTemplate } from '@lark-apaas/fullstack-nestjs-core';
|
|
44
|
+
|
|
45
|
+
@McpTools({ prefix: 'order_' })
|
|
46
|
+
export class OrderMcpTools {
|
|
47
|
+
@McpTool({
|
|
48
|
+
title: '查询订单',
|
|
49
|
+
description: '…',
|
|
50
|
+
inputSchema: SearchOrdersInput,
|
|
51
|
+
outputSchema: SearchOrdersOutput,
|
|
52
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
53
|
+
ui: { resourceUri: 'ui://order/list' }, // 结果由该界面渲染;模型与界面都可调
|
|
54
|
+
})
|
|
55
|
+
async search(/* … */) { /* … */ }
|
|
56
|
+
|
|
57
|
+
@McpTool({
|
|
58
|
+
description: '按 ID 取一条订单明细,供订单列表界面展开详情时调用。',
|
|
59
|
+
inputSchema: GetOrderInput,
|
|
60
|
+
outputSchema: GetOrderOutput,
|
|
61
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
62
|
+
ui: { resourceUri: 'ui://order/list', visibility: ['app'] }, // 只供界面回调,模型工具清单里隐藏
|
|
63
|
+
})
|
|
64
|
+
async detail(/* … */) { /* … */ }
|
|
65
|
+
|
|
66
|
+
@McpUiResource({
|
|
67
|
+
uri: 'ui://order/list',
|
|
68
|
+
title: '订单列表',
|
|
69
|
+
description: '分页订单列表,可展开详情、勾选后取消。',
|
|
70
|
+
csp: { connectDomains: [], resourceDomains: [] }, // 界面要访问的外部域名写这里,默认全禁
|
|
71
|
+
})
|
|
72
|
+
listView() {
|
|
73
|
+
return readMcpUiTemplate('order-list'); // 读 dist/mcp-ui/order-list.html
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `uri` 必须以 `ui://` 开头,全应用唯一;`ui.resourceUri` 找不到对应 `@McpUiResource()` 时 `validate` 与启动都会报错。
|
|
79
|
+
- `visibility` 默认 `['model', 'app']`。只给界面用的取数 / 刷新工具写 `['app']`,避免污染模型的工具清单。隐藏由宿主做,server 不过滤:`mcp_call` 的 `list_tools` 仍会列出它,`_meta.ui.visibility` 为 `['app']` 即正确,不要因为「还在清单里」去改代码。
|
|
80
|
+
- 一个工具既返回 `structuredContent` 又绑界面是常态;模型还需要纯数据继续推理时,拆成「取数工具(无 ui)」+「展示工具(绑 ui)」。
|
|
81
|
+
- 资源读取沿用 `requireUser`:无身份的 `resources/read` 会被拒。
|
|
82
|
+
|
|
83
|
+
## 界面侧:接结果、回调工具
|
|
84
|
+
|
|
85
|
+
React 写法(`@modelcontextprotocol/ext-apps/react`):
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// client/mcp-ui/order-list/main.tsx
|
|
89
|
+
import { useState } from 'react';
|
|
90
|
+
import { createRoot } from 'react-dom/client';
|
|
91
|
+
import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react';
|
|
92
|
+
// 与 outputSchema 对齐的 TS 类型放 shared/(如 shared/mcp-types.ts),server 与界面共用;@shared 是子构建唯一可用别名
|
|
93
|
+
import type { OrderPage as Page } from '@shared/mcp-types';
|
|
94
|
+
|
|
95
|
+
function OrderList() {
|
|
96
|
+
const [page, setPage] = useState<Page | null>(null);
|
|
97
|
+
const [error, setError] = useState<string | null>(null);
|
|
98
|
+
|
|
99
|
+
const { app, isConnected } = useApp({
|
|
100
|
+
appInfo: { name: 'order-list', version: '1.0.0' },
|
|
101
|
+
capabilities: {},
|
|
102
|
+
onAppCreated: (app) => {
|
|
103
|
+
// 宿主推送:工具入参(可先渲染骨架)、工具结果、取消
|
|
104
|
+
app.ontoolinput = ({ arguments: args }) => console.debug('input', args);
|
|
105
|
+
app.ontoolresult = (result) => {
|
|
106
|
+
if (result.isError) setError(textOf(result));
|
|
107
|
+
else setPage(result.structuredContent as Page);
|
|
108
|
+
};
|
|
109
|
+
app.ontoolcancelled = () => setError('调用已取消');
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
useHostStyles(app, app?.getHostContext()); // 应用宿主主题、CSS 变量与字体;第二参数让首帧就生效
|
|
113
|
+
|
|
114
|
+
async function cancel(orderId: string) {
|
|
115
|
+
if (!app) return;
|
|
116
|
+
const res = await app.callServerTool({ name: 'order_cancel', arguments: { orderId } });
|
|
117
|
+
if (res.isError) return setError(textOf(res));
|
|
118
|
+
// 把用户在界面里做的选择同步给模型,模型下一轮据此继续
|
|
119
|
+
await app.updateModelContext({
|
|
120
|
+
content: [{ type: 'text', text: `用户已在界面中取消订单 ${orderId}` }],
|
|
121
|
+
structuredContent: { cancelledOrderId: orderId },
|
|
122
|
+
});
|
|
123
|
+
setPage((p) => p && { ...p, items: p.items.filter((i) => i.id !== orderId) });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!isConnected) return <p>连接宿主中…</p>;
|
|
127
|
+
if (error) return <p role="alert">{error}</p>;
|
|
128
|
+
if (!page) return <p>等待查询结果…</p>;
|
|
129
|
+
return (
|
|
130
|
+
<ul>
|
|
131
|
+
{page.items.map((o) => (
|
|
132
|
+
<li key={o.id}>
|
|
133
|
+
{o.customer} · {o.amount} 元 · {o.status}
|
|
134
|
+
{o.status === 'pending' && <button onClick={() => cancel(o.id)}>取消</button>}
|
|
135
|
+
</li>
|
|
136
|
+
))}
|
|
137
|
+
</ul>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function textOf(result: { content?: Array<{ type: string; text?: string }> }) {
|
|
142
|
+
return result.content?.find((c) => c.type === 'text')?.text ?? '调用失败';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
createRoot(document.getElementById('root')!).render(<OrderList />);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
原生 JS 写法(`@modelcontextprotocol/ext-apps`):
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { App } from '@modelcontextprotocol/ext-apps';
|
|
152
|
+
const app = new App({ name: 'order-list', version: '1.0.0' }, {});
|
|
153
|
+
app.ontoolresult = (result) => render(result.structuredContent);
|
|
154
|
+
await app.connect();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
宿主能力速查(`App` 实例方法):
|
|
158
|
+
|
|
159
|
+
| 方法 | 用途 |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `ontoolinput` / `ontoolinputpartial` / `ontoolresult` / `ontoolcancelled` | 宿主推送的入参(完整 / 流式)、结果、取消 |
|
|
162
|
+
| `callServerTool({ name, arguments })` | 经宿主调本 server 的工具;工具名带 prefix;返回标准 `CallToolResult` |
|
|
163
|
+
| `updateModelContext({ content?, structuredContent? })` | 把界面状态同步给模型,不触发模型立刻回复 |
|
|
164
|
+
| `sendMessage({ role: 'user', content })` | 以用户身份发一条消息,触发模型回复 |
|
|
165
|
+
| `getHostContext()` / `onhostcontextchanged` | 主题、`displayMode`、`containerDimensions`、`locale`、`timeZone`、发起本次渲染的 `toolInfo` |
|
|
166
|
+
| `requestDisplayMode({ mode })` | 请求 inline / fullscreen 等展示模式,以 `availableDisplayModes` 为限 |
|
|
167
|
+
|
|
168
|
+
`useApp` 默认开自动调高(`autoResize`),内容变化不用手动发尺寸。
|
|
169
|
+
|
|
170
|
+
## 界面约束
|
|
171
|
+
|
|
172
|
+
- 不存凭证、不读 cookie / localStorage 里的登录态:iframe 是独立 origin,用户在业务系统的登录态带不进来。所有数据走 `callServerTool`,身份由宿主转发。
|
|
173
|
+
- 外部域名写进 `@McpUiResource` 的 `csp`:`connectDomains`(fetch / WebSocket)、`resourceDomains`(图片、脚本、样式)、`frameDomains`(内嵌 iframe)、`baseUriDomains`;默认全禁,漏写表现为资源静默加载失败。
|
|
174
|
+
- 业务返回值用 `textContent` 或 JSX 文本渲染,不拼 `innerHTML`。
|
|
175
|
+
- 每次调用重新注入数据;界面不假设自己能跨调用保留状态。
|
|
176
|
+
- 宿主不支持 MCP Apps 时只显示 `content` 文本,所以带界面的工具也要返回可读的文本摘要。
|
|
177
|
+
|
|
178
|
+
## 调试
|
|
179
|
+
|
|
180
|
+
- `mcp_call` `read_resource` 读 `ui://…`:成功返回 `text/html;profile=mcp-app` 与正文开头;报「读取 MCP Apps 界面产物失败」→ `dist/mcp-ui/<entry>.html` 不存在:入口名与 `readMcpUiTemplate('<entry>')` 不一致、`client/mcp-ui/<entry>/index.html` 缺失、或 dev server 还没重建,看 dev server 日志里 `[mcp-ui]` 前缀的行。
|
|
181
|
+
- 界面逻辑用 `mcp_call` 看不到;先用 `call` 确认 `structuredContent` 形状正确,再在预览里看渲染。宿主渲染失败时保留工具结果,只重试界面。
|
|
182
|
+
- 界面里调 `callServerTool` 失败,错误也走 server 日志:按 Trace-ID 查。
|