From 847dc78828dffd0400729c413b6bd4e7e15c745c Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Tue, 10 Nov 2026 13:54:18 +0100 Subject: [PATCH 1/4] [Docs] Condense and group the 0.10.0 changelog --- CHANGELOG.md | 214 +++++++++++++++++++++++++-------------------------- 1 file changed, 106 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 072035e5..dd09457c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,19 @@ All notable changes to `mcp/sdk` will be documented in this file. 0.10.0 ------ -* [BC Break] Require PHP 8.2 or higher, as PHP 8.1 reached its end of life. +### TL;DR + +* The SDK is split into the `Mcp\Schema`, `Mcp\Client` and `Mcp\Server` namespaces, ahead of separate packages: most imports change +* PHP 8.2 or higher is required, and JSON-RPC batches are gone +* The client authorizes with OAuth: authorization code, client credentials, `private_key_jwt` and cross-app access +* Handshake-era servers over HTTP run without sessions unless `Builder::setSession()` is called, and sessions belong to the principal that opened them +* Request handlers run under an event loop (Revolt, ReactPHP) and stream their responses +* Tool arguments keep their JSON types, and arguments that don't fit are answered with `-32602` +* stdio works on Windows, in the server and the client ### Namespaces -* [BC Break] Split the root namespace into `Mcp\Schema`, `Mcp\Client` and `Mcp\Server` to prepare package slicing. `Mcp\Schema` depends on nothing, `Mcp\Client` and `Mcp\Server` depend on `Mcp\Schema` only — enforced by Deptrac (`make deptrac`). +* [BC Break] Split the root namespace into `Mcp\Schema`, `Mcp\Client` and `Mcp\Server` to prepare slicing them into separate packages; update your imports. `Mcp\Schema` depends on nothing, `Mcp\Client` and `Mcp\Server` depend on `Mcp\Schema` only — enforced by Deptrac (`make deptrac`). * `Mcp\Server` => `Mcp\Server\Server` * `Mcp\Client` => `Mcp\Client\Client` * `Mcp\Capability\*` => `Mcp\Server\Capability\*` (`Attribute`, `Completion`, `Discovery`, `Formatter`, `Logger`, `Registry`, `Tool`, plus `Registry` and `RegistryInterface`) @@ -24,123 +32,113 @@ All notable changes to `mcp/sdk` will be documented in this file. * `Mcp\Exception\{BadMethodCallException,ClientException,ConfigurationException,ContainerException,InvalidCursorException,MissingRequiredClientCapabilityException,NotFoundExceptionInterface,PromptGetException,PromptNotFoundException,RegistryException,RequestStateException,ResourceNotFoundException,ResourceReadException,ServiceNotFoundException,SessionLockException,SessionLockTimeoutException,SessionStoreException,ToolCallException,ToolNotFoundException}` => `Mcp\Server\Exception\*` * `Mcp\Exception\HandlerNotFoundException` is removed; it was unused. * `Mcp\Server\Exception\ExceptionInterface` and `Mcp\Client\Exception\ExceptionInterface` extend `Mcp\Schema\Exception\ExceptionInterface`, so catching the latter still catches any SDK exception. - * The named exceptions now extend their package's `RuntimeException`/`InvalidArgumentException` instead of the global ones, so `catch (Mcp\Server\Exception\RuntimeException)` reaches `ToolCallException`, `PromptGetException` and friends. - + * The named exceptions extend their package's `RuntimeException`/`InvalidArgumentException` instead of the global ones, so `catch (Mcp\Server\Exception\RuntimeException)` reaches `ToolCallException`, `PromptGetException` and friends. * [BC Break] Move `ExtensionInterface::getRequestHandlers()` to the new `Mcp\Server\Extension\ServerExtensionInterface`, so `Mcp\Schema` does not reference `Mcp\Server`. `AbstractExtension` no longer defines it. An extension that serves its methods must implement the new interface, or `Builder::enableExtension()` does not register its handlers. +### General + +* [BC Break] Require PHP 8.2 or higher, as PHP 8.1 reached its end of life. +* [BC Break] Remove JSON-RPC batch support, as MCP dropped batching in `2025-06-18`. The server refuses a JSON array with `-32600`, and HTTP status `400`, on every revision, including `2025-03-26`, which requires batches and which the server still negotiates. The client drops one with a warning. +* [BC Break] Refuse OAuth URLs, in the server and the client, whose host is `0.0.0.0`, `::`, an IPv4 address within IPv6 or an IPv4 address in a non-standard form (e.g. `127.1`), or that hold percent-encoding or non-ASCII characters (give an internationalized name in punycode). In the server, this applies to the issuer, the `jwks_uri` and the resource. +* [BC Break] Require `phpdocumentor/reflection-docblock` `^6.0`, with `phpdocumentor/type-resolver` `^2.0` and `phpstan/phpdoc-parser` `^2.0`: older versions cannot parse types like `non-empty-array`, and the SDK silently ignored such a `@param`. Require `psr/container` `^1.1`, as `Container` is not compatible with `1.0`. +* `ext-fileinfo` is optional: without it, creating content from a file without a MIME type throws a `LogicException`. + ### Server -* [BC Break] Refuse an OAuth URL whose host is `0.0.0.0`, `::`, an IPv4 address within IPv6 or an IPv4 address in a non-standard form (e.g. `127.1`), or that holds percent-encoding or non-ASCII characters (an internationalized name must be given in punycode), for the issuer, the `jwks_uri` and the resource. -* [BC Break] Return HTTP `503` with `Retry-After` and without a Bearer challenge when `JwtTokenValidator` cannot fetch the signing keys or reaches the limit of the new `RateLimitedJwksClient`, which replaces the `rateLimit` of `CachedKeySet`. -* `CorsMiddleware` exposes `Retry-After` by default. -* [BC Break] Remove JSON-RPC batch support: a JSON array is refused with `-32600`, and HTTP status `400` over HTTP, for every protocol revision. This includes `2025-03-26`, which requires batches and which the server still negotiates. Batching was removed from MCP in `2025-06-18`. -* [BC Break] `ScopePolicy::requiredFor()` takes one decoded message, renames its parameter from `$payload` to `$message` and returns only the default scopes for a list. -* [BC Break] `AuthorizationMiddleware` answers a body over its `maxBodyBytes` with `413` and a JSON-RPC error, like the transports, instead of `400` with a `WWW-Authenticate` challenge. -* Add a `$logger` argument to `AuthorizationMiddleware`, which gets a warning for a body over its `maxBodyBytes`. -* [BC Break] Modern tools with an `outputSchema` send and validate a null result as structured content, with JSON text `null` alongside it. A tool whose schema does not allow null, including a `void` tool as PHP hands back null for it, now returns a tool error instead of the text `(null)` and a warning. Schema-less null results and handshake-era automatic wrapping still omit structured content. -* [BC Break] `ToolReference::extractStructuredContent()` returns `StructuredContent::Absent` when there is no structured content to send, as null is now a value it can return. -* [BC Break] Require `phpdocumentor/reflection-docblock` `^6.0`, and with it `phpdocumentor/type-resolver` `^2.0` and `phpstan/phpdoc-parser` `^2.0`. Older versions cannot parse types like `non-empty-array`, and the SDK silently ignores such a `@param`. -* Require `psr/container` `^1.1`, as `Container` is not compatible with `1.0`. -* [BC Break] `DocBlockParser::parseDocBlock()` no longer accepts `null`; pass `ReflectionMethod::getDocComment()` as is -* [BC Break] Fix a `DELETE` of an unknown or ended session being answered `200` instead of the `404` the spec requires: `Protocol::destroySession()` and `BaseTransport::handleSessionEnd()` return `false` for such a session, as does the listener of `TransportInterface::onSessionEnd()`. A listener that returns nothing keeps the `200`. -* [BC Break] `Builder::build()` and `Builder::buildStateless()` throw a `RuntimeException` when `setDiscovery()` is used without `symfony/finder` and without a custom discoverer, instead of logging a warning and discovering nothing -* [BC Break] Serve handshake-era clients over Streamable HTTP without sessions unless `Builder::setSession()` is called, instead of falling back to an `InMemorySessionStore` that PHP-FPM and `php -S` lose between requests: `initialize` is answered without `Mcp-Session-Id`, every later request is served on an `EphemeralSessionInterface` that is never stored, and `GET` and `DELETE` are answered with `405`, so a horizontally scaled server needs no shared session store or lock for them. Without a session, requests to the client (elicitation, sampling, roots), `Protocol::sendRequest()`, `Protocol::sendNotification()` and saving the session throw a `SessionRequiredException`, whose message reaches the client as `-32603` when a handler does not catch it. Input-required results, unless `withoutInputRequiredShim()` is used, and `resources/subscribe`, `resources/unsubscribe` and `logging/setLevel` are answered with `-32603`. `initialize` offers neither resource subscriptions nor logging, log messages are dropped and the client capability checks of `ClientGateway` return `false`. The protocol version comes from the `MCP-Protocol-Version` header. Call `setSession()` to keep sessions, e.g. `setSession(new InMemorySessionStore())` in a long-running process. Stdio and the `2026-07-28` era are not affected. -* Fix tools with an array shape parameter whose keys are nullable or unions (e.g. `array{name: string, nickname: string|null}`) rejecting valid arguments with `-32602`: a `null` inside the shape no longer turns the parameter into `array|null`, and such keys get their own types instead of `object`, including lists and nested shapes (`string[]|null`, `array{x: int}|null`). -* Fix tools whose DocBlock uses a PHPStan/Psalm pseudo-type or a literal type (`non-empty-string`, `positive-int`, `'asc'|'desc'`, `array`, ...) rejecting valid arguments with `-32602`: parameters and array shape keys now get the JSON Schema type of their base type instead of `object`. -* Fix `array` and other generics with a union element getting `items` of type `object`. A `null` inside a generic no longer makes the parameter nullable, and neither does a DocBlock class whose name contains `null`. -* Fix `InMemorySessionStore::exists()` reporting a session past its TTL as existing, while `read()` already treated it as gone. -* Fix `Psr16NotificationBus` subscribers missing the notifications published after the bus stayed quiet for longer than its TTL: the sequence counter no longer expires, and a subscriber whose cursor is ahead of a lost counter reads what the bus holds. -* Fix `Session::save()` wiping the other keys of a session that the store failed to read, e.g. when `Psr16SessionStore` hits a cache error: it now returns `false` instead of writing the changed keys alone. -* Fix `EnumCompletionProvider` and `ListCompletionProvider` offering all values for the prefix `"0"` instead of only the values that start with it. -* Fix tools, prompts and resources sending `content`, `messages` and `contents` as a JSON object instead of an array when the handler returned a non-list array, e.g. the result of `array_filter()`. Named keys in nested results of mixed arrays no longer overwrite each other. -* Fix `SecureUrl` rejecting plain http on hosts under the reserved `.localhost` TLD (RFC 6761) and on IPv4 loopback addresses other than `127.0.0.1`, so `ProtectedResourceMetadata`, `OidcDiscovery` and `JwtTokenValidator::fromIssuer()` failed for local development setups like `http://shop.localhost:8080`. Plain http is now accepted for `localhost`, any `*.localhost`, `127.0.0.0/8` and `::1`; every other host still requires https. -* Fix `SecureUrl` accepting a URL with a backslash, whitespace or control character, so `http://evil.example\@localhost` passed as loopback although browsers read `evil.example` as the host. -* Tool, prompt and resource results and `TextContent` encode arrays and objects as compact JSON instead of pretty-printed JSON. -* Fix the result of a retried `resources/read` leaving out the required `ttlMs` and `cacheScope`, which the TypeScript SDK's client rejects, or keeping the hints its handler set: it now always carries `ttlMs: 0` and `cacheScope: private`. -* Fix tool name validation not following the spec's rule (1 to 128 characters of `A-Z`, `a-z`, `0-9`, `_`, `-` and `.`): names of 65 to 128 characters no longer log a warning, while names with a `/` or a trailing newline now do. -* [BC Break] `ReferenceHandler` resolves a string handler that is not an invokable class from the container, so a loader can register a service id as handler. The container goes first, so a function handler no longer runs if the container has an entry of the same name. -* Add `Psr6SessionStore` and `Psr6NotificationBus`, which work like `Psr16SessionStore` and `Psr16NotificationBus` over a PSR-6 cache pool. `Builder::setDiscovery()` also accepts a PSR-6 pool as `$cache`. -* Fix a nullable tool parameter with a default (e.g. `?string $category = 'books'`) receiving the default when the client explicitly sends `null`: `ReferenceHandler` now passes the `null` through. -* Fix an `int` parameter of a tool, prompt or resource receiving a wrong value: a number beyond the integer range wrapped around, also past a `minimum` or `maximum` in the schema, a numeric string beyond it became `PHP_INT_MAX` or `PHP_INT_MIN`, and `"--5"` became `0`. `ReferenceHandler` now refuses these values. -* [BC Break] Answer an argument that cannot be cast to its parameter type with `-32602` instead of `-32603` for tools, prompts and resources. The message names the parameter and no longer contains the enum class. A `string` parameter refuses a list or an object instead of receiving `"Array"` or failing, and an int-backed enum refuses a value with a fraction instead of truncating it. -* Fix `CachedDiscoverer` returning the elements of another discovery that only differs in its name patterns, and `CachedDiscoverer::clearCache()` wiping the whole cache: it now only invalidates its own entries, which stay in the cache until they expire or are evicted, as do entries written by earlier versions. -* Fix a `subscriptions/listen` stream that can carry nothing (the server announces none of the requested types, or the client asks for none) being held open for the full subscription lifetime: it is acknowledged with an empty `notifications` filter and closed gracefully right away, instead of pinning a PHP-FPM worker. Adds `NotificationFilter::isEmpty()`. -* [BC Break] Answer `server/discover` with `DiscoverHandler`, which a handler added through `Builder::addRequestHandler()` can replace, and dispatch `RequestEvent`, `ResponseEvent` and `ErrorEvent` for it. A `StatelessProtocol` built without the `Builder` needs `DiscoverHandler` and a `MessageFactory` with `DiscoverRequest` to answer it. -* Add a `binder` argument to `Builder::setRequestState()` binding the modern era's `requestState` to the request it was minted for, as the 2026-07-28 specification recommends: the binder's value is covered by the state's MAC, and a state echoed on a request that binds to another value is refused like a forged one. `OriginatingRequestBinder` binds the authenticated principal, the method, the tool, prompt or resource, and a digest of the arguments; a token without a subject fails closed, and a `subject` resolver identifies users by another claim. `RequestStateBinderInterface` takes custom bindings. Without a binder, nothing changes. -* Rotate the `requestState` signing key without failing the calls in flight: `Builder::setRequestState()` and `RequestStateCodec` take a list of keys, the first signing and every key verifying. The wire format is unchanged, and a single key works as before. -* Log the name of a called tool at info level, so a server logging at INFO shows which tool is called. Its arguments stay at debug level, logged only once the tool is found and its input is valid. -* Add `Builder::setFiberContext()` and `Mcp\Server\Fiber\FiberContextInterface` to carry execution context, e.g. the active OpenTelemetry context, into handler fibers in both protocol eras. -* Event loops - * Add `Builder::setFiberScheduler()` and `Mcp\Server\Fiber\FiberSchedulerInterface`: under an event loop — Revolt (AMPHP), ReactPHP's async — a request handler awaiting I/O suspends its fiber for the loop, and the server now waits through the scheduler until the handler suspends for the server again or returns. The scheduler must wake the server on a later turn of the loop; the guide shows one for Revolt and one for ReactPHP, and the limits under an event loop. - * Add `CallbackStream::chunks()`, which hands the SSE text of a streamed response, in both protocol eras, to a server that writes the body itself, e.g. `amphp/http-server` or `react/http`. On an empty chunk, an idle turn, the server must wait on its loop before it asks for the next one; dropping the generator ends the stream. While a handshake-era handler waits for an answer from the client, the server keeps serving its loop and gets that answer from another request. The Revolt and ReactPHP examples stream these responses instead of answering `501`. +* Sessions + * [BC Break] Serve handshake-era clients over Streamable HTTP without sessions unless `Builder::setSession()` is called, instead of falling back to an `InMemorySessionStore` that PHP-FPM and `php -S` lose between requests. `initialize` is answered without `Mcp-Session-Id`, later requests run on an `EphemeralSessionInterface` that is never stored, and `GET` and `DELETE` get `405`, so a scaled-out server needs no shared session store or lock. The protocol version comes from the `MCP-Protocol-Version` header. Stdio and the `2026-07-28` era are not affected. Without a session, requests to the client (elicitation, sampling, roots), `Protocol::sendRequest()`, `Protocol::sendNotification()` and saving the session throw a `SessionRequiredException`, answered with `-32603` if a handler does not catch it. Input-required results (unless `withoutInputRequiredShim()` is used), `resources/subscribe`, `resources/unsubscribe` and `logging/setLevel` get `-32603`, `initialize` offers neither resource subscriptions nor logging, log messages are dropped and the client capability checks of `ClientGateway` return `false`. Keep sessions with e.g. `setSession(new InMemorySessionStore())` in a long-running process. + * [BC Break] Bind handshake-era sessions to the principal that opened them: `AccessToken::getPrincipal()`, or the `StreamableHttpTransport::PRINCIPAL_ATTRIBUTE` request attribute when set. A request of another principal, or to a session opened before the upgrade, gets `404`. `Protocol::processInput()`, the transports' `handleMessage()`, `handleSessionEnd()`, `handlePostRequest()` and `handleDeleteRequest()`, and the `onMessage()` and `onSessionEnd()` listeners take a `?string $principal` as last argument. + * [BC Break] Add `SessionInterface::isUnreadable()`, which a custom session implements. A session the store holds without attributes, because a handler cleared it or its data is corrupt, gets `404` so the client opens a new one; only a session the store fails to return gets `503`. `FileSessionStore::exists()` no longer reports a zero-byte file. + * [BC Break] Answer a `DELETE` of an unknown or ended session with `404` instead of `200`, as the spec requires: `Protocol::destroySession()`, `BaseTransport::handleSessionEnd()` and the listener of `TransportInterface::onSessionEnd()` return `false` for it. A listener that returns nothing keeps the `200`. + * [BC Break] Answer malformed JSON and an `initialize` sent with a session id with HTTP `400`, and a failure before the session is checked with `500`, instead of `200`. Such an answer no longer carries, or removes, the messages queued for the session in `Mcp-Session-Id`. + * Add `Psr6SessionStore` and `Psr6NotificationBus`, the PSR-6 counterparts of `Psr16SessionStore` and `Psr16NotificationBus`. `Builder::setDiscovery()` also accepts a PSR-6 pool as `$cache`. + * Fix `Session::save()` wiping the other keys of a session the store failed to read, e.g. on a `Psr16SessionStore` cache error: it returns `false` instead. + * Fix `FileSessionStore` serving a partial session on Windows when a write could not replace the session file: it no longer falls back to `copy()`, retries the move for up to a second while another process holds the file, and `write()` returns `false` if the move fails. + * Fix `InMemorySessionStore::exists()` reporting a session past its TTL as existing. + * Fix `Psr16NotificationBus` subscribers missing notifications published after the bus stayed quiet for longer than its TTL. +* Authorization and HTTP security + * [BC Break] Answer `503` with `Retry-After` and without a Bearer challenge when `JwtTokenValidator` cannot fetch the signing keys or reaches the limit of the new `RateLimitedJwksClient`, which replaces the `rateLimit` of `CachedKeySet`. `CorsMiddleware` exposes `Retry-After` by default. + * [BC Break] `AuthorizationMiddleware` answers a body over its `maxBodyBytes` with `413` and a JSON-RPC error, like the transports, instead of `400` with a `WWW-Authenticate` challenge, and logs a warning to its new `$logger`. + * [BC Break] `ScopePolicy::requiredFor()` takes one decoded message, renames its parameter from `$payload` to `$message` and returns only the default scopes for a list. + * Fix `JwtTokenValidator` throwing a `TypeError`, or answering `503`, on a token naming a JWKS key whose type or curve `firebase/php-jwt` does not support (e.g. `AKP`, `P-521` since its v7.1, X25519): the token is rejected with `401`. + * Fix `SecureUrl` rejecting plain http on `*.localhost` hosts (RFC 6761) and IPv4 loopback addresses other than `127.0.0.1`, which broke local setups like `http://shop.localhost:8080`. Plain http is accepted for `localhost`, `*.localhost`, `127.0.0.0/8` and `::1`; every other host still requires https. + * Fix `SecureUrl` accepting a URL with a backslash, whitespace or control character, so `http://evil.example\@localhost` passed as loopback although browsers read `evil.example` as the host. +* Tools, arguments and results + * [BC Break] Keep the JSON types of `tools/call` arguments, so validation sees what the client sent: `{}` and `{"0":"x"}` are objects, `[]` and `["x"]` are arrays. Nested objects in `CallToolRequest::$arguments` and in the arguments of `ToolHandlerInterface::execute()` are `\stdClass`, as they are for parameters without a type or typed `mixed`, `object` or `\stdClass`; parameters typed `array` still receive arrays at every level. `array` parameters generate `{"type": "object"}`, and `array` parameters accept a JSON array or object. + * [BC Break] Reject `tools/call` whose `arguments` is not a JSON object, including `[]`, with `-32602`, and one whose message has a member name starting with a NUL byte with `-32600`. + * [BC Break] Answer params that do not fit a known method (e.g. `tools/call` without `name`) with `-32602` instead of `-32600`, as JSON-RPC requires. `Request::fromArray()` throws the new `InvalidParamsException`, a subclass of `InvalidArgumentException`, and `InvalidInputMessageException::isInvalidParams()` tells the protocols which code to send. + * [BC Break] Answer an argument that cannot be cast to its parameter type with `-32602` instead of `-32603`, for tools, prompts and resources. The message names the parameter. A `string` parameter refuses a list or an object instead of receiving `"Array"`, and an int-backed enum refuses a value with a fraction instead of truncating it. A missing required argument of a prompt, or of a tool that gets past schema validation, gets `-32602` too. + * [BC Break] Modern tools with an `outputSchema` send and validate a null result as structured content, with JSON text `null` alongside it. A tool whose schema does not allow null, including a `void` tool, returns a tool error instead of the text `(null)`. `ToolReference::extractStructuredContent()` returns `StructuredContent::Absent` when there is nothing to send. + * [BC Break] `ReferenceHandler` resolves a string handler that is not an invokable class from the container by service id. The container goes first, so a function handler no longer runs if the container has an entry of that name. + * Fix an `int` parameter receiving a wrong value: a number beyond the integer range wrapped around, also past the schema's `minimum` or `maximum`, a numeric string beyond it became `PHP_INT_MAX` or `PHP_INT_MIN`, and `"--5"` became `0`. They are refused now. + * Fix a nullable parameter with a default (e.g. `?string $category = 'books'`) receiving the default when the client sends `null`. + * Fix DocBlock types producing wrong schemas, so valid arguments were rejected with `-32602`: PHPStan/Psalm pseudo-types and literal types (`non-empty-string`, `positive-int`, `'asc'|'desc'`, `array`) get the JSON Schema type of their base type, nullable and union keys of an array shape (`array{nickname: string|null}`) and union elements of a generic (`array`) get their own types instead of `object`, and a `null` inside them, or in a class name, no longer makes the parameter nullable. + * Fix tool name validation to follow the spec (1 to 128 characters of `A-Z`, `a-z`, `0-9`, `_`, `-` and `.`): names of 65 to 128 characters no longer log a warning, names with a `/` or a trailing newline do. + * Fix `content`, `messages` and `contents` being sent as a JSON object when the handler returned a non-list array, e.g. from `array_filter()`, and named keys in nested mixed arrays overwriting each other. + * Fix a scalar `structuredContent` on `2026-07-28` going out as raw text in `content`: `'sunny'` is sent as the JSON `"sunny"`, as SEP-2106 requires. + * Fix a resource whose text is not valid UTF-8 failing `resources/read`: `ResourceResultFormatter` sends it as a blob with the same MIME type. + * Encode arrays and objects in results and `TextContent` as compact JSON instead of pretty-printed JSON. + * Fix `EnumCompletionProvider` and `ListCompletionProvider` offering all values for the prefix `"0"`. + * Log the name of a called tool at info level; its arguments stay at debug level. +* Discovery and registration + * [BC Break] `Builder::build()` and `Builder::buildStateless()` throw a `RuntimeException` when `setDiscovery()` is used without `symfony/finder` and without a custom discoverer, instead of logging a warning and discovering nothing. + * [BC Break] `DocBlockParser::parseDocBlock()` no longer accepts `null`; pass `ReflectionMethod::getDocComment()` as is. + * Fix `CachedDiscoverer` returning the elements of a discovery that only differs in its name patterns, and `clearCache()` wiping the whole cache instead of its own entries. + * Fix discovery skipping a class whose file declares an enum before it, once that file has been loaded. + * Fix a manually registered element with a bad handler answering every list and call with `-32602` and the internal message: the client gets `-32603` and the `ConfigurationException` is logged. +* Protocol `2026-07-28` + * [BC Break] Answer `server/discover` with `DiscoverHandler`, which `Builder::addRequestHandler()` can replace, and dispatch `RequestEvent`, `ResponseEvent` and `ErrorEvent` for it. A `StatelessProtocol` built without the `Builder` needs `DiscoverHandler` and a `MessageFactory` with `DiscoverRequest`. + * Add a `binder` argument to `Builder::setRequestState()` that binds `requestState` to the request it was minted for, as the spec recommends: a state echoed on a request that binds to another value is refused like a forged one. `OriginatingRequestBinder` binds the principal, the method, the tool, prompt or resource and a digest of the arguments; A token without a subject fails closed, and `RequestStateBinderInterface` takes custom bindings. Without a binder, nothing changes. + * Rotate the `requestState` signing key without failing calls in flight: `Builder::setRequestState()` and `RequestStateCodec` take a list of keys, the first signing and all of them verifying. The wire format is unchanged. + * Fix a `subscriptions/listen` stream that can carry nothing, or that asks for resource subscriptions without a notification bus, being held open for its whole lifetime and pinning a PHP-FPM worker: it is acknowledged with an empty filter and closed right away. `server/discover` no longer announces `resources.subscribe` without a bus, unless `setCapabilities()` sets it. Adds `NotificationFilter::isEmpty()`. + * Fix a retried `resources/read` leaving out the required `ttlMs` and `cacheScope`, which the TypeScript client rejects: it always carries `ttlMs: 0` and `cacheScope: private`. A `ReadResourceResult` no longer sends its own hints to handshake-era clients. + * Fix `StandardHeaderValidator` accepting an `Mcp-Param-*` header that does not match the body: a fraction that rounds to an integer argument, object and array arguments, and integers outside ±(2^53-1) are refused with `-32020`. + * Fix a request with `"id": null` being taken for a notification and left unanswered: it is refused with `-32600`. + * Fix a result that cannot be encoded as JSON escaping as an uncaught `JsonException` or ending a stream without an answer: the client gets `-32603`, with HTTP status `500` or as the last frame of a stream, and the failure is logged and dispatched as an `ErrorEvent`. + * Fix messages with a `jsonrpc` other than `"2.0"` being served: a `2026-07-28` request that skips `MessageFactory`, like `subscriptions/listen`, is refused with `-32600`, and `Notification::fromArray()` throws, as `Request::fromArray()` already did. + * [BC Break] Remove `Rev2025Codec`, which nothing used. +* Event loops and fibers + * Add `Builder::setFiberScheduler()` and `FiberSchedulerInterface`: under an event loop (Revolt, ReactPHP's async), a handler awaiting I/O suspends its fiber for the loop, and the server waits through the scheduler until the handler suspends for the server or returns. The scheduler must wake the server on a later turn of the loop; the guide shows one for Revolt and one for ReactPHP. + * Add `CallbackStream::chunks()`, which hands the SSE text of a streamed response, in both protocol eras, to a server that writes the body itself (e.g. `amphp/http-server`, `react/http`). On an empty chunk, the server waits on its loop before it asks for the next one, and dropping the generator ends the stream. While a handshake-era handler waits for the client, the server keeps serving its loop. The Revolt and ReactPHP examples stream these responses instead of answering `501`. + * Add `Builder::setFiberContext()` and `FiberContextInterface` to carry execution context, e.g. the active OpenTelemetry context, into handler fibers. * [BC Break] A `subscriptions/listen` stream read from `StatelessProtocol::handle()` no longer waits between polls or checks `connection_aborted()`: its consumer paces it, as `StatelessResponder` does. - * [BC Break] `StreamableHttpTransport` no longer has the protected `flushOutgoingMessages()` and `handleFiberTermination()`: its streamed response produces its frames through `CallbackStream::chunks()`. - * Fix the server resuming a request handler's fiber that the runtime had suspended: the handler's await returned early or threw (`Must call resume() or throw() before calling suspend() again`), and the runtime later resumed a fiber that had moved on — under Revolt, terminating the event loop. Without a scheduler, such a handler now fails the request with a `LogicException` naming `Builder::setFiberScheduler()`. `Protocol::__construct()` and `StatelessProtocol::__construct()` take the scheduler as optional last argument. - * What a handler throws while the runtime drives its fiber reaches the server, as it does when the server drives it, never the runtime. What it throws while its fiber is discarded, e.g. in a `finally` after it asked the client for input, is logged. - * [BC Break] A handler calling `\Fiber::suspend()` itself with anything but a `NotificationSuspension` or `RequestSuspension` fails the request without a scheduler, also when no event loop is involved. It used to be resumed with `null`. - * Fix a request failing with `503`, and a resumed request losing its session writes, while another request of the session holds the session lock across an await: both wait for the lock on the event loop. - * Fix a handler still running after its request was answered, e.g. without a fiber scheduler: it no longer fires `ResponseEvent` or `ErrorEvent`, what it throws is logged, and calling the `ClientGateway` throws. -* [BC Break] Bind handshake-era sessions to the principal that opened them: `AccessToken::getPrincipal()`, or the `StreamableHttpTransport::PRINCIPAL_ATTRIBUTE` request attribute when set. Requests of another principal, and to sessions opened before the upgrade, are answered with `404`, and to a session the store cannot read with `503`. `Protocol::processInput()`, the transports' `handleMessage()`, `handleSessionEnd()`, `handlePostRequest()` and `handleDeleteRequest()`, and the `onMessage()` and `onSessionEnd()` listeners take a `?string $principal` as last argument. -* [BC Break] Answer malformed JSON and an `initialize` sent with a session id with HTTP `400`, and a failure before the session is checked with `500`, instead of `200`. Such an answer no longer carries, or removes, the messages queued for the session in `Mcp-Session-Id`. -* [BC Break] Keep the JSON types of `tools/call` arguments, so validation sees what the client sent: `{}` and `{"0":"x"}` are objects, `[]` and `["x"]` are arrays. Nested JSON objects in `CallToolRequest::$arguments` and in the arguments of `ToolHandlerInterface::execute()` are `\stdClass`. -* [BC Break] Tool parameters without a type, or typed `mixed`, receive nested JSON objects as `\stdClass`. Parameters typed `array` still receive arrays at every level. Parameters typed `object` or `\stdClass`, which always failed with a `TypeError`, receive `\stdClass`. -* [BC Break] Reject `tools/call` when `arguments` is not a JSON object with `-32602`, including `"arguments": []`. -* [BC Break] Reject `tools/call` with `-32600` when its message has a member name starting with a NUL byte, as its arguments cannot keep their JSON types then. -* [BC Break] `array` parameters generate `{"type": "object"}` instead of `{"type": "array"}`, and `array` parameters accept a JSON array or a JSON object. -* [BC Break] Add `SessionInterface::isUnreadable()`, which a custom session implements. A session the store holds without attributes, because a handler cleared it or its data is corrupt, is answered with `404` so the client opens a new one; only a session the store fails to return is answered with `503`. `FileSessionStore::exists()` no longer reports a zero-byte file. -* Fix a resource whose text is not valid UTF-8 (e.g. a Latin-1 `.txt` file) failing `resources/read` with an internal error or an uncaught `JsonException`: `ResourceResultFormatter` now sends it as a blob with the same MIME type. -* [BC Break] Answer a request of a known method whose params do not fit it (e.g. `tools/call` without `name`) with `-32602` (Invalid params) instead of `-32600` (Invalid request), as JSON-RPC requires. `Request::fromArray()` throws the new `InvalidParamsException`, a subclass of `InvalidArgumentException`, and `InvalidInputMessageException::isInvalidParams()` tells the protocols which code to send. A malformed envelope and an unknown method keep their codes. -* Fix a modern-era result that cannot be encoded as JSON, e.g. tool or resource text that is not valid UTF-8, escaping as an uncaught `JsonException` or ending a stream without an answer: the client gets `-32603`, with HTTP status `500` or as the last frame of a stream, and the failure is logged and dispatched as an `ErrorEvent`. Outside a stream, an error message that is not valid UTF-8 has the invalid bytes replaced. -* Fix `FileSessionStore` serving a partial session on Windows when a write could not replace the session file: it no longer falls back to a `copy()` that readers can see half done, but on Windows retries the move for up to a second while a reader or another writer holds the file, and `write()` returns `false` if the move fails. -* Fix a stdio server on Windows ending by itself after about 13.5 hours without input, when `fgets()` on the idle pipe reported a false EOF: `StdioTransport` now reads only once `stream_select()` reports input, which also keeps the run loop going while idle. Requires PHP 8.5, as `stream_select()` on Windows reports an idle pipe as readable before that. -* Fix `StdioTransport` throwing a `ValueError` on an input that cannot be selected, like `php://memory`. -* Fix a stdio server never answering a message that reaches it in several reads, such as a tool call with large arguments: `StdioTransport` handled each read as a message and answered the parts with `-32700`. It now waits for the newline. -* Fix `StandardHeaderValidator` accepting an `Mcp-Param-*` header that does not match the body: a fraction that rounds to an integer argument and object or array arguments are refused with `-32020`, and so is a mirrored argument outside the safe integer range of ±(2^53-1). -* Fix `JwtTokenValidator` throwing a `TypeError` on a token naming a JWKS key whose type or curve `firebase/php-jwt` does not support, e.g. `AKP`, or `P-521` since its v7.1, and answering `503` when `firebase/php-jwt` throws for such a curve instead, e.g. X25519. The token is rejected with `401` now. -* Fix a `2026-07-28` request with `"id": null` being taken for a notification and left unanswered: it is refused with `-32600`, as MCP forbids a null id. -* Fix messages with a `jsonrpc` other than `"2.0"` being served: a `2026-07-28` request that does not go through `MessageFactory`, like `subscriptions/listen`, is refused with `-32600`, and `Notification::fromArray()` throws for such a notification, as `Request::fromArray()` already did. -* Fix a stdio server answering a notification it refuses, e.g. one sent before `initialize`, although a notification must never be answered: the refusal is only logged now. -* Fix a scalar `structuredContent` on `2026-07-28` going out as raw text in `content`: a tool returning `'sunny'` with an `outputSchema` now sends the serialized JSON `"sunny"`, as SEP-2106 requires. -* Fix a missing required argument of a prompt, or of a tool that gets past schema validation, being answered with `-32603`: it gets `-32602`, as the spec requires. -* Fix a manually registered element with a bad handler answering every list and call with `-32602` and the internal registration message: the client gets `-32603` `Internal server error.`, also on a `2026-07-28` `tools/call` over HTTP, and the `ConfigurationException` is logged. -* Fix `maxLineBytes` of `StdioTransport` discarding lines two bytes shorter than the limit: the limit now counts a line with its newline. -* Fix a `ReadResourceResult` sending its own `ttlMs` and `cacheScope` to handshake-era clients. -* Fix `subscriptions/listen` without a notification bus agreeing to resource subscriptions and holding the stream open until its lifetime ends with nothing to deliver: it acknowledges nothing and closes at once. `server/discover` no longer announces `resources.subscribe` without a bus, unless `setCapabilities()` sets it. -* [BC Break] Remove `Rev2025Codec`, which nothing used. -* Fix discovery skipping a class whose file declares an enum before it, once that file has been loaded. + * [BC Break] `StreamableHttpTransport` no longer has the protected `flushOutgoingMessages()` and `handleFiberTermination()`. + * [BC Break] A handler calling `\Fiber::suspend()` itself with anything but a `NotificationSuspension` or `RequestSuspension` fails the request without a scheduler, instead of being resumed with `null`. + * Fix the server resuming a fiber the runtime had suspended, which made the handler's await return early or throw and could terminate a Revolt loop. Without a scheduler, such a handler fails the request with a `LogicException` naming `Builder::setFiberScheduler()`. `Protocol` and `StatelessProtocol` take the scheduler as optional last constructor argument. + * Fix what a handler throws while the runtime drives its fiber escaping to the runtime: it reaches the server, and what it throws while its fiber is discarded is logged. + * Fix a request failing with `503`, or a resumed one losing its session writes, while another request of the session holds the lock across an await: both wait for the lock on the loop. + * Fix a handler still running after its request was answered firing `ResponseEvent` or `ErrorEvent`: what it throws is logged, and calling the `ClientGateway` throws. +* stdio + * Fix a stdio server on Windows ending by itself after about 13.5 hours without input: `StdioTransport` reads only once `stream_select()` reports input. Requires PHP 8.5 on Windows. + * Fix a stdio server never answering a message that reaches it in several reads, such as a tool call with large arguments. + * Fix a stdio server answering a notification it refuses, e.g. one sent before `initialize`: the refusal is only logged. + * Fix `maxLineBytes` of `StdioTransport` discarding lines two bytes shorter than the limit, and `StdioTransport` throwing a `ValueError` on an input that cannot be selected, like `php://memory`. ### Client -* [BC Break] Refuse OAuth URLs whose host is `0.0.0.0`, `::`, an IPv4 address within IPv6 or an IPv4 address in a non-standard form (e.g. `127.1`), or that holds percent-encoding or non-ASCII characters (an internationalized name must be given in punycode). -* [BC Break] Start a stdio server on Windows without `cmd.exe`, as it corrupted arguments containing `"`, `%` or `!`. A batch file like `npx` now needs `command: 'cmd'` and `args: ['/c', 'npx', ...]`. -* Fix the stdio client hanging on Windows while the server stays silent, without enforcing its timeouts: `StdioTransport` now reads only once `stream_select()` reports output. Requires PHP 8.5, as `stream_select()` on Windows reports an idle pipe as readable before that. -* [BC Break] Remove JSON-RPC batch support: a JSON array from the server is dropped with a warning. Batching was removed from MCP in `2025-06-18`. -* [BC Break] Fix `HttpTransport` reading a response body that is not an SSE stream without a limit, which could exhaust client memory on a large body. It now reads up to the new `maxResponseBodyBytes` argument (default: 8 MiB): a successful response over that limit fails the request, and an error status keeps its body cut at the limit. Raise the limit if your server sends larger results. -* [BC Break] `HttpTransport` turns off redirects on a `symfony/http-client` client, discovered or passed, and treats every `3xx` answer as an error that names the status and target. Before, a redirect to another origin received the configured headers, such as an API key, and on a `307` or `308` the request body. If an endpoint of yours may have redirected to a host you don't control, rotate those credentials. An endpoint that is reached through a redirect stops working, so configure its final URL. A client you pass must not follow redirects to another origin. -* Fix `HttpTransport` taking an `Mcp-Session-Id` from any response: it replaced the session mid-way and, on `2026-07-28`, made `getSessionId()` return a value and `close()` send a `DELETE`. Only the answer to `initialize` sets it. -* Add `OAuth::forServiceAccount()` for the client credentials grant, `OAuth::setPrivateKeyJwt()` for `private_key_jwt` client authentication and `OAuth::setCrossAppAccess()` to reach a server through an enterprise identity. -* Add `OAuth::setClientMetadataUrl()`, to use a client ID metadata document where the authorization server supports it, and `ConsoleAuthorizationHandler` for a shell without a browser. -* Add the `auth` parameter of `HttpTransport` and `AuthenticatorInterface`, to authenticate every request and retry the ones the server refuses. `BearerToken` sends a token you already have, and `AuthenticatingHttpClient` offers the same behavior as a PSR-18 decorator. -* Add OAuth authorization: pass `OAuth::forApplication('My App')->build()` as `auth` and the client answers a `401` with discovery, dynamic or pre-registered client identity, the authorization code flow with PKCE and a retry. It refreshes tokens, handles step-up challenges and treats the server's metadata as untrusted input. See [Authorization](docs/client/authorization.md). -* `AuthenticatingHttpClient` and `OAuth` turn off redirects on a `symfony/http-client` client, so a redirect can't carry a token or client secret to another path or origin. -* Add `OAuth::setExpectedIssuer()`, required with `setClientCredentials()`, so a hostile MCP server can't receive the secret or an authorization code. -* Add `RedirectAuthorizationHandler` and `OAuthAuthenticator::finishAuthorization()`, to finish the authorization in a later request (e.g. the callback route of a web application). Its storage implements `PendingAuthorizationStorageInterface`, as `FileCredentialStorage` does. -* Add `LoopbackAuthorizationHandler` and `HeadlessAuthorizationHandler` behind `AuthorizationHandlerInterface`, and `InMemoryCredentialStorage` and `FileCredentialStorage` behind `CredentialStorageInterface`. -* [BC Break] A `401` or `403` answer to a request, a notification or the resumption of an event stream fails with an `AuthorizationException` that contains the `WWW-Authenticate` header, instead of a `RequestException`. `Client::connect()` wraps it in a `ConnectionException`, without a retry. -* Fix `Client::connect()` leaving the previous transport open when called again: it closes the current transport first, so no stdio process or HTTP session is left behind. -* Fix `HttpTransport` not reading the listening stream during a request answered with plain JSON, which left server messages waiting for the next SSE-answered request. - -### JSON-RPC - -* Add `UnknownMethodException`, a subclass of `InvalidInputMessageException` for a method the factory has not registered. -* [BC Break] `MessageFactory::create()` returns one `MessageInterface` and throws `InvalidInputMessageException` for invalid input, instead of returning a list holding messages and exceptions. -* [BC Break] Remove `MessageFactory::DEFAULT_MAX_BATCH_SIZE` and the `$maxBatchSize` parameter of `MessageFactory::__construct()` and `MessageFactory::make()`, whose first parameter is now `$additional`. +* Authorization + * Add OAuth authorization: pass `OAuth::forApplication('My App')->build()` as `auth` and the client answers a `401` with discovery, dynamic or pre-registered client identity, the authorization code flow with PKCE and a retry. It refreshes tokens, handles step-up challenges and treats the server's metadata as untrusted input. See [Authorization](docs/client/authorization.md). + * Add `OAuth::forServiceAccount()` for the client credentials grant, `setPrivateKeyJwt()` for `private_key_jwt` client authentication, `setCrossAppAccess()` to reach a server through an enterprise identity, and `setClientMetadataUrl()` for a client ID metadata document. `setExpectedIssuer()` is required with `setClientCredentials()`, so a hostile MCP server can't receive the secret or an authorization code. + * Add `LoopbackAuthorizationHandler`, `HeadlessAuthorizationHandler`, `ConsoleAuthorizationHandler` for a shell without a browser, and `RedirectAuthorizationHandler` with `OAuthAuthenticator::finishAuthorization()` to finish in a later request, such as a web callback. Credentials go to `InMemoryCredentialStorage` or `FileCredentialStorage`, which also implements `PendingAuthorizationStorageInterface`. + * Add the `auth` parameter of `HttpTransport` and `AuthenticatorInterface`, to authenticate every request and retry the ones the server refuses. `BearerToken` sends a token you already have, and `AuthenticatingHttpClient` offers the same as a PSR-18 decorator. + * [BC Break] A `401` or `403` answer to a request, a notification or a stream resumption fails with an `AuthorizationException` holding the `WWW-Authenticate` header, instead of a `RequestException`. `Client::connect()` wraps it in a `ConnectionException`, without a retry. +* HTTP transport + * [BC Break] Turn off redirects on a `symfony/http-client` client in `HttpTransport`, `AuthenticatingHttpClient` and `OAuth`, discovered or passed, and fail on every `3xx` answer, naming the status and target. A redirect to another origin used to receive the configured headers, like an API key, and on a `307` or `308` the body: rotate those credentials if an endpoint of yours may have redirected to a host you don't control. Configure the final URL of an endpoint reached through a redirect. A client you pass must not follow redirects to another origin. + * [BC Break] Limit what `HttpTransport` reads of a response that is not an SSE stream to the new `maxResponseBodyBytes` (default: 8 MiB): a larger successful response fails the request, and an error body is cut at the limit. + * Fix `HttpTransport` taking an `Mcp-Session-Id` from any response instead of only the answer to `initialize`, which replaced the session mid-way and, on `2026-07-28`, made `close()` send a `DELETE`. + * Fix `HttpTransport` not reading the listening stream during a request answered with plain JSON. +* stdio on Windows + * [BC Break] Start a stdio server without `cmd.exe`, which corrupted arguments containing `"`, `%` or `!`. A batch file like `npx` needs `command: 'cmd'` and `args: ['/c', 'npx', ...]`. + * Fix the stdio client hanging while the server stays silent, ignoring its timeouts. Requires PHP 8.5. +* Fix `Client::connect()` leaving the previous transport open when called again. ### Schema -* [BC Break] An `x-mcp-header` annotation that is not on a property reached through `properties` alone, e.g. under `items`, `anyOf` or `$defs`, makes the tool definition invalid instead of being ignored: `Tool` throws an `InvalidArgumentException` for it, the client drops such a tool from `tools/list` and refuses a sampling request that carries one. A null annotation is invalid too, wherever it sits. -* [BC Break] `CallToolResult` preserves explicit `structuredContent: null` in construction and decoding. Calls that used null for omission must omit the argument (use `meta: $meta` for metadata) or pass `StructuredContent::Absent`. An explicit null that violates the output schema becomes a tool error, and strict handshake-era clients can reject it. `hasStructuredContent()` distinguishes absent content from null; text-only `success()` and `error()` results remain absent. -* `ext-fileinfo` is optional: without it, creating content from a file without a MIME type throws a `LogicException`. +* [BC Break] `MessageFactory::create()` returns one `MessageInterface` and throws `InvalidInputMessageException` for invalid input, instead of returning a list of messages and exceptions. +* [BC Break] Remove `MessageFactory::DEFAULT_MAX_BATCH_SIZE` and the `$maxBatchSize` parameter of `MessageFactory::__construct()` and `MessageFactory::make()`, whose first parameter is now `$additional`. +* Add `UnknownMethodException`, a subclass of `InvalidInputMessageException` for a method the factory has not registered. +* [BC Break] An `x-mcp-header` annotation that is not on a property reached through `properties` alone, e.g. under `items`, `anyOf` or `$defs`, or that is null, makes the tool definition invalid: `Tool` throws an `InvalidArgumentException`, and the client drops such a tool from `tools/list` and refuses a sampling request carrying one. +* [BC Break] `CallToolResult` keeps an explicit `structuredContent: null` when constructed and decoded. Code that passed null to omit it must leave the argument out (use `meta: $meta` for metadata) or pass `StructuredContent::Absent`. An explicit null that violates the output schema becomes a tool error, and strict handshake-era clients can reject it. `hasStructuredContent()` tells absent content from null; `success()` and `error()` stay without structured content. 0.9.0 ----- From e8911198089edaa1cffd6326a4ded85a78a76e82 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Tue, 10 Nov 2026 13:59:12 +0100 Subject: [PATCH 2/4] [Docs] Mark cross-app access as experimental in the changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd09457c..a5c342c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * The SDK is split into the `Mcp\Schema`, `Mcp\Client` and `Mcp\Server` namespaces, ahead of separate packages: most imports change * PHP 8.2 or higher is required, and JSON-RPC batches are gone -* The client authorizes with OAuth: authorization code, client credentials, `private_key_jwt` and cross-app access +* The client authorizes with OAuth: authorization code, client credentials, `private_key_jwt` and experimental cross-app access * Handshake-era servers over HTTP run without sessions unless `Builder::setSession()` is called, and sessions belong to the principal that opened them * Request handlers run under an event loop (Revolt, ReactPHP) and stream their responses * Tool arguments keep their JSON types, and arguments that don't fit are answered with `-32602` @@ -118,7 +118,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * Authorization * Add OAuth authorization: pass `OAuth::forApplication('My App')->build()` as `auth` and the client answers a `401` with discovery, dynamic or pre-registered client identity, the authorization code flow with PKCE and a retry. It refreshes tokens, handles step-up challenges and treats the server's metadata as untrusted input. See [Authorization](docs/client/authorization.md). - * Add `OAuth::forServiceAccount()` for the client credentials grant, `setPrivateKeyJwt()` for `private_key_jwt` client authentication, `setCrossAppAccess()` to reach a server through an enterprise identity, and `setClientMetadataUrl()` for a client ID metadata document. `setExpectedIssuer()` is required with `setClientCredentials()`, so a hostile MCP server can't receive the secret or an authorization code. + * Add `OAuth::forServiceAccount()` for the client credentials grant, `setPrivateKeyJwt()` for `private_key_jwt` client authentication, the experimental `setCrossAppAccess()` to reach a server through an enterprise identity, and `setClientMetadataUrl()` for a client ID metadata document. `setExpectedIssuer()` is required with `setClientCredentials()`, so a hostile MCP server can't receive the secret or an authorization code. * Add `LoopbackAuthorizationHandler`, `HeadlessAuthorizationHandler`, `ConsoleAuthorizationHandler` for a shell without a browser, and `RedirectAuthorizationHandler` with `OAuthAuthenticator::finishAuthorization()` to finish in a later request, such as a web callback. Credentials go to `InMemoryCredentialStorage` or `FileCredentialStorage`, which also implements `PendingAuthorizationStorageInterface`. * Add the `auth` parameter of `HttpTransport` and `AuthenticatorInterface`, to authenticate every request and retry the ones the server refuses. `BearerToken` sends a token you already have, and `AuthenticatingHttpClient` offers the same as a PSR-18 decorator. * [BC Break] A `401` or `403` answer to a request, a notification or a stream resumption fails with an `AuthorizationException` holding the `WWW-Authenticate` header, instead of a `RequestException`. `Client::connect()` wraps it in a `ConnectionException`, without a retry. From ffc7a97818f26b886cc1f6f8db6a34ef6a6c1464 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Tue, 10 Nov 2026 14:04:37 +0100 Subject: [PATCH 3/4] [Docs] Keep the redirect error in the changelog specific to HttpTransport --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c342c7..492ae71e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,7 +123,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * Add the `auth` parameter of `HttpTransport` and `AuthenticatorInterface`, to authenticate every request and retry the ones the server refuses. `BearerToken` sends a token you already have, and `AuthenticatingHttpClient` offers the same as a PSR-18 decorator. * [BC Break] A `401` or `403` answer to a request, a notification or a stream resumption fails with an `AuthorizationException` holding the `WWW-Authenticate` header, instead of a `RequestException`. `Client::connect()` wraps it in a `ConnectionException`, without a retry. * HTTP transport - * [BC Break] Turn off redirects on a `symfony/http-client` client in `HttpTransport`, `AuthenticatingHttpClient` and `OAuth`, discovered or passed, and fail on every `3xx` answer, naming the status and target. A redirect to another origin used to receive the configured headers, like an API key, and on a `307` or `308` the body: rotate those credentials if an endpoint of yours may have redirected to a host you don't control. Configure the final URL of an endpoint reached through a redirect. A client you pass must not follow redirects to another origin. + * [BC Break] Turn off redirects on a `symfony/http-client` client, discovered or passed, in `HttpTransport`, `AuthenticatingHttpClient` and `OAuth`. `HttpTransport` fails on every `3xx` answer with an error that names the status and target, while `AuthenticatingHttpClient` returns it unchanged. A redirect to another origin used to receive the configured headers, like an API key, and on a `307` or `308` the body: rotate those credentials if an endpoint of yours may have redirected to a host you don't control. Configure the final URL of an endpoint reached through a redirect. A client you pass must not follow redirects to another origin. * [BC Break] Limit what `HttpTransport` reads of a response that is not an SSE stream to the new `maxResponseBodyBytes` (default: 8 MiB): a larger successful response fails the request, and an error body is cut at the limit. * Fix `HttpTransport` taking an `Mcp-Session-Id` from any response instead of only the answer to `initialize`, which replaced the session mid-way and, on `2026-07-28`, made `close()` send a `DELETE`. * Fix `HttpTransport` not reading the listening stream during a request answered with plain JSON. From e48c82839dabeb8f3f9dd45accd999e713a0707d Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Tue, 10 Nov 2026 14:20:30 +0100 Subject: [PATCH 4/4] [Docs] Mark the fiber scheduler as opt-in in the changelog TL;DR --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 492ae71e..eb0b7062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * PHP 8.2 or higher is required, and JSON-RPC batches are gone * The client authorizes with OAuth: authorization code, client credentials, `private_key_jwt` and experimental cross-app access * Handshake-era servers over HTTP run without sessions unless `Builder::setSession()` is called, and sessions belong to the principal that opened them -* Request handlers run under an event loop (Revolt, ReactPHP) and stream their responses +* Request handlers can run under an event loop (Revolt, ReactPHP) with the opt-in `Builder::setFiberScheduler()`, and stream their responses * Tool arguments keep their JSON types, and arguments that don't fit are answered with `-32602` * stdio works on Windows, in the server and the client