AI 技术
#MCP#JSON Schema#工具调用#参数校验

MCP 工具参数默认值与必填校验:为什么 inputSchema 里省略的字段会让服务端收到 undefined

Agent 调用带可选参数的 MCP 工具时,模型省略字段常让服务端收到缺键参数对象,引发解引用报错或空值写入。文章从 JSON Schema 中 required 与 default 的语义边界出发,说明缺键如何产生、校验点应放在哪里,并比较客户端预填与服务端容错解析的取舍。

一个只在“可选参数”上出现的报错

设想企业知识库里有一个 MCP 工具 search_docs,用来按关键词检索内部文档。它的输入参数有三个:query 是必填的检索词,top_k 控制返回条数,department 限定部门范围。后两个参数在 inputSchema 里没有放进 required 数组,top_k 还写了 "default": 10。

上线后,Agent 在“帮我找一下报销流程”这类请求里只传了 query。服务端处理函数读到 args.top_k 得到 undefined,随后在 Math.min(args.top_k, 50) 或数据库分页计算中抛错;另一条路径里,服务端把 undefined 直接写进了查询条件,检索结果变成空集,但工具返回的是成功状态。

这类故障容易被误判成模型“不听话”。实际上模型的行为符合它看到的 schema:字段没有列入 required,模型就可以不传。问题出在服务端把“schema 允许省略”当成了“运行时一定有值”。

required 与 default 各自管什么

required 只约束键是否存在

JSON Schema 的 required 是一个针对对象实例的断言关键字。它规定实例对象必须包含列出的成员名,缺任何一个,该实例就不通过校验。它不关心值是什么类型、是否为空字符串、是否为 null,这些由 type、minLength 等其它关键字负责。

因此 required: ["query"] 的含义很窄:参数对象里必须有 query 这个键。top_k 不在数组里,就表示缺键合法。

default 是注解,不是填充指令

JSON Schema 规范把 default 归入“基础元数据注解”词汇。资料中的验证规范目录把它列在 title、description、deprecated、readOnly、examples 同一组里,这组关键字的作用是给工具、界面或文档提供提示信息,而不是对实例做断言。

规范对注解与断言做了明确区分:断言决定实例是否合法,注解携带非断言信息。校验器在验证一个实例时,不会因为 default 存在就往实例里补一个值。换句话说,"default": 10 表达的是“如果没有提供,建议按 10 理解”,它把这条建议写给读 schema 的人和程序,但不改变被校验的 JSON 本身。

这一点在 MCP 场景里尤其关键。MCP 的工具定义包含名称、描述、输入 schema 和可选的输出 schema,schema 会直接参与模型对工具的理解和参数生成。模型看到 default 可能据此省略字段,也可能照抄默认值,两种行为都符合规范;服务端不能假设任何一种一定发生。

缺键与显式 null 是两种状态

需要区分三种参数对象:

  • 键不存在:{"query": "报销"},读取 args.top_k 得到 undefined(JavaScript)或等价的语言级“无值”。
  • 键存在但值为 null:{"query": "报销", "top_k": null}。这通过了 required 校验,但 type: "integer" 会拒绝它。
  • 键存在且有值:{"query": "报销", "top_k": 5}。

服务端常见的错误是把后两种当成同一种处理,或者用 args.top_k || 10 兜底,结果 0 也被替换成 10。

参数对象在链路上如何丢掉字段

下面的流程展示一次典型的工具调用,从模型生成参数到服务端解引用,标出了缺键产生和暴露的位置。

flowchart TD
    A[客户端列出工具] --> B[模型读取 inputSchema]
    B --> C{字段在 required 中?}
    C -- 是 --> D[模型生成该字段]
    C -- 否 --> E[模型可省略该字段]
    D --> F[组装 arguments 对象]
    E --> F
    F --> G[客户端发送 tools/call]
    G --> H[服务端接收 arguments]
    H --> I{服务端是否补默认值?}
    I -- 否 --> J[读取 args.top_k 得到 undefined]
    I -- 是 --> K[按默认值填充]
    J --> L[解引用报错或写入空值]
    K --> M[正常执行]

关键转折点在 E 和 I 两处。E 处模型省略字段是合法行为,I 处服务端没有做任何填充,缺键就一路传到业务代码。中间没有任何协议层会自动补齐,因为 JSON Schema 的 default 不承担这个职责,MCP 规范也没有规定服务端必须按 schema 默认值填充参数。

服务端按必填假设解引用会怎样

直接解引用导致运行时异常

最直接的失败是类型错误。以 JavaScript 为例,args.top_k.toFixed() 在 top_k 缺键时抛出 TypeError: Cannot read properties of undefined。这类错误如果没被捕获,会变成 JSON-RPC 层面的内部错误,客户端看到的是一个笼统的失败,Agent 往往据此重试同样的调用,形成循环。

空值写入造成静默错误

更隐蔽的是把 undefined 带进下游。构造数据库查询时,department: args.department 在缺键时可能被序列化成 null 或直接省略条件,检索范围从“某部门”变成“全部部门”。工具返回成功,内容却是错的。对 Agent 来说,这比报错更难发现,因为它拿到的是看起来正常的文本结果。

校验时机决定错误归属

如果服务端在进入业务逻辑前先用 schema 校验 arguments,缺键的可选字段不会触发失败,因为 schema 本来就允许省略。校验只能拦住 required 字段缺失和类型错误,拦不住“可选字段缺键但业务代码假设有值”。这说明校验点必须和默认值处理放在一起,不能只依赖 schema 校验。

两种修法:客户端预填与服务端容错

客户端预填默认值

客户端在发送 tools/call 前,按 inputSchema 里的 default 给缺失字段补值。好处是服务端拿到的参数对象结构稳定,业务代码可以按“字段一定存在”来写。代价是客户端必须正确解析 schema,包括嵌套对象、数组元素和 $ref 引用;一旦解析不全,补值就会漏。

还有一个语义风险:客户端补的值会被服务端当成模型显式传入的值。如果默认值依赖服务端配置(比如租户级的条数上限),客户端并不知道真实默认值,补出来的值可能覆盖服务端策略。

服务端容错解析

服务端在读取参数时统一走一个解析层:先判断键是否存在,不存在再用本地默认值,存在则做类型和范围检查。这种方式把默认值的权威留在服务端,客户端和模型都可以省略字段。

代价是每个工具都要写解析逻辑,容易遗漏。工程上通常把解析封装成按 schema 驱动的通用函数,但要注意 default 只是注解,通用函数读取它是“采纳建议”,不是“执行规范”,所以函数行为需要在文档里写清楚。

对比与选择

维度客户端预填服务端容错解析
默认值权威分散在客户端,可能与服务端策略冲突集中在服务端,随配置变化
服务端代码可假设字段存在,逻辑简单需要统一解析层,初期成本高
对第三方客户端无法控制,行为不一致对所有客户端一致
缺键可见性被预填掩盖,日志里看不到模型省略了什么可在解析层记录缺键,便于观测
适用场景自有客户端、默认值稳定多客户端接入、默认值随租户或环境变化

多数生产环境会两者并用:客户端尽量补,服务端仍然容错。只做客户端预填的风险在于,任何绕过该客户端的调用方都会把缺键直接打到服务端。

校验点应该放在哪里

入口处做结构校验

服务端收到 arguments 后,第一步按 inputSchema 做结构校验,确认 required 字段齐全、类型正确。这一步用标准校验器即可,失败时返回明确的参数错误,而不是让异常在业务层炸开。

解析层做默认值合并

第二步是解析层:对每个可选字段判断键是否存在,缺键时填入服务端默认值,存在时保留原值并做范围检查。这里要避免用真值判断兜底,0、空字符串、false 都是合法值。

伪代码示意:

function resolveArgs(schema, raw):
    args = clone(raw)
    for field in schema.properties:
        if field not in raw:
            if field has serverDefault:
                args[field] = serverDefault[field]
            else if field in schema.required:
                return error("missing required field: " + field)
    return args

这段逻辑的关键在于区分“缺键”和“有值”,并明确服务端默认值来自哪里。如果直接读 schema 的 default,要接受它只是建议这一事实,不能把它当成协议保证。

业务层不再假设可选字段存在

解析层之后,业务代码可以按字段一定存在来写,但前提是解析层覆盖了所有工具。新增工具时如果忘了注册默认值,缺键仍会漏到业务层。工程上通常让工具注册和默认值声明放在同一处,减少遗漏。

需要观察的信号与常见失败模式

可观测指标

  • 参数校验失败率,按工具名和缺失字段分组,能直接看出哪些可选字段经常被省略。
  • 解析层补默认值的次数,按字段统计。补值频率高说明模型倾向于省略,schema 描述可能需要补充说明。
  • 工具返回成功但结果为空的比例。这类信号可能对应空值写入,需要结合查询条件日志排查。

典型失败模式

模型省略字段本身不是故障,故障来自服务端对缺键的处理。常见的有:用 || 兜底把 0 替换掉;把缺键的可选字段直接拼进 SQL 条件;在嵌套对象里只检查了顶层键,子字段缺键仍然报错;客户端预填时把 null 当成缺键,覆盖了模型显式传入的空值语义。

还有一种边界情况:字段在 required 里,但模型仍然没传。这时结构校验会拦住,返回参数错误。Agent 收到这类错误后是否重试、是否改写参数,取决于客户端如何呈现错误信息。错误信息里写清缺了哪个字段,比笼统的“参数无效”更有助于模型修正。

适用边界与仍未解决的问题

default 作为注解这一语义是稳定的,但“谁来落实默认值”在 MCP 生态里没有统一规定。不同客户端对 schema 的解析深度不同,不同服务端对缺键的容忍度也不同。跨实现调用时,最稳妥的做法是服务端不假设任何可选字段存在,客户端不假设服务端会补值。

尚未解决的问题包括:嵌套对象和数组元素的默认值如何统一处理;默认值随租户或环境变化时,schema 里的 default 是否还应该写;以及当模型显式传入 null 表示“不限”时,如何与缺键区分。这些问题没有协议级答案,只能在具体实现里约定并写进工具描述。

资料来源

  1. JSON Schema Validation: A Vocabulary for Structural Validation of JSON
  2. JSON Schema Core: default keyword
  3. Model Context Protocol Specification
  4. MCP工具只把一个字段改成可选,Agent为什么开始替用户猜参数?-腾讯云开发者社区-腾讯云