Repository navigation
Tool serializes empty inputSchema.properties as [] instead of {} #405
Description
Environment
mcp/sdkv0.7.0- PHP 8.4
Summary
A Tool whose inputSchema.properties is an empty PHP array serializes to "properties":[]. JSON Schema requires an object there, so strict clients reject the whole tools/list payload — e.g. OpenAI-compatible clients answer 400 invalid_function_parameters: "[] is not of type 'object'". One parameterless tool then breaks every request, not just calls to that tool.
The SDK already knows empty properties must become {} — it does so in SchemaGenerator::buildSchemaFromParameters() and in Tool::fromArray() (via normalizeSchemaProperties(), PR #223). But this normalization lives only on those input boundaries. Tool::__construct() and Tool::jsonSerialize() do not enforce it, so any Tool built another way — or whose schema survived a json_decode($json, true) round-trip — reaches the wire unnormalized.
Reproduction
Direct construction (bypasses fromArray):
use Mcp\Schema\Tool;
$tool = new Tool(
name: 'no_params',
title: null,
inputSchema: ['type' => 'object', 'properties' => []],
description: null,
annotations: null,
);
echo json_encode($tool);
// {"name":"no_params","inputSchema":{"type":"object","properties":[]}} <-- invalidThe round-trip that bites real servers (an empty JSON object decodes to an empty PHP array, then re-serializes as []):
$schema = json_decode('{"type":"object","properties":{}}', true); // ['type'=>'object','properties'=>[]]
$tool = new Tool('t', null, $schema, null, null);
echo json_encode($tool); // "properties":[] — the {} was silently lostExpected: "properties":{} in every case.
Actual: "properties":[] whenever the schema wasn't produced by SchemaGenerator or passed through Tool::fromArray().
Additional facet — normalization is also shallow
Even SchemaGenerator and Tool::fromArray() only normalize the top-level properties. A nested object parameter with no members, or an empty outputSchema.properties, still serializes as []:
$tool = Tool::fromArray([
'name' => 't',
'inputSchema' => [
'type' => 'object',
'properties' => ['filter' => ['type' => 'object', 'properties' => []]], // nested, not normalized
],
]);
echo json_encode($tool); // ...{"filter":{"type":"object","properties":[]}} <-- still invalidSuggested fix
Make the object invariant total rather than boundary-only:
- Normalize in
Tool::__construct()(and/orTool::jsonSerialize()) so aToolcan never hold or emitproperties: [], regardless of how it was built. - Make the walk recursive so nested object schemas and
outputSchemaare covered too, not just the top-levelproperties.
This mirrors the precedent in PR #378 ("Always emit items for array tool parameter schemas"), which fixed the same class of strict-client rejection (VS Code / Copilot) in the SDK rather than leaving each consumer to patch it at the transport layer. Downstream servers currently have to re-parse and rewrite every tools/list response to work around this.
Activity
[Schema] Always emit {} for empty tool schema properties
[Schema] Always emit {} for empty tool schema properties (#407)