From 9b3b3a916d7a8828e1d7347c36002958de506b60 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sun, 18 Oct 2026 16:21:32 +0200 Subject: [PATCH 1/2] [Docs] Group and condense the 0.9.0 changelog --- CHANGELOG.md | 157 +++++++++++++++++++++++---------------------------- 1 file changed, 72 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0baa099..7f81d213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,92 +5,79 @@ All notable changes to `mcp/sdk` will be documented in this file. 0.9.0 ----- -* [BC Break] `DnsRebindingProtectionMiddleware` always validates `Host` (a missing one is rejected) and checks `Origin` separately against the new `allowedOrigins`, including scheme and port and empty by default. Same-origin requests are always allowed and both lists accept `['*']`. `allowedOrigins` is the new second constructor argument; pass the factories by name. -* Fix the client throwing a native `\JsonException` for request params it cannot encode (e.g. invalid UTF-8): it now throws `InvalidArgumentException`, and a request handler result it cannot encode is answered with an internal error instead of no answer. -* [BC Break] Add `RegistryInterface::publishesListChanges()`: a registry passed to `setRegistry()` and wired to the notification bus with a `PublishingEventDispatcher` gets `listChanged` announced on `server/discover`. A decorating registry forwards the answer of the one it wraps. -* Fix servers announcing `listChanged` where no list changes are delivered: the `initialize` result never carries it, and `server/discover` only with a notification bus and the built-in registry. A server with only an event dispatcher no longer announces it. -* Fix concurrent requests of one session overwriting each other's session changes: `Session::save()` now writes only the keys it changed, onto what the store holds at that point. -* Fix `ClientGateway::progress()` sending another call's progress token when calls of one session run in parallel: the token now comes from the gateway's own request. -* [BC Break] `SchemaValidator` takes an optional `Opis\JsonSchema\Validator` as its first constructor argument, moving `$logger` to second. Pass `logger:` by name. -* [BC Break] `SchemaValidator::validateAgainstJsonSchema()` no longer validates an empty array as an object. Pass `new \stdClass()` for an empty object. -* Fix `SchemaValidator` falling back to opis' wording for camelCase keywords like `minLength`, `multipleOf` or `additionalProperties`, and reporting a pointer relative to the `$ref` target inside `$defs`. Clients now see the SDK's messages and the full pointer. -* Add `Builder::setSchemaValidator()` to configure the validator used for `tools/call` input and output, e.g. with a resolver for external `$ref` schemas. -* [BC Break] Remove the `providerClass` argument of `#[CompletionProvider]`. Use `provider:`, which takes the same class-string and is now the first positional argument. -* [BC Break] `StreamableHttpTransport::handleFiberTermination()` takes the terminated `\Fiber` as its first argument; a subclass overriding it has to accept it too. -* [BC Break] Reject a `Tool` input schema whose `properties` is not an object or whose `required` is neither a list nor `null`, instead of silently replacing the member. Reject a `completion/complete` whose `argument` is missing `name` or `value`, instead of completing against an empty prefix. -* Add `HttpTransport::getSessionId()` to read the server-minted `Mcp-Session-Id`: a request-scoped caller can persist it and pass it back through the constructor's `$headers` on a later transport. Always `null` on `2026-07-28`, which removed protocol-level sessions. -* Fix `HttpTransport` omitting the `MCP-Protocol-Version` header on requests after the `initialize` handshake, including the closing `DELETE`. -* Fix memory growing with every `tools/call` in long-lived processes like FrankenPHP workers or stdio servers. -* Fix the stdio transport answering a request before the notifications its handler sent, so a client could miss them. -* Fix OIDC discovery rejecting issuers with a trailing slash (e.g. Authentik, Auth0). -* Fix `Client::connect()` rejecting an empty `serverInfo.version`, as the Python SDK sends, and leaving the transport open when initialization fails on a malformed result: the transport is now closed and the failure wrapped in a `ConnectionException`. -* Fix parallel elicitations on one session getting each other's answers: requests to the client get UUID ids instead of a session counter, and a request or notification a handler sends goes out on the stream of its own call. -* [BC Break] Ids of requests to the client are UUIDs: `Protocol::sendRequest()`, `Protocol::checkResponse()` and `Protocol::handleFiberYield()` take and return a `Uuid` instead of an `int`, as does the response finder of a transport. On the wire and in the session they are RFC 4122 strings. -* Fix `StreamableHttpTransport` answering a POST of only responses or notifications with `200` and the session's queued messages: it is now accepted with `202` and no body, and the queued messages stay. -* Fix `StreamableHttpTransport` bundling the session's queued messages (e.g. `notifications/resources/updated`) with a request's response into a JSON array, which clients on `2025-06-18` and later reject: such a request is now answered as SSE, the queued messages first, and a JSON answer holds the response alone. -* Fix stateless SSE streams holding back frames until close when PHP output buffering is enabled. -* Fix a session deleted while one of its requests still ran coming back when that request saved it. -* Reject a recognized `Mcp-Param-*` header whose mirrored argument is absent from the body with `-32020`, instead of accepting the request (SEP-2243). -* Fix `RequestEvent`, `ResponseEvent` and `ErrorEvent` not being dispatched for `2026-07-28` requests. -* [BC Break] Validate a tool result's `structuredContent` against the tool's `outputSchema`, which the specification requires the server to honour. A mismatch is answered with a `CallToolResult` carrying `isError: true` instead of the non-conforming value, matching the TypeScript, Python and Java SDKs. Skipped when the tool declares no `outputSchema`, when the result carries no `structuredContent`, and when the result is already an error. Return `new \stdClass()` for an empty object, since `[]` is sent as an array. -* Stop the server `Protocol` from logging full JSON-RPC payloads (tool arguments, client replies) at info level: info records now carry only the method and id, the raw message is logged at debug level. -* Add `PassthroughMiddleware` to opt `StreamableHttpTransport` out of its default middleware without the warning an empty `$middleware` list logs. -* Add `ElicitationSchema::getDefaults()`, returning the declared `default` of each field to accept a form elicitation with. -* [BC Break] `AbstractSchemaDefinition` declares an abstract `getDefault()`, which a custom schema definition has to implement. -* Fix `Client::getPrompt()` without arguments sending `"arguments": []`, which servers validating the spec's object type (e.g. the TypeScript SDK) reject: empty arguments are now omitted. -* Add `Tool::$execution` (`ToolExecution` with a `TaskSupport` enum) and `ServerCapabilities::$tasks`, which were dropped when parsing a 2025-11-25 server's `tools/list` and `initialize` results. -* Add `Client::getServerCapabilities()`, returning what the server declared in `initialize` or, from `2026-07-28` on, in `server/discover`. -* [BC Break] `ClientStateInterface` declares `setServerCapabilities()` and `getServerCapabilities()`, which a custom implementation has to add. -* Fix `Client::getServerInfo()`, `getInstructions()`, `getServerCapabilities()` and `getProtocolVersion()` reporting the previous server's values after a reconnect, whether it failed or reached a `2026-07-28` server whose `server/discover` leaves them out. -* [BC Break] `ClientStateInterface::setProtocolVersion()`, `setServerInfo()` and `setServerCapabilities()` accept `null`, which a custom implementation has to accept too. -* Add a `listen` option to the client's `HttpTransport`, opening the standalone GET stream on which a 2025-era server sends requests and notifications outside of a client request, like `roots/list`. Needs a PSR-18 client that streams response bodies, such as `symfony/http-client`. -* [BC Break] Narrow authorization to the resource server role (ADR 0002): remove `OAuthProxyMiddleware`, `ClientRegistrationMiddleware`, `ClientRegistrarInterface` and `ClientRegistrationException`. -* [BC Break] Replace `OAuthRequestMetaMiddleware` with `RequestContext::getAccessToken()`, returning the validated `Server\Authorization\AccessToken`; `AuthorizationResult::allow()` takes an `AccessToken` instead of request attributes. -* [BC Break] Add an `?AccessToken $accessToken` parameter to `Protocol::processInput()`, `BaseTransport::handleMessage()` and `StreamableHttpTransport::handlePostRequest()`, and as fourth argument of the `TransportInterface::onMessage()` listener; overrides need the new signature. -* [BC Break] Make `JwtTokenValidator` final with a single issuer instead of a list of issuer aliases, and a `$keys` set (e.g. `CachedKeySet`); `JwtTokenValidator::fromIssuer()` discovers and caches keys in a PSR-6 pool, refetching on unknown key ids. The `alg` allowlist is enforced, `$tokenType` and `$leeway` are added, `requireScopes()` is removed in favour of `ScopePolicy`. -* [BC Break] Remove `JwksProvider`, `JwksProviderInterface`, `OidcDiscoveryInterface` and the OIDC metadata policies; `OidcDiscovery` is internal. -* [BC Break] `ProtectedResourceMetadata` requires `$resource`, serves at the path derived from it (RFC 9728 §3.1) and requires https except for loopback hosts; drops localized, policy, ToS, extra fields and `$metadataPaths`. -* [BC Break] Add `ScopePolicy` as third argument of `AuthorizationMiddleware`, answering `403 insufficient_scope` per method and tool, with scope hierarchies; the `resource_metadata` challenge URL comes from the configured resource instead of the `Host` header. -* Expose `WWW-Authenticate` in the default `CorsMiddleware`. -* Fix CORS preflight requests failing with `401` behind `AuthorizationMiddleware`, which now lets `OPTIONS` requests pass without a token. -* Allow `Mcp-Method`, `Mcp-Name` and `Mcp-Param-*` in the default `CorsMiddleware`. An `$allowedHeaders` entry ending in `*` allows each requested header with that prefix. -* [BC Break] Fix concurrent Streamable HTTP streams on one session resuming each other's fibers: each stream now polls only the client request its own fiber sent, so an elicitation answer reaches the tool call that asked for it. `Protocol::handleFiberYield()` returns the ID of the request it sent. -* Fix lost responses on concurrent requests of one session over Streamable HTTP: a POST is answered with its own responses instead of taking them from the session's outgoing queue. -* [BC Break] `TransportInterface::send()` receives every response of a handler that does not suspend, not only errors raised before a session exists; the session's outgoing queue only carries server-initiated requests and notifications. A custom transport must deliver each message it gets, as a batch gets several. -* Fix the client request timeout firing up to a second early and throwing a generic `RequestException`: it now throws `TimeoutException`, which extends `RequestException`, and sends `notifications/cancelled` for the abandoned request. -* [BC Break] `TimeoutException` extends `RequestException` instead of `Exception`, so a `catch (RequestException)` now also catches timeouts. `ClientStateInterface::getPendingRequests()` reports `timestamp` as a `float` from `microtime(true)` instead of an `int`. -* Fix the client's `HttpTransport` waiting out the request timeout when a non-streamed response to a request ends without its answer (truncated, empty, unreadable or not JSON): the call now fails at once with a `RequestException`, also when the body of an error status fails to read. -* Fix the client's `HttpTransport` failing to connect through an amphp-based PSR-18 client, like Symfony's `Psr18Client(new AmpHttpClient())`: the request never left the process and `connect()` failed at the init timeout with `FutureIterator::consume(): Return value must be of type ?array`. Requests are now sent from outside the request fiber, where the event loop runs. -* Fix an unreachable `Psr16SessionStore` cache being answered as "session not found" (`404`): the failure is logged and answered with `503`, so clients retry instead of re-initializing. `SessionStoreInterface::exists()` may throw the new `SessionStoreException`. -* Serve both protocol eras over stdio: `StdioTransport` settles the era on the client's first request and serves `2026-07-28` requests, `subscriptions/listen` and `notifications/cancelled` on the one channel. -* Fix `ClientGateway::log()` ignoring the client's log level: like `ClientLogger`, it drops messages below the level set with `logging/setLevel`, and below `warning` while the client has set none. -* [BC Break] `ClientLogger` takes only the `ClientGateway`, which now applies the client-set level itself. -* [BC Break] `StatelessAwareTransportInterface` declares `setHandshakeVersions()`, so a server without the modern era names only the revisions it negotiates when refusing a `2026-07-28` request, e.g. the one set with `Builder::setProtocolVersion()`. -* Answer a bare `initialize` on a `2026-07-28`-only endpoint with `-32022` naming the served revisions, and a request without a session on the handshake leg with its id. -* Speak both protocol eras from a client configured with `2026-07-28`: `connect()` probes with `server/discover` and falls back to the `initialize` handshake on `2025-11-25` when the server does not speak the modern era. `Builder::setFallbackProtocolVersion()` picks the fallback revision, or `null` for a modern-only client. -* [BC Break] Bump the client's default protocol version to `2026-07-28`, so a client speaks both protocol eras unless told otherwise. Against a server that speaks both, a default client now settles on `2026-07-28`, where a server can no longer sample or list roots through `ClientGateway`; return those asks as an `InputRequiredResult` instead, or call `Builder::setProtocolVersion(ProtocolVersion::V2025_11_25)` on the client. -* On a `2026-07-28` connection, `Client::setLoggingLevel()` stamps the level on every following request, `Client::ping()` sends `server/discover` and `Client::sendRootsListChanged()` sends nothing. -* Fail a client request at once when the HTTP server refuses it with an error status or the stdio server process exits, instead of waiting out the timeout. -* Report the exit code and the end of stderr when the stdio server process dies, instead of only that it is no longer running. +Concurrent requests of one session no longer lose or swap each other's answers, session changes and fibers, and the client fails fast instead of waiting out its timeout. Authorization narrows to the resource server role, and the stdio transport and the client speak both protocol eras. + +### Server + +* Concurrent requests and sessions + * Add an opt-in per-session lock, `Builder::setSessionLock()`, serializing the concurrent requests of one session so none of them overwrites what another saved. `FileSessionLock` locks with `flock()` next to `FileSessionStore`, whose `gc()` also removes stale lock files; `SymfonyLockSessionLock` uses a symfony/lock store for sessions shared across machines; `SessionLockInterface` takes custom ones. A request that cannot get the lock in time, or hits a failing lock backend, is answered with `503` and a JSON-RPC error (`SessionLockTimeoutException` and `SessionLockException`). `Protocol::__construct()` takes the lock as optional last argument. + * Fix concurrent requests of one session overwriting each other's session changes: `Session::save()` writes only the keys it changed, onto what the store holds at that point. Writes of a handler after it resumed from an elicitation, sampling request or notification are saved too, and a session deleted while one of its requests still ran no longer comes back when that request saves it. + * Fix an unreachable `Psr16SessionStore` cache being answered as "session not found" (`404`): the failure is logged and answered with `503`, so clients retry instead of re-initializing. `SessionStoreInterface::exists()` may throw the new `SessionStoreException`. + * Fix parallel calls of one session getting each other's elicitation answers and progress tokens: each stream polls only the client request its own fiber sent, requests to the client get UUID ids instead of a session counter, and `ClientGateway::progress()` takes the token from its own request. + * Fix concurrent stdio tool calls that report progress, log, elicit or sample never finishing: `StdioTransport` keeps every suspended fiber and resumes each with the answer to its own request. A call's notifications now go out before its response. + * [BC Break] Ids of requests to the client are UUIDs: `Protocol::sendRequest()`, `Protocol::checkResponse()` and `Protocol::handleFiberYield()` take and return a `Uuid` instead of an `int`, as does the response finder of a transport; `handleFiberYield()` returns the id of the request it sent. A transport passes the suspended `\Fiber` as last argument to the pending-requests provider and the fiber yield handler, and `StreamableHttpTransport::handleFiberTermination()` takes the terminated `\Fiber` as its first argument. + * [BC Break] Fix lost responses on concurrent requests of one session over Streamable HTTP: a POST is answered with its own responses instead of taking them from the session's outgoing queue. `TransportInterface::send()` receives every response of a handler that does not suspend, and the outgoing queue only carries server-initiated requests and notifications; a custom transport must deliver each message it gets, as a batch gets several. + * Fix `StreamableHttpTransport` bundling the session's queued messages (e.g. `notifications/resources/updated`) with a request's response into a JSON array, which clients on `2025-06-18` and later reject: such a request is answered as SSE, the queued messages first. A POST of only responses or notifications is accepted with `202` and no body, and the queued messages stay. + * Fix requests to the client (elicitation, sampling) staying in the session's pending requests after they timed out or their stream vanished: `Protocol::checkResponse()` expires them with the `Request timed out` error, entries of a vanished stream are dropped a minute past their timeout, and an answer to a request the server is not waiting on is dropped instead of stored. + * Fix a handler throwing after its fiber resumed escaping to the transport: it is answered with an error response, like one throwing before suspending. A suspended handler is resumed with an error instead of `null` when the transport fails to store what it sends to the client; `ClientGateway::notify()` then throws a `ClientException`. + * Fix a handshake-era Streamable HTTP stream pinning its PHP worker until the elicitation (or other client request) times out after the client disconnected: the idle stream sends a keep-alive comment and stops once the connection is aborted. +* Authorization and HTTP security + * [BC Break] Narrow authorization to the resource server role (ADR 0002): remove `OAuthProxyMiddleware`, `ClientRegistrationMiddleware`, `ClientRegistrarInterface`, `ClientRegistrationException`, `JwksProvider`, `JwksProviderInterface`, `OidcDiscoveryInterface` and the OIDC metadata policies; `OidcDiscovery` is internal. + * [BC Break] Replace `OAuthRequestMetaMiddleware` with `RequestContext::getAccessToken()`, returning the validated `Server\Authorization\AccessToken`; `AuthorizationResult::allow()` takes an `AccessToken` instead of request attributes. `Protocol::processInput()`, `BaseTransport::handleMessage()` and `StreamableHttpTransport::handlePostRequest()` take an `?AccessToken $accessToken` parameter, as does the `TransportInterface::onMessage()` listener as fourth argument. + * [BC Break] Make `JwtTokenValidator` final with a single issuer instead of a list of issuer aliases, and a `$keys` set (e.g. `CachedKeySet`); `JwtTokenValidator::fromIssuer()` discovers and caches keys in a PSR-6 pool, refetching on unknown key ids. The `alg` allowlist is enforced, `$tokenType` and `$leeway` are added, `requireScopes()` is removed in favour of `ScopePolicy`. + * [BC Break] Add `ScopePolicy` as third argument of `AuthorizationMiddleware`, answering `403 insufficient_scope` per method and tool, with scope hierarchies; the `resource_metadata` challenge URL comes from the configured resource instead of the `Host` header. + * [BC Break] `ProtectedResourceMetadata` requires `$resource`, serves at the path derived from it (RFC 9728 §3.1) and requires https except for loopback hosts; drops localized, policy, ToS, extra fields and `$metadataPaths`. + * Fix OIDC discovery rejecting issuers with a trailing slash (e.g. Authentik, Auth0). + * Fix CORS preflight requests failing with `401` behind `AuthorizationMiddleware`, which now lets `OPTIONS` requests pass without a token. The default `CorsMiddleware` allows `Mcp-Method`, `Mcp-Name` and `Mcp-Param-*` and exposes `WWW-Authenticate`; an `$allowedHeaders` entry ending in `*` allows each requested header with that prefix. + * [BC Break] `DnsRebindingProtectionMiddleware` always validates `Host` (a missing one is rejected) and checks `Origin` separately against the new `allowedOrigins`, including scheme and port and empty by default. Same-origin requests are always allowed and both lists accept `['*']`. `allowedOrigins` is the new second constructor argument; pass the factories by name. + * Add `PassthroughMiddleware` to opt `StreamableHttpTransport` out of its default middleware without the warning an empty `$middleware` list logs. +* Protocol eras + * [BC Break] Serve both protocol eras over stdio: `StdioTransport` settles the era on the client's first request and serves `2026-07-28` requests, `subscriptions/listen` and `notifications/cancelled` on the one channel. `StatelessAwareTransportInterface` declares `setHandshakeVersions()`, so a server without the modern era names only the revisions it negotiates when refusing a `2026-07-28` request. + * Answer a bare `initialize` on a `2026-07-28`-only endpoint with `-32022` naming the served revisions, and a request without a session on the handshake leg with its id. + * Reject a recognized `Mcp-Param-*` header whose mirrored argument is absent from the body with `-32020`, instead of accepting the request (SEP-2243). + * [BC Break] Announce `listChanged` only where list changes are delivered: never in the `initialize` result, and on `server/discover` only when the registry publishes them to a notification bus. `RegistryInterface` declares `publishesListChanges()`, which `Registry` answers with `true` when its dispatcher is a `PublishingEventDispatcher`; a decorating registry forwards the answer of the one it wraps. +* Tools and validation + * [BC Break] Validate a tool result's `structuredContent` against the tool's `outputSchema`, which the specification requires the server to honour. A mismatch is answered with a `CallToolResult` carrying `isError: true` instead of the non-conforming value, matching the TypeScript, Python and Java SDKs. Skipped when the tool declares no `outputSchema`, when the result carries no `structuredContent`, and when the result is already an error. Return `new \stdClass()` for an empty object, since `[]` is sent as an array. + * [BC Break] Add `Builder::setSchemaValidator()` to configure the validator used for `tools/call` input and output, e.g. with a resolver for external `$ref` schemas. `SchemaValidator` takes an optional `Opis\JsonSchema\Validator` as its first constructor argument, moving `$logger` to second (pass `logger:` by name), and `validateAgainstJsonSchema()` no longer validates an empty array as an object (pass `new \stdClass()`). + * Fix `SchemaValidator` falling back to opis' wording for camelCase keywords like `minLength`, `multipleOf` or `additionalProperties`, and reporting a pointer relative to the `$ref` target inside `$defs`. Clients now see the SDK's messages and the full pointer. + * [BC Break] Remove the `providerClass` argument of `#[CompletionProvider]`. Use `provider:`, which takes the same class-string and is now the first positional argument. +* Logging, events and runtime + * [BC Break] Fix the client's log level being ignored: `ClientGateway::log()` drops messages below the level set with `logging/setLevel`, and below `warning` while the client has set none, and `ClientLogger` honours a lower level a `2026-07-28` request asks for with `io.modelcontextprotocol/logLevel`. `ClientLogger` takes only the `ClientGateway`, which applies the level itself. + * Stop the server `Protocol` from logging full JSON-RPC payloads (tool arguments, client replies) at info level: info records now carry only the method and id, the raw message is logged at debug level. + * Fix `RequestEvent`, `ResponseEvent` and `ErrorEvent` not being dispatched for `2026-07-28` requests, and `ResponseEvent` and `ErrorEvent` not when a suspended fiber completes. + * Add `ServerRequestEvent` and `ClientResponseEvent` for handshake-era server-initiated requests (`elicitation/create`, `sampling/createMessage`) and the client's replies. + * Fix memory growing with every `tools/call` in long-lived processes like FrankenPHP workers or stdio servers. + * Fix the server's `StdioTransport` answering a request on an idle connection only after ~50ms: it now waits on the input with `stream_select()` instead of sleeping. + * Fix stateless SSE streams holding back frames until close when PHP output buffering is enabled. + +### Client + +* Protocol eras + * [BC Break] Speak both protocol eras by default: the client's default protocol version is `2026-07-28`, and `connect()` probes with `server/discover` and falls back to the `initialize` handshake on `2025-11-25` when the server does not speak the modern era. `Builder::setFallbackProtocolVersion()` picks the fallback revision, or `null` for a modern-only client. Against a server that speaks both, a default client now settles on `2026-07-28`, where a server can no longer sample or list roots through `ClientGateway`; return those asks as an `InputRequiredResult` instead, or call `Builder::setProtocolVersion(ProtocolVersion::V2025_11_25)` on the client. + * On a `2026-07-28` connection, `Client::setLoggingLevel()` stamps the level on every following request, `Client::ping()` sends `server/discover` and `Client::sendRootsListChanged()` sends nothing. + * Add a `listen` option to the client's `HttpTransport`, opening the standalone GET stream on which a 2025-era server sends requests and notifications outside of a client request, like `roots/list`. Needs a PSR-18 client that streams response bodies, such as `symfony/http-client`. +* Failing fast and recovering + * Fail a client request at once instead of waiting out the timeout when the HTTP server refuses it with an error status, or a non-streamed response ends without its answer (truncated, empty, unreadable or not JSON), also when the body of an error status fails to read. + * Fix the client after the stdio server process exited: requests fail at once with the exit code and the end of stderr, and `Client::isConnected()` turns false. + * [BC Break] Fix the client request timeout firing up to a second early and throwing a generic `RequestException`: it now throws `TimeoutException` and sends `notifications/cancelled` for the abandoned request. `TimeoutException` extends `RequestException` instead of `Exception`, so a `catch (RequestException)` now also catches timeouts. `ClientStateInterface::getPendingRequests()` reports `timestamp` as a `float` from `microtime(true)` instead of an `int`. + * Resume an interrupted SSE response in `HttpTransport` with a GET carrying `Last-Event-ID`, honouring the server's `retry` interval. Configure attempts and fallback backoff with `ReconnectPolicy`, the scheduling clock with any PSR-20 `ClockInterface` (default: a monotonic clock). Protocol version `2026-07-28` does not resume streams. A stream that ends without its response and without an event ID now fails the request instead of waiting for its timeout. + * Fix a client `onProgress` callback receiving progress for an earlier request that timed out: progress now only reaches the callback of the request whose `progressToken` it carries. + * Fix `Client::getServerInfo()`, `getInstructions()` and `getProtocolVersion()` reporting the previous server's values after a reconnect, whether it failed or reached a `2026-07-28` server whose `server/discover` leaves them out. + * Fix `Client::connect()` rejecting an empty `serverInfo.version`, as the Python SDK sends, and leaving the transport open when initialization fails on a malformed result: the transport is now closed and the failure wrapped in a `ConnectionException`. + * Fix the client's `HttpTransport` failing to connect through an amphp-based PSR-18 client, like Symfony's `Psr18Client(new AmpHttpClient())`, where `connect()` failed at the init timeout with `FutureIterator::consume(): Return value must be of type ?array`: requests are now sent from outside the request fiber, where the event loop runs. +* API + * [BC Break] Add `Client::getServerCapabilities()`, returning what the server declared in `initialize` or, from `2026-07-28` on, in `server/discover`. `ClientStateInterface` declares `setServerCapabilities()` and `getServerCapabilities()`, and its `setProtocolVersion()` and `setServerInfo()` accept `null`, which a custom implementation has to follow. + * Add `HttpTransport::getSessionId()` to read the server-minted `Mcp-Session-Id`: a request-scoped caller can persist it and pass it back through the constructor's `$headers` on a later transport. Always `null` on `2026-07-28`, which removed protocol-level sessions. + * Fix `HttpTransport` omitting the `MCP-Protocol-Version` header on requests after the `initialize` handshake, including the closing `DELETE`. + * Fix `Client::getPrompt()` without arguments sending `"arguments": []`, which servers validating the spec's object type (e.g. the TypeScript SDK) reject: empty arguments are now omitted. + * Fix the client throwing a native `\JsonException` for request params it cannot encode (e.g. invalid UTF-8): it now throws `InvalidArgumentException`, and a request handler result it cannot encode is answered with an internal error instead of no answer. + +### Schema + * [BC Break] Bump `MessageInterface::PROTOCOL_VERSION` to `2026-07-28`. Use `ProtocolVersion::latestHandshake()` where a handshake revision is needed, e.g. in an `initialize` answer. -* Add `ServerRequestEvent` and `ClientResponseEvent` for handshake-era server-initiated requests (`elicitation/create`, `sampling/createMessage`) and the client's replies. -* Dispatch `ResponseEvent` and `ErrorEvent` when a suspended Fiber completes. -* Fix a handler throwing after its fiber resumed (e.g. after elicitation or sampling) escaping to the transport: it is now answered with an error response like one throwing before suspending. -* Drop a client response to a request ID the server is not waiting on, or that timed out, instead of storing it in the session, where nothing would ever consume it. -* Fix `ClientLogger` dropping messages below `warning` on `2026-07-28` requests that ask for a lower level with `io.modelcontextprotocol/logLevel`. -* Resume an interrupted SSE response in `HttpTransport` with a GET carrying `Last-Event-ID`, honouring the server's `retry` interval. Configure attempts and fallback backoff with `ReconnectPolicy`, the scheduling clock with any PSR-20 `ClockInterface` (default: a monotonic clock). Protocol version `2026-07-28` does not resume streams. A stream that ends without its response and without an event ID now fails the request instead of waiting for its timeout. -* Fix a client `onProgress` callback receiving progress for an earlier request that timed out: progress now only reaches the callback of the request whose `progressToken` it carries. -* Fix `Client::isConnected()` staying true after the stdio server process exited, which left later calls failing against the dead process. -* Fix the server's `StdioTransport` answering a request on an idle connection only after ~50ms: it now waits on the input with `stream_select()` instead of sleeping. -* Fix a client request that timed out (elicitation, sampling) staying in the session's pending requests: `Protocol::checkResponse()` now expires it, answering with the `Request timed out` error, so the transports no longer implement the timeout themselves. -* Fix a handshake-era Streamable HTTP stream pinning its PHP worker until the elicitation (or other client request) times out after the client disconnected: the idle stream now sends a keep-alive comment and stops once the connection is aborted. -* [BC Break] Fix concurrent stdio tool calls that report progress, log, elicit or sample never finishing: `StdioTransport` keeps every suspended fiber and resumes each with the answer to its own request. A transport passes the suspended `\Fiber` as last argument to the pending-requests provider and the fiber yield handler. -* Add an opt-in per-session lock, `Builder::setSessionLock()`, serializing the concurrent requests of one session so none of them overwrites what another saved: pending server requests, client answers, client info. `FileSessionLock` locks with `flock()` next to `FileSessionStore`, `SymfonyLockSessionLock` uses a symfony/lock store for sessions shared across machines; `SessionLockInterface` takes custom ones. Off by default. A request that cannot get the lock in time is answered with `503` and a JSON-RPC error; a failing lock backend with `503` and `Session lock is unavailable.`, like a failing session store. `SessionLockTimeoutException`, a `SessionLockException`, tells a timeout from a backend failure. `SymfonyLockSessionLock` releases its locks from a shutdown function after a fatal error in a handler. -* `Protocol::__construct()` takes an optional `SessionLockInterface $sessionLock` as its last argument. -* `FileSessionStore::gc()` also removes the lock files of expired sessions a `FileSessionLock` in the same directory left behind, each only while no request holds it. -* Resume a suspended handler with an error when the transport fails to store what it sends to the client next, instead of resuming it with `null`; `ClientGateway::notify()` then throws a `ClientException`. -* Fix session writes of a handler after it resumed from an elicitation, sampling request or notification being lost. -* Fix pending client requests of a vanished stream, and their late answers, staying in the session until it expires: they are dropped once a minute past their timeout. +* Add `Tool::$execution` (`ToolExecution` with a `TaskSupport` enum) and `ServerCapabilities::$tasks`, which were dropped when parsing a 2025-11-25 server's `tools/list` and `initialize` results. +* [BC Break] Reject a `Tool` input schema whose `properties` is not an object or whose `required` is neither a list nor `null`, instead of silently replacing the member. Reject a `completion/complete` whose `argument` is missing `name` or `value`, instead of completing against an empty prefix. +* [BC Break] Add `ElicitationSchema::getDefaults()`, returning the declared `default` of each field to accept a form elicitation with. `AbstractSchemaDefinition` declares an abstract `getDefault()`, which a custom schema definition has to implement. 0.8.1 ----- From bc66f944ae76427e2c5771dec6d1a3d60f744558 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sun, 18 Oct 2026 16:27:41 +0200 Subject: [PATCH 2/2] [Docs] Turn the 0.9.0 summary into a TL;DR list --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f81d213..468c1e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,12 @@ All notable changes to `mcp/sdk` will be documented in this file. 0.9.0 ----- -Concurrent requests of one session no longer lose or swap each other's answers, session changes and fibers, and the client fails fast instead of waiting out its timeout. Authorization narrows to the resource server role, and the stdio transport and the client speak both protocol eras. +### TL;DR + +* Concurrent requests of one session keep their own answers, session changes and fibers +* The client fails fast instead of waiting out its timeout +* Authorization narrows to the resource server role +* stdio and the client speak both protocol eras ### Server