* [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.
16 KiB
16 KiB
Changelog
All notable changes to mcp/sdk will be documented in this file.
0.8.0
- Always emit
{}for empty tool schemas:Toolrecursively normalizes every empty sub-schema —properties,items,additionalProperties,$defs, combinators and the other draft-07 to 2020-12 schema keywords — in the constructor, for bothinputSchemaandoutputSchema, so an object position is never serialized as[]. - Prompt generators returning content as typed arrays (
['type' => 'text', ...]etc.) no longer lose the optional fields:annotationson every content type, and_metaand an explicitmimeTypeon embedded resource contents, now carry through to the resultingPromptMessageinstead of being silently dropped. A missing resourcemimeTypestill defaults totext/plain/application/octet-streamas before. - Add
annotationssupport toImageContent(constructor,fromArray(),fromFile(),fromString(),jsonSerialize()), matchingTextContentandAudioContent. - Add client-side
roots/listhandler (ListRootsRequestHandler+RootsCallbackInterface) andClient::sendRootsListChanged(), plus server-sideClientGateway::listRoots()/supportsRoots()andListRootsResult::fromArray(). - Add
ClientGateway::supportsSampling(), so a tool can check the client's advertised capabilities before issuing asampling/createMessagerequest instead of asking and catching the refusal. Matches the existingsupportsRoots()andsupportsElicitation(). - [BC Break] Gate
structuredContenton the negotiated protocol revision:ToolReference::extractStructuredContent()takes an optionalProtocolVersionand, for revisions predating SEP-2106 (2025-11-25and earlier, wherestructuredContentmust be a JSON object), returnsnullfor a tool result that is a PHP list or an object serializing to a JSON array. From2026-07-28on both are emitted as-is. Objects serializing to a scalar and arrays holdingContentinstances are never emitted, in any revision.CallToolHandlerresolves the revision from the request's_meta(modern era) or the session (handshake era) and falls back to the strictest rule; it logs a warning when a tool declares anoutputSchemabut returns a value that cannot be sent, and when a self-builtCallToolResultcarries astructuredContentthe revision does not allow (that one is passed through unchanged). Tools returning a list against an older client keep their JSON-encoded value incontent; they just no longer advertise an invalidstructuredContent. - Add
Mcp\Schema\Content\ResourceLinkfor the spec'sresource_linkcontent block (protocol revision 2025-06-18+), letting tool results and prompt messages reference a resource by URI/name without embedding its contents. Accepted anywhereresource(EmbeddedResource) content is (de)serialized:CallToolResult::fromArray(),PromptMessage::fromArray(), andPromptResultFormatter. - Negotiate the protocol revision during the
initializehandshake: the server echoes a revision it supports and counter-offersProtocolVersion::latestHandshake()otherwise (Builder::setProtocolVersion()pins it to exactly one), and the client fails the handshake on a counter-offer it cannot speak rather than continuing on an unagreed revision. AddsClient::getProtocolVersion(), the2026-07-28revision, and the era helpers onProtocolVersion— revisions from2026-07-28on have noinitialize, so they are excluded from negotiation and fromProtocolVersionMiddleware's default supported set. - Add sampling with tools support: sampling requests now accept tools and tool-choice preferences, messages support tool-use/tool-result content blocks and multiple content blocks, and clients can advertise the
sampling.contextandsampling.toolscapabilities. AddsClientGateway::supportsSamplingTools()/supportsSamplingContext()to check the sub-capabilities before sending, andCreateSamplingMessageRequest::validateToolFlow(), which asserts the spec's tool-flow rules across the whole message list — the client handler rejects a violating request with-32602instead of leaving it unanswered, and the gateway refuses to send one. - [BC Break]
SamplingMessage::$contentandCreateSamplingMessageResult::$contentmay now hold a list of content blocks instead of a single one, so code reading them directly must handle both. Use the newgetContentBlocks()on either class to always get a list. - [BC Break]
CreateSamplingMessageResultnow rejects any role other thanassistant, and rejects empty content, as the specification requires. - Close the schema gaps left in
2025-06-18and2025-11-25and add the non-sampling part of2026-07-28, all of it optional and defaulting to current behaviour. From2025-11-25: url-mode elicitation (ElicitationMode,ElicitRequest::forUrl(),ClientGateway::elicitUrl()andsupportsElicitationUrl()), whose result carries the user's action alone —ElicitResult::fromArray()takes the request's mode, requires content only in form mode and rejects it in url mode; theelicitation.form/elicitation.urlsub-capabilities, where a capability naming no mode declares form; andIcon::theme. From2025-06-18:Implementation::title, now settable throughClient\Builder::setClientInfo()andServer\Builder::setServerInfo(). From2026-07-28(SEP-2106):Tool::outputSchemaandCallToolResult::structuredContentaccept any JSON value —ToolReference::extractStructuredContent()keeps a scalar when the tool declared an outputSchema and the negotiated revision allows it, andCallToolHandlerwarns when a self-built result carries a value the revision does not permit — plus the revision's three error codes (-32020header mismatch,-32021missing required client capability,-32022unsupported protocol version), the last of whichProtocolVersionMiddlewarereturns with the supported set as structured data the client can retry from.
0.7.0
- Add client-side elicitation support:
ElicitationCallbackInterface,ElicitationRequestHandler, andElicitationExceptionlet clients respond to server elicitation requests. - Defer element loading to the first registry read: loaders now run at request time (first
has*/get*call) instead of eagerly atBuilder::build(), fixing empty registries under persistent runtimes (e.g. FrankenPHP worker mode) where a loader's data source is not ready at build time. AddsBuilder::setLazyLoading()(default on), a publicRegistry::load(), and an optionalLoaderInterfaceconstructor argument onRegistry. - [BC Break] Element loading is lazy by default: loader failures now surface on the first request rather than at
Builder::build(), andinitializeadvertises capabilities from the configured sources rather than the loaded registry. CallBuilder::setLazyLoading(false)to restore eager build-time loading. - Allow
[$instance, 'methodName']as an element handler inBuilder::addTool(),addResource(),addResourceTemplate(), andaddPrompt(). Unblocks handlers with constructor dependencies that the container-lessnew $className()fallback cannot build. - Always emit an
itemsschema for array tool parameters: untyped arrays getitems: {}and nullable typed arrays (e.g.string[]|null) keep their element type. Fixes strict clients rejecting tools with "array type must have items" (#151). - Harden JSON-RPC input parsing: single-message vs batch is now decided from the decoded JSON type (object → single, list array → batch) instead of the raw first byte. Scalars, empty payloads, and non-object batch elements are surfaced as
InvalidInputMessageExceptionentries instead of triggering warnings or aTypeError. - Add
maxBatchSize(default100) toMessageFactory— oversized JSON-RPC batches are rejected before any message is constructed, guarding against amplification. - Add
maxBodyBytes(default 4 MiB) toStreamableHttpTransport— POST bodies exceeding the cap are rejected with413. Unknown-size/chunked bodies are read incrementally and stopped at the cap so they cannot exhaust memory. - Reject malformed
Mcp-Session-Idheaders with a400response: a repeated header or a value that is not a valid UUID is now rejected up front instead of surfacing as an uncaughtUuid::fromString()error. - Extract RFC 9728 metadata serving into
ProtectedResourceMetadataHandler, a transport-neutral PSR-15RequestHandlerInterfacethat can be mounted directly as a Symfony/Laravel controller;ProtectedResourceMetadataMiddlewarenow delegates to it (no BC break).
0.6.0
- Add
Builder::add(Tool|ResourceDefinition|ResourceTemplate|Prompt $definition, ElementHandlerInterface $handler)for explicit registration of elements whose schema is only known at runtime. - Add handler interfaces
ToolHandlerInterface,ResourceHandlerInterface,ResourceTemplateHandlerInterface,PromptHandlerInterface, and theElementHandlerInterfacemarker. - [BC Break] Renamed
Mcp\Schema\ResourcetoMcp\Schema\ResourceDefinition. No alias. - [BC Break] Renamed
Mcp\Capability\Registry\Loader\ArrayLoadertoMcp\Capability\Registry\Loader\ReflectedElementLoader. - [BC Break] Bump default protocol version to
2025-11-25 - Add support for MCP Apps extension in schema and server
- Add
extensionstoServerCapabilitiesandClientCapabilitiesandBuilder::enableExtension() - Allow overriding the default name pattern for Discovery
- Add configurable session garbage collection (
gcProbability/gcDivisor) - Add optional
titlefield toResourceDefinitionandResourceTemplatefor MCP spec compliance - Add
ChainLoaderto compose multipleLoaderInterfaceimplementations via explicit ordering. - Add
RegistryInterface::unregisterTool(),unregisterResource(),unregisterResourceTemplate(),unregisterPrompt()— idempotent removals. - Add
RegistryInterface::hasTool(),hasResource(),hasResourceTemplate(),hasPrompt()— by-name existence checks. DiscoveryLoadernow refreshes only its own previously written entries; manual registrations (viaBuilder::addTool()etc. or runtime$registry->registerTool()calls) survive rediscovery, and a same-name manual registration takes precedence over discovery on collision.- [BC Break] Removed
ElementReference::$isManualpublic property and thebool $isManualparameter from all*Referenceconstructors. Origin tracking is no longer carried on the element; manual-over-discovered precedence is encoded by loader execution order. - [BC Break]
RegistryInterface::registerTool(),registerResource(),registerResourceTemplate(),registerPrompt()lost their trailingbool $isManual = falseparameter. Callers using positional arguments must drop the flag. - [BC Break] Removed
RegistryInterface::clear(),getDiscoveryState(),setDiscoveryState(). Rediscovery now goes throughDiscoveryLoader::load()directly. Registry::register*()semantics changed to plain last-write-wins (overwrites silently) and the methods now return the stored*Reference. The previous "discovered registration is ignored when a manual one already exists" precedence rule still applies, but is now enforced byDiscoveryLoadervia reference-identity tracking — and still emits a debug log when a discovery is skipped due to a conflicting registration.- Add optional
titleparameter toBuilder::addResource()andBuilder::addResourceTemplate()for MCP spec compliance - [BC Break]
Builder::addResource()signature changed —$titleparameter added between$nameand$description. Callers using positional arguments must switch to named arguments. - [BC Break]
Builder::addResourceTemplate()signature changed —$titleparameter added between$nameand$description. Callers using positional arguments must switch to named arguments. - Add
CorsMiddleware,DnsRebindingProtectionMiddleware, andProtocolVersionMiddlewareforStreamableHttpTransport, composed automatically as the default stack viaStreamableHttpTransport::defaultMiddleware() - [BC BREAK]
StreamableHttpTransportconstructor:$corsHeadersparameter removed; CORS is now configured viaCorsMiddleware. The$middlewareparameter is nullable —null(or omitted) installs the default stack;[]disables all defaults. DefaultAccess-Control-Allow-Originis no longer set (was*). - [BC Break]
ResourceDefinition::__construct()signature changed —$titleparameter added between$nameand$description. Callers using positional arguments must switch to named arguments. - [BC Break]
ResourceTemplate::__construct()signature changed —$titleparameter added between$nameand$description. Callers using positional arguments must switch to named arguments. - [BC Break]
McpResourceandMcpResourceTemplateattribute signatures changed —$titleparameter added between$nameand$description. Callers using positional arguments must switch to named arguments.
0.5.0
- Add built-in authentication middleware for HTTP transport using OAuth
- Add client component for building MCP clients
- Add
Builder::setReferenceHandler()to allow customReferenceHandlerInterfaceimplementations (e.g. authorization decorators) - Add elicitation enum schema types per SEP-1330:
TitledEnumSchemaDefinition,MultiSelectEnumSchemaDefinition,TitledMultiSelectEnumSchemaDefinition - [BC break] Make Symfony Finder component optional. Users would need to install
symfony/findernow themselves - Add
LenientOidcDiscoveryMetadataPolicyfor identity providers that omitcode_challenge_methods_supported(e.g. FusionAuth, Microsoft Entra ID) - Add OAuth 2.0 Dynamic Client Registration middleware (RFC 7591)
- Add optional
titlefield toPromptandMcpPromptfor MCP spec compliance - [BC Break]
Builder::addPrompt()signature changed —$titleparameter added between$nameand$description. Callers using positional arguments for$descriptionmust switch to named arguments. - Add optional
titlefield toToolandMcpToolfor MCP spec compliance - [BC Break]
Tool::__construct()signature changed —$titleparameter added between$nameand$inputSchema. Callers using positional arguments must switch to named arguments or passnullfor$title. - [BC Break]
McpToolattribute signature changed —$titleparameter added between$nameand$description. Callers using positional arguments for$descriptionmust switch to named arguments. - [BC Break]
Builder::addTool()signature changed —$titleparameter added between$nameand$description. Callers using positional arguments for$descriptionmust switch to named arguments.
0.4.0
- Rename
Mcp\Server\Session\Psr16StoreSessiontoMcp\Server\Session\Psr16SessionStore - Add missing handlers for resource subscribe/unsubscribe and persist subscriptions via session
- Introduce
SessionManagerto encapsulate session handling (replacesSessionFactory) and move garbage collection logic fromProtocol.
0.3.0
- Add output schema support to MCP tools
- Add validation of the input parameters given to a Tool.
- Rename
Mcp\Capability\Registry\ResourceReference::$schematoMcp\Capability\Registry\ResourceReference::$resource. - Introduce
SchemaGeneratorInterfaceandDiscovererInterfaceto allow custom schema generation and discovery implementations. - Remove
DocBlockParser::getSummary()method, useDocBlockParser::getDescription()instead.
0.2.2
- Throw exception when trying to inject parameter with the unsupported names
$_sessionor$_request. Throwableobjects are passed to log context instead of the exception message.
0.2.1
- Add
RunnerControlforStdioTransportto allow break out from continuously listening for new input. - Open range of supported Symfony versions to include v5.4
0.2.0
- Make
Protocolstateless by decouple if fromTransportInterface. RemovedProtocol::getTransport(). - Change signature of
Builder::addLoaders(...$loaders)toBuilder::addLoaders(iterable $loaders). - Removed
ClientAwareInterfacein favor of injecting aRequestContextwith argument injection. - The
ClientGatewaycannot be injected with argument injection anymore. UseRequestContextinstead. - Removed
ClientAwareTrait - Removed
Protocol::getTransport() - Added parameter for
TransportInterfacetoProtocol::processInput()
0.1.0
- First tagged release of package
- Support for implementing MCP server