[Schema][Server] Close the 2025-11-25 schema gaps and add the 2026-07-28 surface - #421
Merged
chr-hertel merged 7 commits intoAug 17, 2026
Conversation
chr-hertel
requested review from
CodeWithKyrian,
Nyholm and
soyuka
as code owners
August 15, 2026 02:32
Contributor
There was a problem hiding this comment.
Pull request overview
Adds the optional 2026-07-28 schema surface while preserving existing defaults.
Changes:
- Adds URL elicitation, sub-capabilities, icon themes, and implementation titles.
- Widens tool output schemas and structured content; hardens sampling types.
- Adds protocol error codes, structured version errors, and coverage.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
CHANGELOG.md |
Documents the schema additions. |
src/Client/Builder.php |
Supports client titles. |
src/Schema/ClientCapabilities.php |
Models elicitation sub-capabilities. |
src/Schema/Content/ToolUseContent.php |
Validates tool input and metadata. |
src/Schema/Enum/ElicitationMode.php |
Defines form and URL modes. |
src/Schema/Enum/IconTheme.php |
Defines light and dark themes. |
src/Schema/Icon.php |
Adds icon theme support. |
src/Schema/Implementation.php |
Adds implementation titles. |
src/Schema/JsonRpc/Error.php |
Adds protocol error codes and factories. |
src/Schema/Request/ElicitRequest.php |
Models form and URL elicitation. |
src/Schema/Result/CallToolResult.php |
Widens structured content values. |
src/Schema/Result/ElicitResult.php |
Makes result parsing mode-aware. |
src/Schema/Tool.php |
Allows non-object output schemas. |
src/Schema/ToolChoice.php |
Rejects explicit null modes. |
src/Server/Builder.php |
Supports server titles. |
src/Server/ClientGateway.php |
Adds URL elicitation APIs. |
src/Server/Transport/Http/Middleware/ProtocolVersionMiddleware.php |
Returns structured version errors. |
tests/Unit/Client/BuilderTest.php |
Covers client title forwarding. |
tests/Unit/Schema/ClientCapabilitiesTest.php |
Covers elicitation capabilities. |
tests/Unit/Schema/Content/ToolUseContentTest.php |
Covers stricter tool inputs. |
tests/Unit/Schema/ElicitationModeTest.php |
Covers elicitation wire shapes. |
tests/Unit/Schema/IconTest.php |
Covers icon themes. |
tests/Unit/Schema/ImplementationTest.php |
Covers implementation titles. |
tests/Unit/Schema/JsonRpc/ErrorCodesTest.php |
Covers new error payloads. |
tests/Unit/Schema/NonObjectOutputSchemaTest.php |
Covers widened output values. |
tests/Unit/Schema/Result/ElicitResultTest.php |
Covers mode-aware results. |
tests/Unit/Schema/ToolChoiceTest.php |
Covers null mode rejection. |
tests/Unit/Server/BuilderTest.php |
Covers server title forwarding. |
tests/Unit/Server/ClientGatewayTest.php |
Covers URL elicitation and capabilities. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+57
to
+59
| $content = isset($data['content']) && \is_array($data['content']) ? $data['content'] : null; | ||
|
|
||
| if (ElicitAction::Accept === $action && null === $content) { | ||
| if (ElicitationMode::Form === $mode && ElicitAction::Accept === $action && null === $content) { |
…-28 surface Ports the type definitions this SDK still misses outside of sampling tool use, which modelcontextprotocol#409 and modelcontextprotocol#420 already cover. Every addition is optional and defaults to current behaviour, so a connection negotiated on an older revision is unaffected. From 2025-11-25, elicitation gains modes. ElicitationMode splits `form` — build a form from the requested schema — from `url`, which sends the user out of band and returns only the accept/decline/cancel outcome. That is why requestedSchema becomes optional and `url` appears beside it. ClientCapabilities learns the matching sub-capabilities, where an `elicitation` naming no mode declares form, the only shape that existed before. Icon gains `theme` from the same revision, and Implementation gains the `title` BaseMetadata has carried since 2025-06-18. From 2026-07-28, schemas loosen where the revision loosens them: SEP-2106 drops the object-only restriction, so Tool::outputSchema may describe any JSON value and CallToolResult::structuredContent follows. The same revision defines three error codes (-32020 header mismatch, -32021 missing required client capability, -32022 unsupported protocol version). ProtocolVersionMiddleware switches to the last of them, so a rejected version carries the supported set as structured data the client can retry from rather than only as prose.
`Implementation::title` reached the typed constructor unchecked, so malformed
wire data raised a TypeError instead of InvalidArgumentException.
`ToolUseContent::input` accepted a list and serialized it as a JSON array,
where the protocol requires an object. The empty array stays exempt: it is
also an empty map and still emits `{}`.
`ToolChoice` and `ElicitRequest` read their mode with isset(), which is false
for an explicit null, so `{"mode": null}` silently became the default instead
of being rejected. Both use array_key_exists() now, letting the existing type
check refuse null.
…ders `Implementation::title` could be parsed but never sent: neither `Client\Builder::setClientInfo()` nor `Server\Builder::setServerInfo()` accepted one, so every SDK user emitted null. Both gain a trailing optional `$title`. On the server it sits where the Implementation constructor already puts it, so existing positional calls keep their meaning; the client builder forwards it by name, leaving the icons and websiteUrl slots defaulted.
The object-only hydration guard was never that: `!is_array()` admitted `[1, 2, 3]` and `[]`, which serialize to JSON arrays, while rejecting the scalars 2026-07-28 permits. The truthiness emission gate was backwards in the same way — it dropped `[]`, `0`, `false` and `""`, yet emitted lists, strings and an empty stdClass. Hydration now accepts any JSON value, and `null` alone means absent, matching `ToolResultContent` which already carries this field that way. Which values a given revision permits is a question for version-aware serialization, which results cannot answer yet.
`ElicitRequest::forUrl()` built a request no SDK user could send: the only public gateway method always constructed form mode from an ElicitationSchema, and `request()` is private. `elicitUrl()` joins `elicit()`, and both funnel through one send path that hydrates the result with the request's own mode. Without that, a url-mode accept — contentless by design — threw, because ElicitResult requires content whenever the action is accept. The result carries no discriminator of its own, so the mode has to come from the request it answers. `supportsElicitationUrl()` reports whether the client named the mode, reusing the sub-capability reader the sampling checks already use.
chr-hertel
force-pushed
the
feature/schema-2026-07-28-drift
branch
from
August 15, 2026 03:02
fd4198f to
c1a0d3a
Compare
…to end Review follow-up on the SEP-2106 widening, which stopped at the type. `ToolReference::extractStructuredContent()` returned `?array`, so a tool declaring a scalar `outputSchema` had its result dropped and logged as unsendable. It returns `mixed` now and keeps a scalar — but only from 2026-07-28 on, and only when the tool declared an outputSchema: without one the value is already carried in `content`, and advertising it twice is not an improvement. `CallToolHandler` only warned about list-shaped `structuredContent` on a self-built `CallToolResult`, so a scalar reached revisions that require an object unremarked. The check is on the shape now, not on the list case alone. `ElicitResult` retained a `content` the spec says is absent from url-mode results, leaving a malformed response indistinguishable from a valid one. `Tool::jsonSerialize()` emitted the empty root schema as `[]`, which is not a schema at all. Sub-schemas already had this treatment.
chr-hertel
force-pushed
the
feature/schema-2026-07-28-drift
branch
from
August 17, 2026 21:06
c1a0d3a to
06063b3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports the type definitions this SDK still misses outside sampling tool use — #409 and #420 cover that. Every addition is optional and defaults to current behaviour, so a connection negotiated on an older revision is unaffected.
From 2025-11-25
ElicitationModesplitsformfromurl, which sends the user out of band and returns only the accept/decline/cancel outcome — hence the optionalrequestedSchemaand the newurl.ClientGateway::elicitUrl()makes it sendable;ElicitResult::fromArray()takes the request's mode, requires content only in form mode, and rejects it in url mode.elicitation.form/elicitation.url, where a capability naming no mode declares form — the only shape that existed before url mode.Icon::theme.From 2025-06-18
Implementation::title(BaseMetadata), now settable throughClient\Builder::setClientInfo()andServer\Builder::setServerInfo().From 2026-07-28
Tool::outputSchemaandCallToolResult::structuredContentaccept any JSON value.ToolReference::extractStructuredContent()keeps a scalar when the tool declared anoutputSchemaand the negotiated revision allows it;CallToolHandlerwarns when a self-built result carries a value the revision does not permit.-32020header mismatch,-32021missing required client capability,-32022unsupported protocol version.ProtocolVersionMiddlewarereturns the last one with the supported set as structured data the client can retry from.Two fixes to the sampling types #409/#420 left untouched:
ToolChoice::fromArray()read its mode withisset(), so an explicit{"mode": null}becameautoinstead of being rejected; andToolUseContentaccepted a list asinput(serialized as a JSON array, which the protocol forbids) and silently dropped a malformed_meta.🤖 Generated with Claude Code