Skip to content

[Server] Keep JSON types of tool call arguments - #678

Merged
chr-hertel merged 6 commits into
modelcontextprotocol:mainfrom
chr-hertel:json-decode-issue
Nov 8, 2026
Merged

chr-hertel merged 6 commits into
modelcontextprotocol:mainfrom
chr-hertel:json-decode-issue

Conversation

@chr-hertel

@chr-hertel chr-hertel commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Fixes #586
Closes #587

json_decode(assoc: true) makes {} and [] the same value, so tool arguments got validated against the wrong JSON type - and the schema can't restore what's already lost. tools/call arguments now keep their JSON types, the rest of the message stays arrays since its typed parsers lose nothing that way.

  • The parameter type decides what a handler gets: array still gets arrays at every level, \stdClass, object, mixed and untyped ones get \stdClass. So not \stdClass everywhere like Preserve native JSON types when passing arguments to tool handlers #587 asks for, but you can opt in per parameter.
  • arguments must be a JSON object per spec, so null and [] are refused with -32600 - PHP clients encoding an empty array will notice, our own Client doesn't send that.
  • array<string, T> generates object instead of array, which only worked around the lost {}. An empty default is advertised as {}.
  • OriginatingRequestBinder sorts \stdClass as well, otherwise a retry with reordered nested members fails verification.

A PHP object can't hold a member name starting with NUL. Such a message falls back to arrays and parses like before, except tools/call, which can't keep its argument types then and is refused with -32600 - that's why this drops the NUL datasets and the raw handler from #665.

BC breaks are in the CHANGELOG.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Active issues cause malformed JSON to return the wrong protocol error and schema overrides to leave advertised defaults that fail validation.

2 open issues
What changed in this PR

Preserves JSON object and array distinctions in tool arguments, addressing validation failures from #586 and supporting type-preserving handler inputs from #587.

Changes:

  • Preserve JSON types through parsing and validation, with conversion based on handler parameter types.
  • Update map schemas, empty defaults, and request-state binding.
  • Add regression coverage and document breaking changes.
File Description
tests/​Unit/​Server/​Transport/​StdioDualEraTest.php Test JSON types and invalid arguments across both protocol eras.
tests/​Unit/​Server/​Stateless/​StatelessProtocolTest.php Use object-shaped empty arguments.
tests/​Unit/​Server/​Stateless/​StandardHeaderValidatorTest.php Update empty argument fixtures.
tests/​Unit/​Server/​Stateless/​OriginatingRequestBinderTest.php Test object ordering and type-sensitive binding.
tests/​Unit/​Server/​RequestContextTest.php Use object-shaped request arguments.
tests/​Unit/​Server/​Handler/​Request/​CallToolHandlerTest.php Encode argument fixtures as objects.
tests/​Unit/​Server/​BuilderTest.php Update empty tool arguments.
tests/​Unit/​Schema/​Request/​CallToolRequestTest.php Cover argument parsing, rejection, and serialization.
tests/​Unit/​JsonRpc/​MessageFactoryTest.php Cover type preservation and batch detection.
tests/​Unit/​Client/​Stateless/​HeaderFactoryTest.php Update empty argument fixture.
tests/​Unit/​Capability/​Registry/​ReferenceHandlerTest.php Test parameter-driven object conversion.
tests/​Unit/​Capability/​Discovery/​SchemaGeneratorTest.php Cover map schemas, defaults, and nested lists.
tests/​Integration/​StatelessLifecycleTest.php Send object-shaped empty arguments.
tests/​Integration/​RequestStateBindingTest.php Remove leading-NUL argument datasets.
tests/​Integration/​Fixture/​request_state.php Remove the custom handler for NUL-member tests.
src/​Server/​Stateless/​StatelessProtocol.php Parse the original request body without re-encoding.
src/​Server/​Stateless/​OriginatingRequestBinder.php Canonicalize objects while preserving type distinctions.
src/​Server/​Handler/​Request/​CallToolHandler.php Validate root arguments as an object.
src/​Schema/​Request/​CallToolRequest.php Preserve nested types and require object arguments.
src/​Schema/​JsonRpc/​Request.php Add overridable parameter conversion.
src/​Schema/​JsonRpc/​Notification.php Convert decoded objects to arrays.
src/​JsonRpc/​MessageFactory.php Decode objects distinctly and reject NUL-prefixed members.
src/​JsonRpc/​Json.php Add recursive object-to-array conversion.
src/​Capability/​Registry/​ReferenceHandler.php Convert arguments according to parameter types.
src/​Capability/​Discovery/​SchemaGenerator.php Infer map types and normalize empty object defaults.
docs/​servers/​schemas.md Explain argument representations and map annotations.
CHANGELOG.md Record compatibility changes.

Review effort: Balanced

Comment thread src/JsonRpc/MessageFactory.php Resolved
@chr-hertel chr-hertel added the Server Issues & PRs related to the Server component label Nov 8, 2026
@chr-hertel chr-hertel added this to the 0.10.0 milestone Nov 8, 2026
@chr-hertel
chr-hertel requested a balanced review from Copilot November 8, 2026 11:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The iterable conversion rejects valid Traversable arguments such as ArrayIterator before handler invocation, so existing Traversable values must be preserved.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Preserve Traversable arguments for iterable parameters

src/​Capability/​Registry/​ReferenceHandler.php:250

Routing iterable through castToArray() also rejects valid Traversable arguments supplied programmatically: an ArrayIterator accepted by the PHP handler now throws Cannot cast value to array before invocation. Keep existing Traversable values unchanged for iterable parameters, while recursively converting JSON objects and arrays.

2 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Rejecting NUL-prefixed member names and changing request-state canonicalization introduce intentional breaking changes to protocol interoperability and multi-instance compatibility that warrant human review.

No open issues 🎉

Review effort: Balanced

chr-hertel added a commit that referenced this pull request Nov 8, 2026
It decodes to arrays, which loses the difference between {} and [] that #678 keeps.
@chr-hertel
chr-hertel requested a balanced review from Copilot November 8, 2026 13:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the changes preserving JSON object and array distinctions in tool arguments.

No open issues 🎉

Review effort: Balanced

@chr-hertel
chr-hertel merged commit 0d340e1 into modelcontextprotocol:main Nov 8, 2026
31 checks passed
@chr-hertel chr-hertel added the breaking change Breaking the Backwards Compatibility Promise label Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Breaking the Backwards Compatibility Promise Server Issues & PRs related to the Server component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Preserve native JSON types when passing arguments to tool handlers SchemaValidator rejects nested empty JSON objects as arrays

2 participants