# 豆包智能服务 MCP 协议与业务建模

## 豆包智能服务 MCP 协议设计

本 Skill 用于把业务 MCP tool 的返回协议和豆包模型/卡片链路接起来。目标产物通常包括：

- MCP tool 做什么、输入是什么、业务 ToolResult 返回是什么。
- Manifest 中 `entities`、`tool_card_binding`、`tools.output` 如何配置。
- 哪些业务字段给模型看，哪些字段只给卡片代码或后续业务动作使用。

协议写完后，如果要确认出卡数据链路，先使用 `dbx simulator eval` 跑通内置 demo，再在业务项目中用 `dbx simulator eval --query "<当前轮用户问题>"` 验证 MCP result、Manifest、tool_card_binding 和 `card_delta` 都正确；如果只是打开 Web 模拟器验证前端渲染或交互，直接执行 `dbx dev`。

需要从业务对象出发决定 entity 拆分、模板选型、模型可见字段或动作结果形态时，先读 [业务建模方法](#业务建模方法)，产出 `entities_designed`。需要设计列表卡、列表 item 是否给模型筛选、`llm_modifiable` 或 item 回填时，再读 [列表卡协议字段](#列表卡协议字段)。

### 协议边界

- 业务 MCP tool 返回 MCP ToolResult 风格的 `content`、`structuredContent`、`isError`。
- Manifest 是平台配置：声明工具输出类型、业务实体 schema、模型可见字段、模型可修改字段，以及“当前工具返回某类 entity 时使用哪个前端 `widget_id` 出卡”。
- `tool_card_binding` 的 value 必须是前端工程 `defineAppConfig({ widgets })` 中声明或由简写推导出的 `widget_id`，不是 `@doubao-apps/template` 的模板组件名。
- 卡片字段展示、按钮、跳转、列表排版、降级策略都属于业务卡片代码职责，不写在 Manifest 中。
- 业务返回只表达业务事实；工具输出类型和卡片 widget 引用写在 Manifest 中。

### 业务 MCP tool 返回值 schema

业务 MCP tool 返回 MCP ToolResult 风格结构：

```json
{
  "content": [{ "type": "text", "text": "面向模型和用户的自然语言描述" }],
  "structuredContent": {},
  "isError": false
}
```

顶层字段规则：

- `structuredContent`：必填，JSON object。成功时放业务结果；失败时放标准错误对象。
- `content`：可选，推荐返回。自然语言描述写在 `content[].text`。
- `isError`：可选，缺失按 `false` 处理；只有显式 `true` 表示业务错误分支。

#### entities 成功返回

当 `tools.<tool>.output.kind=entities` 时，业务返回入口固定为 `structuredContent.entities`。每个顶层 entity 必须携带 `entity_type`，并携带 Manifest 中通过 `is_entity_id: true` 标记的稳定业务 ID 字段。

```json
{
  "content": [{ "type": "text", "text": "找到 1 条可用路线。" }],
  "structuredContent": {
    "entities": [{
      "entity_id": "route_001",
      "entity_type": "route",
      "title": "公司到机场",
      "duration_minutes": 42,
      "price_yuan": 128,
      "summary": "预计 42 分钟到达",
      "deeplink": "page/route?id=route_001"
    }]
  },
  "isError": false
}
```

#### execution_result 成功返回

当 `tools.<tool>.output.kind=execution_result` 时，业务 `structuredContent` 直接返回执行结果 object，不生成 entity，也不出卡。

```json
{
  "content": [{ "type": "text", "text": "订阅成功。" }],
  "structuredContent": {
    "success": true,
    "message": "订阅成功"
  }
}
```

#### 错误返回

业务失败时返回 `isError=true`，并在 `structuredContent` 中返回数字错误码和错误文案。

```json
{
  "content": [{ "type": "text", "text": "缺少目的地。" }],
  "structuredContent": {
    "code": 40001,
    "message": "缺少目的地"
  },
  "isError": true
}
```

错误分支约束：

- `structuredContent.code`：必填，number。
- `structuredContent.message`：必填，string。
- 错误分支不生成 entity，也不出卡。

### 核心概念

#### entities

`entities` 描述业务实体类型、字段 schema、模型可见性、模型可修改性和卡片 widget 引用。

```yaml
entities:
  route:
    schema:
      entity_id: { type: string, description: 路线业务 ID, is_entity_id: true, is_refresh_key: true }
      entity_type: { type: string, description: 业务实体类型, llm_visible: false }
      title: { type: string, description: 路线标题 }
      duration_minutes: { type: int, description: 预计耗时 }
      price_yuan: { type: int, description: 预估价格 }
      summary: { type: string, description: 路线摘要 }
      deeplink: { type: string, description: 智能服务跳转链接, llm_visible: false }
    tool_card_binding:
      search_routes: route_result_card
    history_card_refresh:
      enabled: true
      scope: same_conversation
      refresh_to: fixed_style
```

字段规则：

- `entity_type` 是业务实体类型，业务返回中必带，并且必须命中 Manifest 中声明的实体类型。
- `is_entity_id: true` 标记顶层 entity 的稳定业务 ID。字段名由业务 schema 决定。
- `is_refresh_key: true` 标记卡片刷新键。卡片下发时该字段值会作为卡片的 `refresh_key`，服务端可通过「刷新卡片实例」API 用相同值更新卡片数据。同一 entity 内最多一个字段可标记为刷新键。历史卡片刷新也依赖该字段匹配。
- `llm_visible` 控制字段是否投影给模型，默认 `true`。
- `llm_modifiable` 控制字段是否允许模型修改；首期只支持 `type: array` 的列表字段，语义是模型可从原始列表中筛选 item 子集。
- `tool_card_binding` 与 `schema` 同级，key 是工具名，value 是当前工具返回该 entity 时使用的前端 `widget_id`。
- `history_card_refresh` 与 `schema` 同级，用于声明是否支持历史卡片刷新。匹配键来自 `schema` 中被 `is_refresh_key: true` 标记的字段值；实体 schema 中必须声明 `is_refresh_key` 字段才能使用历史卡刷新。

复杂 entity 的 schema 可以按模型需要精简，不必复制完整的业务返回结构：

- 必须声明顶层稳定业务 ID，并配置 `is_entity_id: true`。
- 必须声明明确需要进入模型上下文的字段；字段已声明但省略 `llm_visible` 时默认按 `true` 处理，也可以显式写 `llm_visible: true`。
- 使用 `llm_modifiable` 等依赖 schema 的平台能力时，必须声明对应字段及其必要的嵌套结构。
- 只供 Widget 渲染、跳转或交互使用的字段可以不在 schema 中声明。它们仍会随 `structuredContent.entities` 原样透传给卡片，但不会因为透传而自动进入模型上下文。
- 已声明字段的名称、类型和嵌套结构必须与实际返回一致；不要为了省略复杂 schema 而给同一字段声明错误或残缺的结构。

Manifest schema 字段类型：

- `string`：字符串。
- `int`：整数数值。
- `float`：浮点数值。
- `boolean`：布尔值。
- `object`：对象结构，通常配合 `properties` 描述子字段。
- `array`：数组结构，通常配合 `items` 描述 item schema；需要模型筛选 item 时可配置 `llm_modifiable: true`。

`history_card_refresh` 当前文档明确的取值：

- `scope: same_conversation`：同一会话范围内刷新历史卡片。
- `refresh_to: fixed_style`：刷新为固定样式卡片。

#### tools

`tools` 声明 MCP tool 的输出类型和运行配置。工具入参以 MCP Server `tools/list` 暴露的 `inputSchema` 为准，Manifest 不重复声明。

```yaml
tools:
  search_routes:
    description: 搜索路线
    output:
      kind: entities
      entity_types: [route]
```

配置规则：

- `output.kind=entities` 表示业务返回 `structuredContent.entities`，平台按 `entities` schema 处理。
- `output.kind=execution_result` 表示业务返回执行结果 object，不生成业务实体卡片。
- `output.entity_types` 限定该工具允许返回哪些业务实体类型。
- 工具本身不声明 `rendering`。是否出卡由“当前工具名 + entity_type”是否命中 `entities.<entity_type>.tool_card_binding` 决定。

### 设计步骤

1. 建模阶段：读取 [业务建模方法](#业务建模方法)，从业务对象、用户选择行为和展示方式推导 `entities_designed`。
2. 定义 MCP tool：用户意图、入参、成功 ToolResult、业务错误 ToolResult。
3. 判断输出类型：查询/推荐/搜索通常是 `entities`；提交/收藏/发起动作通常是 `execution_result`。
4. 为 `entities` 定义业务对象 schema：声明稳定 ID、需要进入模型上下文的字段和必要的 `llm_modifiable=true` 列表字段；复杂的卡片专用数据可以不完整声明。
5. 为需要出卡的业务对象写 `tool_card_binding`，只声明工具名到前端 `widget_id` 的映射。
6. 在 `tools` 中声明 `output.kind` 和 `output.entity_types`。
7. 用开发者可感知的方式说明最终效果：模型能看到哪些字段、哪些 entity 会成为卡片候选、列表 item 是否可被模型筛选。

### Few-shot

#### Case 1：普通业务对象出卡

MCP tool：`search_routes`，搜索路线，返回路线 entity。

业务返回：

```json
{
  "content": [{ "type": "text", "text": "找到 1 条可用路线。" }],
  "structuredContent": {
    "entities": [{
      "entity_id": "route_1",
      "entity_type": "route",
      "title": "公司到机场",
      "duration_minutes": 42,
      "price_yuan": 128,
      "summary": "预计 42 分钟到达",
      "deeplink": "page/route?id=route_1"
    }]
  }
}
```

Manifest：

```yaml
tools:
  search_routes:
    output:
      kind: entities
      entity_types: [route]

entities:
  route:
    schema:
      entity_id: { type: string, is_entity_id: true, is_refresh_key: true }
      entity_type: { type: string, llm_visible: false }
      title: { type: string }
      duration_minutes: { type: int }
      price_yuan: { type: int }
      summary: { type: string }
      deeplink: { type: string, llm_visible: false }
    tool_card_binding:
      search_routes: route_result_card
    history_card_refresh:
      enabled: true
      scope: same_conversation
      refresh_to: fixed_style
```

最终效果：

- 模型可见：`title`、`duration_minutes`、`price_yuan`、`summary`。
- 模型不可见：`entity_type`、`deeplink`。
- 平台通过 `search_routes + route` 选择前端 `widget_id=route_result_card`。
- 标题、按钮、跳转等 UI 字段由 `route_result_card` 对应卡片代码读取 entity 后自行渲染。

#### Case 2：业务对象只给模型参考，不出卡

MCP tool：`search_routes_for_reference`，返回路线给模型比较，但不渲染卡片。

Manifest 差异：

```yaml
tools:
  search_routes_for_reference:
    output:
      kind: entities
      entity_types: [route]

entities:
  route:
    schema:
      entity_id: { type: string, is_entity_id: true }
      entity_type: { type: string, llm_visible: false }
      title: { type: string }
      duration_minutes: { type: int }
      price_yuan: { type: int }
```

最终效果：

- 模型可基于可见字段做比较和回复。
- 因为 `route` 没有为 `search_routes_for_reference` 配置 `tool_card_binding`，不会生成卡片候选。

#### Case 3：列表卡，模型可筛选 item

MCP tool：`search_products`，返回一个父级商品列表 entity，商品明细在 `products` 数组中。

业务返回：

```json
{
  "content": [{ "type": "text", "text": "找到 2 个候选商品。" }],
  "structuredContent": {
    "entities": [{
      "entity_id": "product_list_1",
      "entity_type": "product_list",
      "title": "推荐 2 个保温杯",
      "summary": "按性价比排序",
      "products": [
        { "product_id": "p1", "name": "轻量保温杯", "selling_point": "适合通勤", "price_yuan": 99 },
        { "product_id": "p2", "name": "大容量保温杯", "selling_point": "适合出差", "price_yuan": 129 }
      ]
    }]
  },
  "isError": false
}
```

Manifest 关键配置：

```yaml
tools:
  search_products:
    output:
      kind: entities
      entity_types: [product_list]

entities:
  product_list:
    schema:
      entity_id: { type: string, is_entity_id: true }
      entity_type: { type: string, llm_visible: false }
      title: { type: string }
      summary: { type: string }
      products:
        type: array
        llm_modifiable: true
        items:
          type: object
          properties:
            product_id: { type: string }
            name: { type: string }
            selling_point: { type: string }
            price_yuan: { type: int }
    tool_card_binding:
      search_products: product_list_widget
```

最终效果：

- 父级 entity 承载整卡上下文。
- `products` 因为 `llm_modifiable: true`，item 可以被模型作为候选筛选。
- 模型只能从原始 item 中选子集，不能新增 item 或改写 item 字段。
- 列表展示、排序、截断、空态和点击逻辑由 `product_list_widget` 对应卡片代码处理。

#### Case 4：执行结果，不出卡

MCP tool：`subscribe_product`，执行订阅动作。

业务返回：

```json
{
  "content": [{ "type": "text", "text": "订阅成功。" }],
  "structuredContent": {
    "success": true,
    "message": "订阅成功"
  },
  "isError": false
}
```

Manifest：

```yaml
tools:
  subscribe_product:
    kind: execution
    output:
      kind: execution_result
```

最终效果：

- 模型根据 `content[0].text` 和 `structuredContent` 回复用户。
- 不生成业务 entity，也不出卡。
## 业务建模方法

本文用于生命周期最前端的建模阶段：先把用户业务建模成适合豆包智能服务展示和模型决策的形态，再进入协议和 Manifest 编写。推荐产物命名为 `entities_designed`，供后续生成 `entities`、`tool_card_binding`、`tools.output` 和运行态 Skill 使用。

这不是纯协议说明，也不是模板字段手册。它解决的问题是：业务里哪些东西应该成为 entity，哪些只是字段，应该用 single 还是 list，哪些字段给模型看，哪些场景根本不应该建实体。

### 输出产物：entities_designed

建模阶段先输出一个简短设计草稿：

```yaml
entities_designed:
  - entity_type: route
    business_object: 路线方案
    stable_id: route_id
    template_kind: single
    llm_visible_fields: [title, duration_minutes, price_yuan, summary]
    render_only_fields: [entity_type, detail_url]
    tools: [search_routes]
    widget_id: route_result_card
    reason: 用户会比较路线，并可点击某条路线查看详情
```

列表卡可写成：

```yaml
entities_designed:
  - entity_type: product_group
    business_object: 商品推荐分组
    stable_id: group_id
    template_kind: list
    list_source: products
    item_stable_id: product_id
    llm_modifiable: true
    llm_visible_fields: [title]
    item_llm_visible_fields: [name, price_yuan, selling_point]
    render_only_fields: [group_id]
    item_render_only_fields: [product_id, detail_url]
    tools: [search_products]
    widget_id: product_list_widget
    reason: 用户会从商品列表里选择某一项继续操作
```

### 业务对象到 entity 的拆分

优先把这些对象建模成 entity：

- 用户会比较、选择、追问或继续操作的业务对象，例如路线、商品、餐厅、订单、日程、文档。
- 工具返回中的核心结果单元，例如“一个路线方案”“一个订单详情”“一个商品推荐分组”。
- 可以稳定引用的对象，通常有业务主键，例如 `route_id`、`order_id`、`product_id`。
- 需要渲染成卡片的对象，尤其是需要点击、跳转、按钮操作或后续工具调用的对象。

优先作为字段，不单独建 entity：

- 只描述父对象的属性，例如价格、时长、摘要、状态、封面图、跳转 URL。
- 不能被用户单独选择或操作的展示信息。
- 只服务排序、筛选、渲染或跳转的辅助数据。
- 生命周期完全依附父对象的短文本、标签、统计值。

主键选择：

- `is_entity_id: true` 对应业务稳定 ID，字段名由业务协议决定。
- 选择后续工具能继续使用的 ID，而不是展示名、排序号或临时下标。
- 如果对象没有稳定 ID，也可以展示，但要谨慎承诺后续“选择这个继续操作”的能力。
- 列表 item 如果后续可单独操作，也应有可回传的业务 ID。

拆分边界：

- 不要因为 UI 上有多个区域就拆成多个 entity；entity 先表达业务对象，不表达布局。
- 不要把一次动作结果强行建成 entity；纯动作结果通常用 `execution_result`。
- 不要把每个字段都建成 entity；字段只有在能被独立选择、引用或操作时才升级为 entity。

### single-tpl 与 list-tpl 选型

用 single-tpl 的场景：

- 工具返回一个明确对象或详情页式结果。
- 用户下一步通常围绕这一条对象操作，例如查看详情、订阅、打开、取消、确认。
- 卡片主体是单个业务对象的标题、摘要、状态和操作按钮。

典型例子：

- `query_order` 返回一个订单详情。
- `get_route_detail` 返回一条路线详情。
- `create_order` 成功后展示一个订单结果。

用 list-tpl 的场景：

- 工具返回一组可比较候选，例如商品、路线、餐厅、攻略、文件。
- 用户可能说“第一个”“便宜的那个”“帮我选一个”“只看这几项”。
- 卡片需要一个父级上下文，例如“为你找到 5 个商品”，下面挂多个 item。

典型例子：

- `search_products` 返回商品列表。
- `search_routes` 返回多条路线候选。
- `list_restaurants` 返回餐厅列表。

选型启发：

- 单条结果优先 single。
- 多条同类候选优先 list。
- 多条候选如果每条都需要复杂独立操作，先评估是否拆成多个 single；但多数搜索/推荐场景用一个 list 更利于模型选择和用户浏览。
- 列表卡的父对象承载查询条件、标题、摘要、排序理由；item 承载可选择的具体候选。

### llm_visible 字段选择

给模型看的字段应该帮助模型理解、比较、选择或回复用户：

- 名称、标题、摘要、状态。
- 价格、时间、距离、评分、数量等决策指标。
- 推荐理由、卖点、限制条件、可用性。
- 用户可能追问或用自然语言指代的字段。

只用于渲染或执行，不建议给模型看的字段：

- 跳转 URL、图片 URL、图标、颜色、布局辅助字段。
- 业务后台 ID、鉴权 token、签名、埋点参数。
- 只用于端上点击或后续接口拼参的字段。
- 过长、噪声高、对模型选择没有帮助的原始详情。

判断方法：

- 用户问“为什么选它”时需要用到的字段，通常 `llm_visible=true`。
- 用户不会直接感知、但卡片或接口需要的字段，通常 `llm_visible=false`。
- 如果字段泄露会带来安全、隐私或误导风险，设为 `llm_visible=false`。
- 如果 entity 结构复杂且字段只供卡片使用，可以直接不在 schema 中声明；未声明字段仍会透传给卡片。需要明确记录字段存在、或希望显式表达模型不可见时，再声明并设置 `llm_visible=false`。

### 何时用 execution_result

以下场景用 `execution_result`，不要强行建 entity：

- 工具只表达动作是否完成，例如订阅成功、收藏成功、提交成功。
- 结果没有可独立展示或选择的业务对象。
- 用户下一步不需要围绕返回对象继续选择，只需要知道动作结果。
- 返回内容是确认信息、错误说明、轻量状态。

仍应使用 entities 的场景：

- 动作成功后产生了可展示、可追问、可继续操作的对象，例如创建订单后返回订单详情。
- 查询/推荐/搜索返回了用户需要比较或选择的对象。
- 卡片展示本身是核心体验，不只是文字确认。

判断边界：

- “订阅成功”是 `execution_result`。
- “订阅成功，并返回一个可管理的订阅项详情”可以是 entity。
- “下单成功，返回订单详情卡”通常建模为订单 entity。
- “发起导航成功”如果只是状态确认，用 `execution_result`；如果返回路线详情或导航卡，则建模为路线或导航会话 entity。

### 从建模到后续产物

`entities_designed` 进入后续步骤时这样落地：

- `entity_type`、`stable_id`、`llm_visible_fields`、`render_only_fields` 生成 Manifest `entities`。
- `template_kind`、`list_source`、`llm_modifiable` 影响 Manifest schema 中的数组字段设计和 `llm_modifiable` 配置。
- `tools` 影响 Manifest `tools.<tool>.output.kind`、`entity_types`，以及 `entities.<entity_type>.tool_card_binding.<tool_name>`。
- `widget_id` 写入 `tool_card_binding`。它必须来自前端工程 `defineAppConfig({ widgets })` 中声明或由简写推导出的 widget，不在 Manifest 中声明字段到 UI 的映射。
- 运行态 Skill 使用 `llm_visible_fields` 和 item 选择能力来写工具选择、反问、卡片引用和后续动作规则，具体写法见 [generate-skill.md](generate-skill.md)。
## 列表卡协议字段

列表卡用于表达“一个父级业务 entity + 一个或多个数组字段 + 数组中的 item”。平台不会把多个普通业务对象自动合成一张列表卡；业务必须返回一个父级 entity，并在父 entity 中放数组字段。

Manifest 不声明列表 UI 点位、`source_path`、item 字段映射或点击动作。列表如何展示、排序、截断、空态和点击逻辑由 `tool_card_binding` 选出的前端 widget 代码实现。Manifest 只声明数组字段 schema，以及该字段是否允许模型筛选。

### 最小业务返回

`output.kind=entities` 时，业务返回入口固定为 `structuredContent.entities`。

```json
{
  "content": [{ "type": "text", "text": "找到 2 个候选商品。" }],
  "structuredContent": {
    "entities": [{
      "entity_id": "product_list_1",
      "entity_type": "product_list",
      "title": "推荐商品",
      "summary": "按性价比排序",
      "products": [
        { "product_id": "p1", "name": "轻量保温杯", "price_yuan": 99 },
        { "product_id": "p2", "name": "桌面收纳盒", "price_yuan": 39 }
      ]
    }]
  },
  "isError": false
}
```

业务只返回父 entity 和原始数组数据。

### entities 配置

```yaml
entities:
  product_list:
    schema:
      entity_id: { type: string, is_entity_id: true }
      entity_type: { type: string, llm_visible: false }
      title: { type: string }
      summary: { type: string }
      products:
        type: array
        description: 商品候选列表
        llm_modifiable: true
        items:
          type: object
          properties:
            product_id: { type: string, description: 商品 ID }
            name: { type: string, description: 商品名称 }
            selling_point: { type: string, description: 推荐理由 }
            price_yuan: { type: int, description: 商品价格 }
            detail_url: { type: string, llm_visible: false }
    tool_card_binding:
      search_products: product_list_widget
```

字段说明：

- 父级 entity 承载整卡上下文，例如标题、摘要、筛选条件、排序理由。
- 父级稳定 ID 用 `is_entity_id: true` 标记；字段名由业务 schema 决定。
- `entity_type` 是顶层业务实体类型，业务返回中必带。
- 列表源数组就是父 entity 的普通数组字段，例如 `products`。
- item 可以有自己的普通业务 ID 字段，例如 `product_id`，但它不替代父级 entity ID。
- `tool_card_binding` 只声明当前工具返回该 entity 时使用哪个前端 `widget_id`。

### llm_modifiable=true

`llm_modifiable=true` 只能配置在 `type: array` 的列表字段上。它表示模型可以从原始列表 item 中筛选子集。

列表字段常用 schema 类型：

- `array`：数组结构，列表源字段必须使用该类型。
- `object`：对象结构，常用于数组 item。
- `string`：字符串字段，例如名称、标题、ID、URL。
- `int`：整数数值，例如价格分、数量、耗时分钟。
- `float`：浮点数值，例如评分、经纬度。
- `boolean`：布尔值，例如是否可用、是否选中。

适合场景：

- 商品候选、地点候选、攻略条目、新闻列表、候选操作项。
- 用户可能说“第一个”“便宜的那个”“只保留这几个”。
- 模型需要从候选列表中选择若干 item，再让卡片展示筛选后的子集。

链路效果：

- 平台识别 `type: array` 且 `llm_modifiable=true` 的字段。
- 平台把数组 item 作为模型可筛选的候选。
- 模型只能从原始 item 中选择子集。
- 被选 item 会回填到父级 entity 对应数组字段上下文。
- 卡片代码读取父级 entity 和筛选后的列表数据完成渲染。

约束：

- 不支持模型新增 item。
- 不支持模型改写 item 字段值。
- 不支持原列表之外的新数据。
- 不支持把 `llm_modifiable=true` 配在 string、number、object 等非数组字段上。

### llm_modifiable=false 或不配置

适合列表只作为普通结构化数据或卡片展示内容、不希望模型单独筛选 item 的场景。

```yaml
products:
  type: array
  items:
    type: object
    properties:
      product_id: { type: string }
      name: { type: string }
      price_yuan: { type: int }
```

最终效果：

- 字段仍是业务 entity 的结构化数据。
- 模型不会把 item 当作可筛选候选。
- 卡片代码仍可读取该数组并自行展示。

### 降级和错误处理

- `llm_modifiable=true` 配在非数组字段上：平台按配置错误或忽略该声明处理。
- 业务实际返回值不是数组：该字段不进入模型筛选链路；卡片代码按缺失、空态或普通降级处理。
- 模型筛选结果为空：平台保留父级 entity 上下文；空态由卡片代码处理。
- item 缺少业务 ID：仍可作为当前轮候选被筛选，但后续依赖 item ID 的工具可能缺少业务入参。
- 未命中 `tool_card_binding`：列表字段仍可作为模型上下文处理，但无法生成对应业务卡片。

### 易错点

- 列表卡的业务返回形态是一个父级 `product_list` entity，商品明细放在父 entity 的 `products` 数组中。
- 不要在 Manifest 中写 `source_path`、列表槽位、按钮、点击 URL 或 item UI 字段映射。
- 不要把 item 声明成独立 `entity_type`，除非它确实是可独立出卡和操作的顶层业务对象。
- 不要把 `llm_modifiable=true` 理解成模型可改写业务数据；它只允许筛选原始列表 item 子集。

## MCP协议的一些基础规则

### MCP Server 日志与可观察性

MCP Server 必须具备可定位问题、但不过度冗余的结构化日志。日志用于回答“哪个请求、调用了哪个工具、执行到哪一步、结果是什么、耗时多久、下游平台返回了什么定位 ID”，不能依赖临时增加 `console.log` 才能排查。

每次 `tools/call` 至少记录：

- `event`：如 `mcp_tool_call_started`、`mcp_tool_call_finished`、`mcp_tool_call_failed`。
- `request_id` / `trace_id`、MCP `tool_call_id`（存在时）。
- `tool_name`、经过脱敏的参数摘要、开始/结束时间或 `duration_ms`。
- 成功/业务失败/系统异常、结果码、错误类型；不要记录完整 ToolResult 或大体积 entity。
- 调用下游 OpenAPI 时记录接口名、HTTP 状态码、业务错误码、耗时和平台返回的 `log_id`。

日志粒度遵守以下规则：

- 正常请求通常一条开始日志和一条结束日志；失败时结束日志带错误摘要，不为每个内部函数重复打印相同上下文。
- 参数摘要只保留定位所需的非敏感字段、数量、类型或稳定 ID；禁止打印 AppSecret、Authorization、login/phone code、access/refresh token、手机号、私钥、CSR 私钥材料或完整请求/响应体。
- 需要关联敏感凭证时，只记录不可逆指纹或脱敏后缀，并保持同一请求链路可关联；不要记录原值。
- 日志格式保持稳定，优先 JSON 或固定字段结构；生产默认使用 `info` / `warn` / `error`，高频内部细节只在显式 debug 开关下输出。
- Server 启动时记录服务版本、监听地址、运行环境和已注册 tool 数量，但不得打印环境变量值或凭据。

### Server 启动配置与凭据边界

- MCP Server 必须支持从 `DOUBAO_OPENAPI_BASE_DOMAIN` 读取平台 OpenAPI BaseDomain；本地调试启动方式见 [本地调试总流程](local-debug/overview.md)。
- AppID/AppSecret 的读取方式遵循项目现有安全配置；如有部署需要，可选用环境变量注入。AppSecret 不得进入前端、Manifest、运行态 Skill、日志、错误响应或版本库；如果选择环境变量方案，提供仅含变量名的 `.env.example`，并将 `.env.local` 加入 `.gitignore`。
- 本地调试时按 [本地调试总流程](local-debug/overview.md)，由 Agent 使用实际项目的启动脚本帮用户启动和观测 Server；确认 endpoint 可达后再运行 `dbx dev --mcp-endpoint http://127.0.0.1:<port>/mcp`。

- MCP endpoint 必须支持 POST JSON-RPC 请求。
- initialize 必须返回 HTTP 2xx，且 body 是合法 JSON-RPC response:
```
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": { "tools": {} },
    "serverInfo": { "name": "xxx", "version": "x.y.z" }
  }
}
```
- initialize.result 必须是 object。
- 如果 initialize 响应头返回了 Mcp-Session-Id，后续请求必须接受同一个 session id:
```
Mcp-Session-Id: <session-id>
```
- notifications/initialized 是 notification，没有 id：
```
{
  "jsonrpc": "2.0",
  "method": "notifications/initialized",
  "params": {}
}
```
- 对 notifications/initialized 必须返回 HTTP 2xx + 空 body。不要返回 {}。
- tools/list 必须返回 HTTP 2xx，且 body 是合法 JSON-RPC response：
```
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": []
  }
}
```
