Skip to content

Tool serializes empty inputSchema.properties as [] instead of {} #405

Description

@BrocksiNet

Environment

  • mcp/sdk v0.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":[]}}   <-- invalid

The 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 lost

Expected: "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 invalid

Suggested fix

Make the object invariant total rather than boundary-only:

  • Normalize in Tool::__construct() (and/or Tool::jsonSerialize()) so a Tool can never hold or emit properties: [], regardless of how it was built.
  • Make the walk recursive so nested object schemas and outputSchema are covered too, not just the top-level properties.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions