Commit graph php-sdk/tests/Unit/Schema
Author SHA1 Message Date
Christopher Hertel
1bd7895b5f
[Schema] Fix _meta guard key in Request and Notification jsonSerialize() (#485) 2026-09-05 01:18:54 +02:00
Christopher Hertel
8c06dcecc6
[Client] Speak the 2026-07-28 lifecycle (#456)
* [Client] Speak the 2026-07-28 lifecycle

On that revision the client opens with server/discover instead of
initialize, stamps each request's _meta with the protocol version, its own
capabilities and client info, and sends the standard Mcp-Method / Mcp-Name /
Mcp-Param-* headers an intermediary routes on. ToolCatalog is what knows,
from the tool list, which arguments a call has to mirror.

An input_required result is answered rather than surfaced: InputRequestResolver
asks the host's elicitation, sampling and roots handlers and retries the same
request carrying inputResponses and the requestState the server sent.

McpHeader holds the header names and the =?base64?…?= sentinel, so the two
sides no longer keep their own copies of the same rules.

* Fix CI baseline drift and address Copilot review findings

Drop stale expected-failure entries the new client fixture now passes.

Copilot review: fix McpHeader round-trip for a wrapper-shaped literal,
encode empty/numeric-keyed inputResponses as a JSON object, stop the
incident example repeating side effects on MRTR retry, and reset
ToolCatalog on reconnect instead of carrying the previous server's
verdicts.

* Skip DualEraExampleTestCase on PHP 8.1

PHP_CLI_SERVER_WORKERS does not reliably fork multiple php -S
workers there (php/php-src#9400), reproducing the single-worker
deadlock the extra workers exist to avoid.
2026-08-19 02:41:02 +02:00
Christopher Hertel
5e73683079
[Server] Close the conformance gaps in the modern lifecycle (#447)
* [Server] Let the client gateway see modern-era capabilities

The six supports*() probes read session state only InitializeHandler wrote,
so every request served statelessly reported no client capabilities at all —
a tool guarding on them took the unsupported branch without a signal.
StatelessProtocol now writes the request's declaration under the same keys.

* [Server] Reject the RPCs 2026-07-28 removed

resources/subscribe and resources/unsubscribe were still dispatched and
answered 200 OK, recording subscriptions the modern era never reads. They
join initialize, ping and logging/setLevel in the era guard; the handlers
stay registered because the handshake era still serves them.

* [Server] Decode a wrapped Mcp-Name before comparing it

The Base64 sentinel was only unwrapped for Mcp-Param-*, so any resource URI
or tool name outside the header-safe ASCII set — which the spec explicitly
tells clients to wrap — was compared encoded and refused with -32020.

* [Server] Require the MCP-Protocol-Version header

It was only checked for contradicting the body, so omitting it entirely
passed — leaving the value intermediaries route on unenforced. Tied to the
header validator's presence, since that is what a header-bearing transport
installs and stdio carries its metadata inline.

* [Server] Read the modern-era request body whole

A single read() takes whatever the stream cares to give — 64 bytes from a
chunked transfer is legal — so an oversized-but-valid POST came back as a
parse error. Shares the legacy transport's incremental read as a trait.

* [Schema][Server] Answer a notification with a status, not a response

A body with no id was dispatched as a request and came back as a JSON-RPC
error carrying "id": "" — an id nobody issued, and on a path a client uses
to tell a modern server from a legacy one. Notifications now get 202 and no
body, and Error omits an id it could not read instead of emptying it.

* [Server] Stop emitting the error codes 2026-07-28 reserved

resources/read answered -32002, which this revision forbids; prompts/get
and completion/complete answered it for an unknown *name*, which it never
meant; tools/call answered -32601, which belongs to an unknown method.
Only the resources/read code is revision-gated — older peers expect -32002
there, and nowhere else. Also routes the -32021 capability exception out of
the prompt and resource handlers, which were swallowing it into -32603.
2026-08-18 23:09:34 +02:00
Christopher Hertel
60711951d4
[Schema][Server] Add the extensions framework SEP-2133 defines (#443)
* [Schema][Server] Add the extensions framework SEP-2133 defines

enableExtension() took any string and advertised it, with no way for an
extension to add a method: MessageFactory could not decode one, so nothing
downstream ever saw it.

Identifiers are now checked against the _meta key naming rules through
ExtensionIdentifier - a prefix is mandatory, since an unprefixed name has no
owner, and the modelcontextprotocol/mcp second labels the specification
reserves are recognised. An extension implementing
MethodProvidingExtensionInterface contributes both its message classes and
the handlers serving them.

MessageFactory::make() takes an $additional list of message classes, and
RequestHandlerInterface's result template is covariant so a handler
declaring a concrete result satisfies a collection typed by the interface.

* Fix stale @see reference in MethodProvidingExtensionInterface docblock

* [Schema][Server][Client] Make ExtensionIdentifier a value object

ExtensionInterface::getId() now returns ExtensionIdentifier, which
validates the SEP-2133 naming rules at construction instead of the
callers checking it. getMessages()/getRequestHandlers() move into
ExtensionInterface itself; AbstractExtension gives them empty
defaults for extensions that only announce a capability.
2026-08-18 20:47:27 +02:00
Christopher Hertel
605df8771f
[Schema] Refuse an invalid x-mcp-header annotation at definition time (#441)
* [Schema] Refuse an invalid x-mcp-header annotation at definition time

SEP-2243 calls a tool definition invalid when its x-mcp-header annotation is
empty, is not an HTTP field name, collides case-insensitively with another,
or sits on a number, array or object property - none of which can be
mirrored onto a header.

Nothing said so. The earliest place to is where the tool is defined, rather
than on the request that trips over it.

* Clarify x-mcp-header validation only reaches properties

* Make checkHeaderAnnotations() a private instance method

* Make headerAnnotations() a private instance method

* Apply suggestion from @chr-hertel
2026-08-18 20:16:35 +02:00
Christopher Hertel
cf2c4d4ead
[Schema] Stop rejecting elicitation schemas the specification allows (#437)
* [Schema] Stop rejecting elicitation schemas the specification allows

The specification makes `title` optional on every elicitation schema
definition. AbstractSchemaDefinition required it, so a client reading a
conformant server's elicitation/create refused the request - and a server
building one could not omit it either.

`title` is now nullable on all seven definitions, omitted from the
serialized form when absent, and rejected only when present and not a
string.

* Fix stale title docblocks; allow omitting title where safe

* Give Number's title param a default too

* Apply suggestions from code review

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-18 19:49:15 +02:00
Christopher Hertel
08d332c7ac
[Schema] Serialize a settingless extension as an object (#438)
* [Schema] Serialize a settingless extension as an object

An extension declaring support with no settings was advertised as `[]`, an
empty JSON array, which the wire schema rejects - `extensions` maps an id to
a settings *object*. Both ServerCapabilities and ClientCapabilities now cast
each entry, so it goes out as `{}`.

* Apply suggestion from @chr-hertel
2026-08-18 14:59:12 +02:00
Christopher Hertel
d5351759d7
[Schema] Drop SDK-only name pattern on resources and resource templates (#430)
* [Schema] Drop SDK-only name pattern on resources and resource templates

* Update src/Schema/ResourceDefinition.php

* Update src/Schema/ResourceTemplate.php
2026-08-18 02:09:07 +02:00
Christopher Hertel
6f58fe596a
[Schema][Client][Server] Close MCP Apps gaps: extension negotiation checks (#429)
* [Schema][Client][Server] Close MCP Apps gaps: extension negotiation checks

Add ClientGateway::supportsExtension(), Client\Builder::enableExtension() and
ClientCapabilities::withExtensions(); introduce side-agnostic ExtensionInterface
and deprecate ServerExtensionInterface; document attribute-based UI linkage.

* Drop ServerExtensionInterface in favour of ExtensionInterface
2026-08-18 01:46:53 +02:00
Christopher Hertel
f84d8f5aa9
[Schema][Server] Close the 2025-11-25 schema gaps and add the 2026-07-28 surface (#421)
* [Schema][Server] Close the 2025-11-25 schema gaps and add the 2026-07-28 surface

Ports the type definitions this SDK still misses outside of sampling tool use,
which #409 and #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.

* [Schema] Reject what the 2026-07-28 types cannot represent

`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.

* [Client][Server] Advertise the implementation title through both builders

`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.

* [Schema] Serialize every non-null structuredContent

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.

* [Schema][Server] Make url elicitation reachable

`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.

* [Schema] Add CHANGELOG entry for the 2026-07-28 surface

* [Capability][Schema][Server] Carry the widened structuredContent end 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.
2026-08-17 23:09:39 +02:00
Christopher Hertel
43815e38a8
[Schema][Server] Harden sampling tools against the spec (#420)
* harden sampling tools against the spec

Report tool-flow violations as -32602 instead of dropping the request,
accept resource_link in tool results, gate tools on sampling.tools, and
reject empty content.

* normalize keyed content arrays to lists

array_filter() and friends preserve keys, and a keyed array serializes as a
JSON object instead of the content-block array the schema requires.
2026-08-15 04:13:53 +02:00
Christopher Hertel
becce29c41
[Capability] Harden structuredContent extraction (#417)
* Address review feedback on resource_link

Three follow-ups from review of the resource_link work:

* PromptResultFormatter: add a regression test proving the optional
  fields of a typed resource_link block (title, description, mimeType,
  size, annotations, _meta) survive formatting. The delegation to
  ResourceLink::fromArray() already landed while rebasing onto the
  PromptResultFormatter refactor, but the existing test only supplied
  uri/name and so could not catch a reintroduced field drop.

* ResourceLink::fromArray(): validate optional fields consistently, the
  way the equivalent ResourceDefinition::fromArray() already does.
  description, mimeType and size now raise InvalidArgumentException
  instead of surfacing a TypeError (or silently coercing, for size's
  (int) cast); annotations goes through Annotations::tryFromArray(),
  which carries the is_array() guard that icons already had; icons uses
  Icon::listFromArray() so a non-array entry is reported in context.

* ToolReference::extractStructuredContent(): never emit a list as
  structuredContent. The Content guard added earlier fixed only one
  instance of the real invariant - structuredContent must be a JSON
  object, and a PHP list can never be one. Tool::fromArray() already
  enforces the matching rule by rejecting an outputSchema whose type is
  not "object", so this aligns the runtime path with it. The test
  asserting the opposite for an array-typed outputSchema contradicted
  the phpstan type and the fromArray() check introduced alongside it in
  the same commit, so it is replaced by tests for the two list shapes.

* Apply the structuredContent object rule to object results too

`extractStructuredContent()` guards raw array results against being emitted
as a JSON array, but the object branch handed back whatever `json_decode()`
produced. A `JsonSerializable` returning a list or a scalar slipped straight
through, producing exactly the `structuredContent` the array guard exists to
prevent — and a return value that contradicts the method's own
`array<string, mixed>|null` signature.

Check the decoded value before returning it, and cover the object branch,
which had no tests at all.

* Document structured tool output

`outputSchema` and `structuredContent` were undocumented: the tool return
value docs covered only the `content` side, and the schema generation
section is about tool parameters.

Add a "Structured Output" subsection covering how to declare the schema,
which return values populate `structuredContent`, and why a list has to be
wrapped in a key to get structured output at all.

* Gate structuredContent on the negotiated protocol revision

SEP-2106 (revision 2026-07-28) widens structuredContent to any JSON value;
earlier revisions require an object. Resolve the revision per request and
apply the matching rule, and warn when a declared outputSchema yields none.

* Resolve the protocol revision on RequestContext

Keeps the `_meta`-then-session lookup in one place instead of per handler,
and exposes the negotiated revision to tool handlers.

* Document reading the negotiated revision from RequestContext

* Warn when a self-built CallToolResult carries an invalid structuredContent

Returning a CallToolResult opts out of the extraction rules, so the value is
still sent unchanged — but a JSON array is not valid before SEP-2106.
2026-08-15 04:03:20 +02:00
wWzZb
82aac972c1
[Schema][Server] feat: support sampling with tools (#409)
* feat: support sampling with tools

* address sampling tools review feedback

* tighten sampling tools spec compliance

* drop SamplingStopReason enum in favor of open string

The spec leaves stopReason open for provider-specific values, so upcasting
the four known ones to enum cases makes every future enum addition a silent
BC break for string comparisons.

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-15 03:28:11 +02:00
Christopher Hertel
eb20453f49
[Client][Schema][Server] Implement protocol version negotiation (#403)
* [Client][Docs][Schema][Server] Implement protocol version negotiation

The `initialize` handler never looked at the version the client asked for:
it always answered with the server's configured version (or the SDK default),
so a client offering a revision the server supports could still be told to
speak a different one. The spec requires echoing back a supported request and
only counter-offering when the requested version cannot be honoured.

Turns ProtocolVersion into the registry the other SDKs converged on
(typescript-sdk's protocolEras.ts, python-sdk's mcp_types/version.py):
`latestHandshake()`, `handshakeVersions()`, `modernVersions()` and ordered
comparison through `isAtLeast()`. Comparison goes through declaration order
rather than string collation, so an unrecognized peer string compares
conservatively instead of accidentally sorting above a known revision. The
one throw the enum can produce is the SDK's own LogicException, so a consumer
catching `Mcp\Exception\ExceptionInterface` does not miss it.

Adds 2026-07-28 as a known revision and splits the set into two eras:
handshake (2025-11-25 and earlier, negotiated via `initialize`) and modern
(2026-07-28+, per-request `_meta`). The lists are kept apart so a modern
revision can never leak into an `initialize` exchange, which has no
`initialize` at all. Serving the modern era is follow-up work; this only
teaches the SDK that the revision exists and must not be mis-negotiated.
Deliberately absent is any "newest revision in any era" accessor, which
would hand back the one revision the handshake must never offer.

Server: negotiates and stores the result on the session. A version pinned in
Configuration still pins the handshake to exactly that revision.
Client: validates the counter-offer and fails the handshake instead of
continuing on a revision it cannot speak; records the negotiated version on
the client state, readable through the new Client::getProtocolVersion().

The HTTP middleware now defaults to the handshake versions rather than every
declared case, so it stops accepting a modern version the server cannot yet
serve. When the header is absent it falls back to
ProtocolVersion::DEFAULT_NEGOTIATED_VERSION (2025-03-26), the revision that
introduced both Streamable HTTP and the header itself.

The negotiation behaviour is one provideNegotiationTable() data provider
whose rows are the documented table -- supported revision echoed back,
unknown or malformed counter-offered, modern counter-offered -- so the docs
and the suite cannot drift apart. The supported-revision rows iterate
handshakeVersions() rather than a literal list, so declaring a new handshake
revision extends the table automatically, and the "never answer with a modern
revision" assertion runs on every row.

Documented in server-builder.md (the enum registry, how the server answers a
handshake, what pinning changes), transports.md (the header check) and
client.md (the client's side), all linking the spec's versioning section.

* [Client][Docs][Schema][Server] Warn when the configured protocol version cannot be negotiated

* Log a warning instead of silently falling back when a modern revision is
  configured on the client or pinned on the server builder.
* Document that setProtocolVersion() does not restrict the MCP-Protocol-Version
  header check, which validates against a fixed set.
* Pin every revision's era in a test, so a new case declared past the modern
  boundary fails instead of dropping out of the handshake set.
* Add CHANGELOG entry.

* [Client][Docs][Schema][Server] Rename version fallback constant and trim comments
2026-08-15 01:41:19 +02:00
Alex Rothberg
fc456125e2
[Schema] Add resource_link content block (#400)
* Add resource_link content block

The spec's `resource_link` content type (protocol revision 2025-06-18,
still present in 2025-11-25) had no corresponding schema class: it lets a
tool result or prompt message reference a resource by URI/name without
embedding its full contents, the way `EmbeddedResource` does. Without it,
tools returning many or large resource references (e.g. search results
over a DAM) are stuck smuggling URIs through text/structured content.

Add `Mcp\Schema\Content\ResourceLink`, mirroring the spec's `Resource`
shape (uri, name, title, description, mimeType, annotations, size, icons,
_meta) with a fixed `type: "resource_link"`, following the conventions of
the sibling `EmbeddedResource`/`ResourceDefinition` classes.

Register it everywhere `resource` content blocks are (de)serialized by
type per the spec's `ContentBlock` union:
- `CallToolResult::fromArray()`
- `PromptMessage` (constructor type, `fromArray()`, phpstan types)
- `PromptResultFormatter` (typed-array and direct-instance paths)

`SamplingMessage`/`CreateSamplingMessageResult` are intentionally left
unchanged: the spec's `SamplingMessageContentBlock` does not include
`ResourceLink`.

Fixes #399

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add resource_link example coverage to discovery-userprofile

Add a lookup_user tool to the discovery-userprofile example: given a
userId, it returns a short text summary plus a resource_link pointing
at the existing user://{userId}/profile resource template, instead of
embedding the profile. This mirrors the PR's motivating use case (a
tool referencing many/large resources by URI rather than inlining
them) and exercises resource_link through the example's own inspector
snapshot tests (tools_call-lookup_user[_not_found], tools_list).

Wiring this up surfaced a real bug: ToolReference::extractStructuredContent()
passed any raw array return value straight through as structuredContent,
even when it held Content instances (e.g. the TextContent/ResourceLink
pair above). That serializes to a JSON list, which fails the spec's
requirement that structuredContent be an object - the inspector's MCP
client rejected the tool call outright until fixed. Skip structuredContent
extraction whenever the raw result contains a Content item, since those
are already represented in `content` and were never meant to double as
structured data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Rothberg <51059+cancan101@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 23:20:56 +02:00
Johannes Wachter
bdc9d86671
[Client][Server] Add Roots support (#395)
* [Client][Server] Add Roots support

Wire up the MCP "roots" capability on both sides:

Client (php-sdk as client):
- RootsCallbackInterface + ListRootsRequestHandler answer server roots/list
  requests, mirroring the Sampling handler.
- Client::sendRootsListChanged() emits notifications/roots/list_changed.

Server (php-sdk as server):
- ClientGateway::listRoots() requests the client's roots, and
  supportsRoots() reports whether the client advertised the capability,
  normalizing the stored capabilities to an array so an empty
  capabilities object (\stdClass) doesn't cause a TypeError.
- Add ListRootsResult::fromArray() (used by listRoots()).
- ListRootsRequestHandler forwards RootsException messages to the server
  (new Mcp\Exception\RootsException), mirroring SamplingRequestHandler.
- Client::sendRootsListChanged() throws when the client did not advertise
  the roots.listChanged capability.

Examples, docs and tests:
- examples/README/docs advertise
  `new ClientCapabilities(roots: true, rootsListChanged: true)` before
  sending notifications/roots/list_changed.
- CHANGELOG entry for Roots (unreleased 0.7.0 section at the time).
- Unit tests for capability serialization/round-trip, sendRootsListChanged
  gating, and RootsException message forwarding.

phpunit, php-cs-fixer and phpstan green.

* [Client][Server] Address review: schema validation, changelog section, connection guard, working example

Responds to chr-hertel's review of the Roots support:

- ListRootsResult::fromArray() rejects non-array root elements instead of
  accepting malformed entries silently.
- CHANGELOG entry moved to the next unreleased section as the release
  train advanced past it (0.7.0 -> 0.8.0).
- Client::sendRootsListChanged() went straight to
  Protocol::sendNotification(), which sends via
  `$this->transport?->send()` -- on a client that is not connected the
  notification was silently dropped. It now throws a ConnectionException
  like sendRequest() does.
- The stdio example only advertised the roots capability, so the
  roots/list handler never actually ran. The client-communication demo
  server gets an `inspect_workspace_roots` tool that calls
  ClientGateway::supportsRoots()/listRoots(); the example calls it, so
  the server actually issues roots/list and the client answers it.
  Dropped the try/catch wrapper from the example.
- ClientTest's connected-client mock now goes through setState() so
  isConnected() reflects a real handshake instead of a stubbed flag.

phpunit, php-cs-fixer and phpstan green.
2026-08-14 23:04:25 +02:00
Christopher Hertel
8bae3c054e
[Capability] Preserve optional content fields in PromptResultFormatter (#406)
* Preserve optional content fields in PromptResultFormatter

The formatter's typed-content path rebuilt each Content object from only
its required fields, silently dropping everything else a prompt returned
(annotations on every type, _meta and explicit mimeType on embedded
resource contents). Delegate to the schema classes' fromArray() instead,
keeping the historical mimeType defaulting and RuntimeException-with-
index error contract.

ImageContent was the only content class without annotations support, so
it gains the same constructor/fromArray()/jsonSerialize() handling as
TextContent and AudioContent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Reword CHANGELOG entry to be user-facing

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 22:47:26 +02:00
Christopher Hertel
c8ed07bba2
[Server] Handle malformed JSON-RPC input graceful (#412)
* Never let a throwable escape Protocol::processInput()

* Validate payload types in the schema hydration methods

* Stop leaking handler exception messages to the client
2026-08-14 22:34:01 +02:00
Björn Meyer
9236824b6d
[Schema] Always emit {} for empty tool schema properties (#407)
* [Schema] Always emit {} for empty tool schema properties

Normalize empty properties arrays (including nested object schemas and
outputSchema) in Tool::__construct so tools/list never serializes
invalid JSON Schema properties: [].

Fixes #405

* [Schema] Normalize every empty sub-schema, not only properties

Walking into `items` and `additionalProperties` without normalizing an empty
sub-schema at those positions left the `[]`-instead-of-`{}` bug intact one
level down. Most notably it regressed #151 on a `Tool::fromArray()`
round-trip: `SchemaGenerator` emits `items: {}` for untyped array parameters,
which decodes to `[]` and was re-serialized as `[]` — the exact schema strict
clients reject.

Empty sub-schemas are now replaced with a `\stdClass` at every schema
position, and the keyword list is completed to cover draft-07 through
2020-12: `$defs`, `definitions`, `dependentSchemas`, `patternProperties`,
`propertyNames`, `contains`, `not`, `if`/`then`/`else`, `additionalItems`,
and `unevaluatedItems`/`unevaluatedProperties`. Keywords holding plain JSON
arrays (`allOf: []`, `enum: []`, `dependentRequired`) keep encoding as `[]`.

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-14 21:50:08 +02:00
Dev
206aa9da52
[Schema] Fix Implementation::fromArray rejecting falsy-but-valid name/version (#393)
* [Schema] Fix Implementation::fromArray rejecting falsy-but-valid name/version

`empty()` treats the string "0" as empty, so a valid clientInfo/serverInfo
with `"version": "0"` (or `"name": "0"`) was rejected with an
InvalidArgumentException. During initialize this aborted request parsing and
surfaced as a misleading "A valid session id is REQUIRED" error.

Replace the `empty()` guards with explicit isset/is_string/empty-string checks
so genuinely missing, non-string, and empty values are still rejected while
"0" is accepted. Adds ImplementationTest covering the regression and the
existing validation paths.

Fixes #392

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [Schema] Suppress PHPStan argument.type on invalid-input tests

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 23:41:51 +02:00
Christopher Hertel
d150772e24
[Schema][Server] Add MCP Apps extension (io.modelcontextprotocol/ui) support (#281)
* feat: add MCP Apps extension (io.modelcontextprotocol/ui) support

Adds support for the MCP Apps protocol extension, which lets servers
expose interactive HTML UIs as resources rendered by spec-conforming
clients (e.g. Goose) in sandboxed iframes.

- Generic extension plumbing: ServerExtensionInterface and
  Builder::enableExtension(), with extensions advertised under
  capabilities.extensions during the initialize handshake.
  ClientCapabilities/ServerCapabilities gain an extensions field.
- Schema DTOs under Mcp\Schema\Extension\Apps: the McpApps marker,
  UiToolMeta, ToolVisibility enum, UiResourceContentMeta,
  UiResourceCsp, and UiResourcePermissions (with empty-object
  presence markers per spec).
- A worked weather example under examples/server/mcp-apps/ with a
  minimal spec-conforming view (ui/initialize handshake, postMessage
  JSONRPCMessage objects, and ResizeObserver-driven size reporting).
- docs/extensions.md guide with pointers from index.md and
  examples.md to the ext-apps repository for the TypeScript SDK and
  richer view-side patterns.
- Unit and inspector snapshot test coverage.

Spec: https://github.com/modelcontextprotocol/ext-apps

* Type UiToolMeta visibility as ToolVisibility enum

Also fix UiResourceContentMeta::jsonSerialize() return phpdoc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address review nitpicks for MCP Apps extension

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Apply PR review feedback for MCP Apps extension

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add regression tests and CHANGELOG entry for capabilities extensions

- Pin the is_array() guards on extensions in ServerCapabilities and
  ClientCapabilities::fromArray() with explicit non-array payload tests.
- Pin the isset()-rejects-null contract in UiResourcePermissions::fromArray().
- Surface the new extensions field on both capability classes in CHANGELOG.
- Inline the ?: idiom in Builder::build() to match the surrounding style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 09:35:54 +01:00
Mateu Aguiló Bosch
0347dc85b4
[Server] Allow runtime-resolved element handlers (#294)
* [Server] feat: allow runtime handlers

Adds Mcp\Server\Handler\RunTimeHandlerInterface so integrators
(e.g. configuration-driven element sources) can register handlers
whose arguments and execution are resolved at runtime rather than
via PHP reflection of a callable.

ReferenceHandler::handle() dispatches instanceof RunTimeHandler
ahead of the callable/array/string branches, calls filterArguments
to narrow the argument map, and forwards execute() a ClientGateway
built from the active session.

The Registry-layer surface (ElementReference and the four
*Reference subclasses, RegistryInterface, Registry::register*,
ArrayLoader's handler-description helper) is widened to accept the
new type. The Builder/ArrayLoader path keeps its narrow callable
union; exposing runtime handlers there requires reworking
HandlerResolver and is left for a follow-up.

* feat(server): add runtime handler metadata API

Extend RunTimeHandlerInterface with four nullable metadata
accessors (getInputSchema, getOutputSchema, getPromptArguments,
getCompletionProviders) so the loader can obtain element
metadata without reflection.

Add RunTimeHandlerTrait providing null defaults for all four
accessors so single-purpose handlers can override only the
methods relevant to their element kind.

* feat(server): expose runtime handlers via Builder

Widen Builder::addTool, addResource, addResourceTemplate, and
addPrompt to accept RunTimeHandlerInterface instances alongside
the existing closure/array/string handler forms.

Branch ArrayLoader::load() on instanceof RunTimeHandlerInterface
in each of the four registration loops so runtime handlers skip
the reflection-based HandlerResolver/HandlerSchema path and pull
metadata from the interface accessors instead. Require explicit
name and description for runtime handlers and raise
ConfigurationException when missing or when a runtime tool has
no input schema source.

Replace direct $data['handler'] use in error log context with
getHandlerDescription() to avoid serializing large runtime
handler object graphs into log records.

Document the runtime handler API in docs/server-builder.md with a
new Runtime Handlers subsection and add a cross-reference from
docs/mcp-elements.md.

* test(server): cover runtime handler Builder path

Add ArrayLoaderRunTimeHandlerTest covering:
- successful tool registration with handler-supplied input schema,
  with kwarg input schema (precedence over the handler), and the
  same precedence rule for output schema
- ConfigurationException for tools missing name, description, or
  any input schema source
- successful resource registration via Builder kwargs and the
  matching missing-name failure mode
- successful resource template registration with completion
  providers sourced from the handler and the missing-description
  failure mode
- successful prompt registration with arguments and completion
  providers sourced from the handler, plus the missing-name
  failure mode
- a trait-only fixture verifying the four metadata accessors
  return null by default

Update existing ReferenceHandlerTest anonymous classes to use
RunTimeHandlerTrait so they satisfy the widened interface.

* refactor(server): split runtime handler interface

Splits RunTimeHandlerInterface into a minimal base contract (only
execute()) plus element-specific subtypes: RunTimeToolHandlerInterface,
RunTimePromptHandlerInterface, and
RunTimeResourceTemplateHandlerInterface. Each subtype declares only
the metadata accessors relevant to its element kind, so a tool
handler no longer has to no-op out prompt or template methods.

Drops the now-redundant RunTimeHandlerTrait and removes
filterArguments; execute() receives the full argument map directly.

Updates ArrayLoader, ReferenceHandler, and Builder type signatures
to use the per-element interfaces, and rewrites the Runtime
Handlers documentation accordingly.

* refactor(server): rename runtime handler interfaces to Runtime casing

Renames RunTimeHandlerInterface and its element-specific subtypes to
Runtime casing, adds a dedicated RuntimeResourceHandlerInterface so
addResource() rejects mismatched runtime handlers at the type level,
and strips _session and _request from arguments forwarded to runtime
handlers. Hardens ArrayLoader to re-throw ConfigurationException
without wrapping, and extracts inline test fixtures into
tests/Fixtures/Runtime/.

* chore: please the linting gods

* test(server): use Uuid::v4 for symfony/uid 5.4 compatibility

Uuid::v7() was added in symfony/uid 6.2 and breaks the Symfony 5.4
matrix job. Match the convention used elsewhere in the suite.

* refactor(server): split ArrayLoader registration

Extract per-element preparation into dedicated private prepare*
methods for tools, resources, templates, and prompts. Factor the
shared required-field assertion and reflected name/description
resolution into helpers.

* refactor(server): non-null runtime handler types

Drop nullable return types on RuntimeToolHandlerInterface::
getInputSchema(), RuntimePromptHandlerInterface::
getPromptArguments()/getCompletionProviders(), and
RuntimeResourceTemplateHandlerInterface::
getCompletionProviders() in favor of plain arrays.
ArrayLoader no longer needs the null-coalescing
fallbacks, and NullSchemaToolHandler plus its
ConfigurationException test become redundant.

* doc: update docs/server-builder.md

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

* feat: peer revie feedback

Introduce a new `Builder::add(...)` pattern for a more generic runtime
handler.

* refactor(server): apply PR 294 review feedback

Move per-interface handler dispatch from ReferenceHandler into
ExplicitElementLoader by wrapping each ElementHandlerInterface in a
Closure that satisfies the existing callable contract, revert the
*Reference / Registry handler-type widening PR 294 introduced, throw
Mcp\Exception\InvalidArgumentException (not \TypeError) on mismatched
Builder::add() pairings, and rename two classes for clarity:

* Mcp\Capability\Registry\Loader\ArrayLoader -> ReflectedElementLoader
* Mcp\Schema\Resource -> Mcp\Schema\ResourceDefinition (no alias)

ReferenceHandler::prepareArguments() now injects ClientGateway by type
and binds an `array $arguments` / `array $variables` closure parameter
from the leftover inbound arguments. This addresses Antoine's coupling
concern (ReferenceHandler no longer knows about the four handler
interfaces) and Chris's package-exception preference, and clears the
phpdoc reserved-word collision Antoine flagged on the resource schema
class. Existing tests are re-targeted to exercise the closure path
through ExplicitElementLoader / ReferenceHandler.

* doc: address PR 294 review feedback

Apply Chris's line-level suggestions on docs/server-builder.md
(rewrap to 120 chars, smoothed Drupal mcp module name-drop in the
explicit-element registration section, exception name reads
Mcp\Exception\InvalidArgumentException), rewrap docs/mcp-elements.md
to 120 chars, and add 0.6.0 CHANGELOG entries covering the
Builder::add() entry point, the new handler interfaces, the new
ExplicitElementLoader, plus three BC breaks (Resource ->
ResourceDefinition rename, ArrayLoader -> ReflectedElementLoader
rename, mismatched-pairing exception swap).

* refactor: address self-review feedback

* CHANGELOG: drop spurious [BC Break] entry for the exception
  type Builder::add() raises (method is new in this PR).
* docs/server-builder: revert wrap-only edits to pre-existing
  main content; keep 120ch wrapping only for new sections.
* ExplicitElementLoader: trim verbose class docblock.
* ReferenceHandler: rename SPLAT_PARAMETER_NAMES constant to
  ARGUMENT_BAG_PARAMETERS.

* refactor: rename CallableHandler to Handler

* style: fix phpdoc param alignment

* refactor: inline $reference->handler access

* fix: maintainer review feedback

docs: mention ExplicitElementLoader in comment

docs: move Drupal reference to README list

docs: drop implementation-only changelog entry

docs: revert rewrap noise from mcp-elements.md

* style: fix phpdoc param alignment

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
Co-authored-by: soyuka <soyuka@users.noreply.github.com>
2026-05-29 07:16:12 +02:00
Dayna Blackwell
a820497b62
Add title field to Resource and ResourceTemplate (#301)
The MCP specification (2025-11-25) defines an optional title field for
Resource and ResourceTemplate as a human-readable display label, distinct
from name (a short identifier). Tool and Prompt already implement this
field; Resource and ResourceTemplate were missing it.

Changes:
- Add title parameter to Resource and ResourceTemplate constructors
- Add title to fromArray(), jsonSerialize(), and PHPStan type arrays
- Add title to McpResource and McpResourceTemplate attributes
- Pass title through Discoverer when constructing schema objects
- Add tests for deserialization, serialization, and null omission

Fixes #296
2026-05-17 14:10:31 +02:00
Dayna Blackwell
797472777f
[Schema] fix: allow URIs without double slash in Resource and ResourceTemplate (RFC 3986) (#297)
* fix: allow URIs without double slash in Resource and ResourceTemplate (RFC 3986)

The URI regex required "://" after the scheme, rejecting valid URIs
like urn:isbn:123, mailto:user@example.com, config:key, and data:
URIs. RFC 3986 only requires ":" after the scheme; the "//" is part
of the authority component which is optional.

Changed both Resource.URI_PATTERN and ResourceTemplate.URI_TEMPLATE_PATTERN
to require ":" instead of "://".

Adds data-provider tests for URIs without double slash in both classes.

Fixes #293

* Reword unit tests

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-05-08 10:14:08 +02:00
Mateu Aguiló Bosch
fb2c8c2ee4
[Schema][Server] Add top-level title to Tool (#288)
* feat(schema): add top-level title to Tool

Mirror PR #278's Prompt change for Tool:

- Add optional ?string $title to Mcp\Schema\Tool between $name
  and $inputSchema. fromArray() now reads $data['title'];
  jsonSerialize() emits 'title' right after 'name'. The
  @phpstan-type ToolData gains title?: string.
- Add ?string $title to the #[McpTool] attribute between $name
  and $description.
- Note ToolAnnotations::$title as deprecated-for-display in
  favor of Tool::$title per MCP spec 2025-06-18 (PHPDoc only,
  field retained for BC).

Phase 1 of plan 01--tool-title-spec-compliance. Later phases
update Discoverer, Builder::addTool(), ArrayLoader, and the
conformance fixture to match the new signature.

* feat(server): thread title through discovery

- Discoverer::processMethod() now propagates McpTool::$title
  into every discovered Tool via named arguments.
- Builder::addTool() accepts ?string $title between $name and
  $description. [BC Break] positional callers passing the old
  $description as 3rd positional argument must switch to named
  arguments, matching the precedent set for addPrompt() in
  0.5.0.
- tests/Conformance/server.php migrated to named-argument form
  for every addTool() call.

Phase 2 of plan 01--tool-title-spec-compliance.

* feat(loader): forward title in ArrayLoader

ArrayLoader now reads 'title' from each tool config entry and
passes it into the Tool constructor, completing the wiring
from Builder::addTool() through array-driven registration.

The $tools @param array-shape typedef gains title: ?string
between name and description.

Phase 3 of plan 01--tool-title-spec-compliance.

* test(schema): cover Tool.title end-to-end

- New tests/Unit/Schema/ToolTest.php: constructor round-trip,
  jsonSerialize key-order when title set vs null, fromArray
  tolerance.
- New tests/Unit/Capability/Discovery/DiscovererToolTitleTest
  plus a TitleFixture class verifying McpTool(title: ...)
  propagates through discovery into the registered Tool.
- New tests/Unit/Capability/Registry/Loader/ArrayLoader...
  test verifying array-driven title propagation.
- Extend McpToolTest for title named-arg and default-null.
- Extend BuilderTest with addTool(title: 't') propagation.
- Update tests/Unit/.../RegistryTest, CallToolHandlerTest,
  ListToolsHandlerTest and examples/.../server.php
  new Tool(...) call sites to pass title (named args) so
  they match the new constructor signature.
- CHANGELOG 0.6.0 section: feature bullet + [BC Break]
  note for addTool() signature, mirroring 0.5.0 Prompt
  entries.
- docs/server-builder.md: addTool() row and prose now
  list title? between name and description.

Phase 4 of plan 01--tool-title-spec-compliance. All 730
phpunit tests pass; remaining phpstan warnings are
pre-existing and unrelated to Tool.title.

* refactor(schema): make Tool.title non-BC-breaking

Move `title` to the end of the Tool, Builder::addTool, and McpTool
attribute constructors with a default of null. This preserves
positional-argument compatibility and removes the need for
`title: null` boilerplate at call sites. JSON serialization order
(name, title, inputSchema, ...) is unchanged.

Also tighten the new tests: parameterize ToolTest with data
providers, fold the Discoverer title fixture into the existing
DiscoverableToolHandler, and drop the reflection-heavy Builder
test that overlapped with ArrayLoaderToolTitleTest.

* fix: move title after name

---------

Co-authored-by: DDEV User <nobody@example.com>
2026-04-26 15:37:40 +02:00
Christopher Hertel
97979ec48d
CS update after release of v3.95 (#282) 2026-04-26 12:39:27 +02:00
Mateu Aguiló Bosch
73e4cc2456
style: apply php-cs-fixer and PHPStan fixes (#289)
* style: apply php-cs-fixer to test files

Addresses drift flagged by the default PHP-CS-Fixer config — mostly
adds missing `: void` return types on pre-existing test methods.

No behavioural changes.

* fix: resolve phpstan errors surfaced by qa

- Drop the obsolete `Method ::test*() has no return type specified`
  ignore rule — every test method now declares a return type, so
  the pattern never matches and phpstan reports it as unmatched.
- Remove `?? []` from two reads of `CallToolRequest::$arguments`;
  the property is typed `array` (non-nullable), so the coalesce is
  a no-op that phpstan flags.

---------

Co-authored-by: DDEV User <nobody@example.com>
2026-04-20 20:49:15 +02:00
Christopher Hertel
e8f5a8ba22
Extend elicitation enum schema compliance (#261) 2026-03-22 18:46:40 +01:00
Johannes Wachter
02e86377e4
Add elicitation support for server-to-client user input requests (#228)
- Add ElicitAction enum (accept/decline/cancel)
- Add schema definitions for elicitation fields (string, number, boolean, enum)
- Add ElicitationSchema wrapper for requestedSchema
- Add ElicitRequest and ElicitResult for elicitation/create method
- Update ClientCapabilities to support elicitation capability
- Add elicit() method to ClientGateway
- Store client capabilities in session during initialization
- Add comprehensive unit tests
- Add elicitation example server with book_restaurant, confirm_action, and collect_feedback tools
2026-02-15 23:34:19 +01:00
Sule-Balogun Olanrewaju
aafac3ddf5
Feat: Improve invalid resource template messages (#245) 2026-02-13 21:46:03 +01:00
Alexis Lefebvre
6e97aafb9c
feat: improve invalid resource messages (#242) 2026-02-13 18:02:43 +01:00
Christopher Hertel
5804ba9773
Add support for icons and website url (#141) 2025-11-09 14:53:36 +01:00
Christopher Hertel
fb3c1e65a6
Enable servers to send sampling messages to clients (#101) 2025-11-01 09:02:24 +01:00
Christopher Hertel
3ec4cf612a
Split testsuite for unit and inspector based tests (#82) 2025-09-25 22:27:41 +01:00