MCP Adapter 输入输出 Schema 设计实战:对象格式 vs 扁平化格式怎么选?

原创2026-10-11 11:29:50233 阅读

MCP Adapter 输入输出 Schema 设计实战:对象格式 vs 扁平化格式怎么选?

MCP Adapter 是 WordPress 官方的 MCP 集成包,它把 WordPress Abilities 桥接成 AI 客户端可调用的 MCP 工具、资源与提示词。其中输入输出 Schema 设计是最容易踩坑的一环:MCP 协议强制工具 schema 为 object 类型,而 WordPress 允许你写"扁平化"的单值 schema。本文带你彻底搞懂对象格式 vs 扁平化格式怎么选,以及 adapter 背后的自动包装机制。

MCP Adapter 输入输出 Schema 设计实战:WordPress 与 MCP 协议桥接全景

🧭 先搞懂前提:MCP 为什么"只认" object?

MCP 规范要求工具的 inputSchema 必须是 type: object——AI 客户端发起 tools/call 时,arguments 永远是一个 JSON 对象,不存在"裸传一个字符串"这种调用方式。

而 WordPress Abilities API 的 input_schema 比 MCP 更宽松:一个工具只需要接收一个字符串时,你可以直接把 schema 写成 {'type': 'string', ...},不必套 object。这就是扁平化格式(flattened schema)的由来。

MCP Adapter 的定位就是抹平这两层差异:你写哪种格式都能用,adapter 在 wire 层(MCP 客户端看到的)和执行层(你的回调收到的)之间自动转换。

📐 两种格式到底差在哪?

以一个"按文章类型计数"的工具为例,同一份能力的两种写法:

对象格式(Object Schema)——标准的 JSON Schema 对象:

{
  "type": "object",
  "properties": {
    "post_type": { "type": "string", "description": "要统计的文章类型" }
  },
  "required": ["post_type"]
}

扁平化格式(Flattened Schema)——直接描述那一个值:

{
  "type": "string",
  "description": "要统计的文章类型",
  "enum": ["post", "page", "attachment"]
}

区别一目了然:

维度 对象格式 扁平化格式
顶层 type 必须是 object string / number / integer / boolean / array
参数承载 properties 下可放多个具名参数 只有一个值,没有参数名
required 语义 声明哪些字段必填 由 adapter 自动生成
wire 层是否被改写 原样透传 被包装成 {type: object} 并生成包装键
适用场景 多参数、复杂校验、嵌套结构 单值入参 / 单值出参

💡 注意:扁平化 ≠ "省略写法",它表达的是"这个工具只有一个输入"。多参数场景请老老实实用对象格式。

⚙️ 自动包装如何运作?(3 个关键步骤)

1️⃣ Wire 层:SchemaTransformer 统一"套壳"

转换逻辑集中在 SchemaTransformer.php:

  • schema 是 object → 原样返回,不做任何改动;
  • schema 是 string/number/integer/boolean/array → 被 wrap_in_object() 包成 {'type': 'object', 'properties': {包装键: 原schema}, 'required': [包装键]};
  • 原始的 description、enum、minimum、pattern 等所有元数据完整保留在包装键内部。

包装键由 RegisterAbilityAsMcpTool 决定:

  • input_schema → 包装键固定为 input
  • output_schema → 包装键固定为 result

也就是说,上面那个扁平化的 string schema,AI 客户端实际看到的就是:

{
  "type": "object",
  "properties": {
    "input": { "type": "string", "description": "要统计的文章类型", "enum": ["post", "page", "attachment"] }
  },
  "required": ["input"]
}

2️⃣ 执行层:入参解包、出参包装

转换发生时,adapter 会在内部元数据里记一笔 input_schema_transformed / output_schema_transformed(见 RegisterAbilityAsMcpTool.php),供执行阶段使用:

一句话总结:回调世界里永远是解包后的值;wire 世界里永远是 object。

🎯 一张表拍板:什么时候用哪种?

场景 推荐格式 理由
多个参数(如标题 + 正文 + 状态) ✅ 对象格式 每个参数有名字、有独立校验,AI 更容易填对
需要嵌套结构、数组 of object ✅ 对象格式 扁平化装不下层级
单个字符串 / 数字 / 布尔 / 数组 ✅ 扁平化格式 声明更短,约束(enum、min/max)仍全部保留
输出只有单个值(一个 ID、一个计数、一个 slug) ✅ 扁平化格式 回调直接 return 42;,不用自己组数组
输出是多个相关字段(post_id + url + status) ✅ 对象格式 字段需要自描述

决策口诀:默认对象,单值才扁平。 拿不准时选对象格式——它永远不会被 adapter 改写,行为最可预测(这也是官方文档推荐写法的原因,见 creating-abilities.md)。

💻 实战:一个扁平化工具的完整闭环

wp_register_ability( 'my-plugin/count-posts', [
    'label'       => 'Count Posts',
    'category'    => 'site',
    'input_schema'  => [
        'type' => 'string',
        'enum' => [ 'post', 'page', 'attachment' ],
    ],
    'output_schema' => [
        'type' => 'integer',
        'description' => 'Total number of posts found',
    ],
    'execute_callback' => function ( $input ) {
        // $input 是解包后的裸字符串,例如 'post'
        $count = wp_count_posts( $input );
        return $count->publish; // 直接 return 42,不要包成 ['result' => 42]
    },
    'permission_callback' => function () {
        return current_user_can( 'read' );
    },
    'meta' => [ 'public' => true ],
] );

一次调用的完整数据流:

  1. MCP 客户端发送:{"name": "my-plugin-count-posts", "arguments": {"input": "post"}} —— 永远是 object;
  2. adapter 解包:{"input": "post"} → 'post';
  3. 你的回调:wp_count_posts('post') 并 return 42;
  4. adapter 包装:42 → {"result": 42};
  5. MCP 客户端收到:{"result": 42}。

同样的能力如果写成对象格式,execute_callback 收到的就是 $input<a href="https://link.gitcode.com/i/365c47cc7aecd8d924661764e9c6b237" target="_blank">'post_type'],客户端也传 {"post_type": "post"}——多一层具名参数,语义更明确,适合参数会变多的工具(完整对比见 [basic-examples.md 的 create-post 示例)。

🚧 常见坑与排错

坑 1:回调里多包了一层

扁平格式下返回 ['result' => 42] 是常见误操作。包装是 adapter 的职责,你返回裸值 42 即可;反过来,对象格式下返回什么,客户端就收到什么(无包装),别指望 adapter 帮你套壳。

坑 2:空参数 {} 传进来变成了 null 或 []?

这是 AbilityArgumentNormalizer 的有意设计:

  • ability 没有 input_schema:{} → null("无参数");
  • ability 有 schema:{} → [],这样空对象能通过 schema 校验,而不是报 "not of type object";
  • schema 顶层声明了 default:{} 和 null 都归一化为 null,让 WordPress 去应用默认值。

坑 3:默认服务器路径 vs 自定义服务器路径

默认服务器的 mcp-adapter-execute-ability 元工具传的是 {"ability_name": "...", "parameters": {...}},parameters 按 ability 原始 schema 的形状传递(见 ExecuteAbilityAbility.php);而注册在自定义服务器上的工具,才由 adapter 执行 input/result 包装与解包。两条路径不要混着理解。

坑 4:schema 里没有 type 字段

SchemaTransformer 会补上 type: object 并原样透传,不会误伤——但建议显式写出 type,可读性更好。

✅ 5 条要点带走

  1. MCP 强制 object:客户端的 arguments 永远是 JSON 对象,这是协议层约束;
  2. 扁平化只是你的写法:wire 上会被自动包成 {input: ...} / {result: ...},且 enum、minimum 等约束原样保留;
  3. 回调永远处理裸值:解包入参、包装出参都是 adapter 的活,别手动加壳;
  4. 决策默认值:多参数 / 嵌套 / 复杂校验 → 对象格式;单值输入或输出 → 扁平化格式;
  5. 空参数交给归一化器:{} 的 null / [] / default 行为已有明确规则,不要在回调里写防御性兼容。

把 Schema 设计想成"契约分层":你与 WordPress 之间用裸值契约,MCP 客户端与 wire 之间用 object 契约,中间的桥就是 adapter。选对格式后,剩下的转换它全帮你干了。

登录后查看全文
mcp-adapter