Skip to content

[Schema][Server] Close the 2025-11-25 schema gaps and add the 2026-07-28 surface - #421

Merged
chr-hertel merged 7 commits into
modelcontextprotocol:mainfrom
chr-hertel:feature/schema-2026-07-28-drift
Aug 17, 2026
Merged

[Schema][Server] Close the 2025-11-25 schema gaps and add the 2026-07-28 surface#421
chr-hertel merged 7 commits into
modelcontextprotocol:mainfrom
chr-hertel:feature/schema-2026-07-28-drift

Conversation

@chr-hertel

@chr-hertel chr-hertel commented Aug 15, 2026

Copy link
Copy Markdown
Member

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

  • Url elicitation. ElicitationMode splits form from url, which sends the user out of band and returns only the accept/decline/cancel outcome — hence the optional requestedSchema and the new url. 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 sub-capabilities. 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 through Client\Builder::setClientInfo() and Server\Builder::setServerInfo().

From 2026-07-28

  • SEP-2106. Tool::outputSchema and CallToolResult::structuredContent accept any JSON value. ToolReference::extractStructuredContent() keeps a scalar when the tool declared an outputSchema and the negotiated revision allows it; CallToolHandler warns when a self-built result carries a value the revision does not permit.
  • Error codes -32020 header mismatch, -32021 missing required client capability, -32022 unsupported protocol version. ProtocolVersionMiddleware returns 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 with isset(), so an explicit {"mode": null} became auto instead of being rejected; and ToolUseContent accepted a list as input (serialized as a JSON array, which the protocol forbids) and silently dropped a malformed _meta.

🤖 Generated with Claude Code

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.

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 thread src/Schema/Result/CallToolResult.php
Comment thread src/Schema/Result/ElicitResult.php Outdated
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) {
Comment thread src/Server/ClientGateway.php Outdated
Comment thread src/Schema/Tool.php
…-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
chr-hertel force-pushed the feature/schema-2026-07-28-drift branch from fd4198f to c1a0d3a Compare August 15, 2026 03:02
@chr-hertel chr-hertel changed the title [Schema][Server] Add the 2026-07-28 schema surface outside sampling [Schema][Server] Close the 2025-11-25 schema gaps and add the 2026-07-28 surface Aug 15, 2026
@chr-hertel chr-hertel added this to the 0.8.0 milestone Aug 16, 2026
@chr-hertel chr-hertel added Server Issues & PRs related to the Server component Schema Issues & PRs related to the Schema component improves spec compliance Improves consistency with other SDKs such as TyepScript labels Aug 16, 2026
…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
chr-hertel force-pushed the feature/schema-2026-07-28-drift branch from c1a0d3a to 06063b3 Compare August 17, 2026 21:06
@chr-hertel chr-hertel added the 2026-07-28 All issues and PRs related to the spec release 2026-07-28 label Aug 17, 2026
@chr-hertel
chr-hertel merged commit f84d8f5 into modelcontextprotocol:main Aug 17, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2026-07-28 All issues and PRs related to the spec release 2026-07-28 improves spec compliance Improves consistency with other SDKs such as TyepScript Schema Issues & PRs related to the Schema component Server Issues & PRs related to the Server component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants