What's Changed
The tracked MCP specification moves from 2025-11-25 to 2026-07-28 with no compatibility layer, so this boundary is breaking throughout. BREAKING_CHANGES.md is the upgrade guide. The entries below are the inventory.
Added
- The Streamable HTTP server transport.
StreamableHttpServerTransportis a PSR-15 handler, andSecuredHttpEndpointwraps it in the recommended middleware: CORS for browser clients, DNS-rebinding Origin protection with opt-in Host allow-listing, a request-body size limit answering413,Mcp-Param-{Name}header validation against tool-call arguments, and an optional authentication slot. Responses stream as SSE with keep-alives or buffer as JSON per response mode, andServer::listen()attaches the dispatcher without blocking, for hosts that drive the transport per request. - The Streamable HTTP client transport.
StreamableHttpClientTransportPOSTs one envelope per exchange, parses SSE responses frame by frame so progress notifications arrive mid-call, mirrorsx-mcp-headertool arguments intoMcp-Param-{Name}headers, retries a call once after aHeaderMismatchanswer by re-listing the tool, and retries once with a protocol version the server named as supported. Outbound requests are bounded by a progress-aware deadline configured viaClientBuilder::setRequestTimeout()/setMaxRequestTimeout(). An exchange whose answer cannot settle the request it carries fails it withUnexpectedHttpStatusExceptioninstead of leaving the caller waiting. - The OAuth 2.1 client half.
AuthorizedHttpClientdecorates the HTTP client the transport already takes: on a401it discovers the protected-resource and authorization-server metadata, resolves a client identifier through pre-registration, a Client ID Metadata Document, or Dynamic Client Registration, runs the PKCE (S256) authorization-code exchange through a caller-suppliedUserAuthorizationInterface, validates the responseiss, binds the token to the MCP server it was issued for, and replays the request. Scope selection follows the challenge, thenscopes_supported, then the caller'sdefaultScopes. An insufficient-scope answer steps the scopes up under a capped budget or is reported viaInsufficientScopeExceptionperInsufficientScopePolicy.offline_accessand refresh tokens are opt-in. Tokens and client registrations persist throughTokenStoreInterface/ClientRegistrationStoreInterface, and everything that writes a token is serialised behind one cancellable lock. - The OAuth 2.1 resource-server half.
BearerAuthenticationMiddlewarevalidates bearer tokens through anAccessTokenValidatorInterface, binds the token audience to the server's canonical URI, and enforces required scopes.ProtectedResourceMetadataHandlerserves the RFC 9728 metadata document at its two well-known paths. The validatedVerifiedAccessTokenreaches handlers as$context->receiveContext->authInfo. JwksAccessTokenValidator, a shipped JWT validator riding the suggestedfirebase/php-jwtpackage. It checks signature and expiry against a caller-supplied key set and maps the claim spellings the common providers use (scope/scp,azp/client_id/cid). Constructing it without the package installed throwsMissingSuggestedDependencyExceptionnaming the install command.subscriptions/listenon both sides.Client::listen(SubscriptionFilter, \Closure)opens a notification stream and returns aSubscriptionStreamhandle. The server serves it from aSubscriptionStoreregistered viaServerBuilder::setSubscriptionStore(). The built-in stores became runtime-mutable (addTool()/removeTool()and siblings on the mutable store interfaces), firelist_changednotifications onto open streams, and the matchinglistChangedcapability flags are advertised only when genuinely deliverable.- The input-required flow (multi round-trip requests).
callTool(),readResource(), andgetPrompt()can answer with anInputRequiredResultdescribing the input the server needs, and the caller answers by calling again withinputResponses:and the echoedrequestState:. Server-side executors, renderers, and readers may returnInputRequiredResultto ask. - Completion registration grew a fluent surface and attribute sugar:
ServerBuilder::addPromptCompletion()/addResourceTemplateCompletion()take a closure or aCompletionProviderInterfaceper argument, and#[AsCompletion](repeatable) marks a method as a provider discovered throughServerBuilder::register(). Registering any completion advertises the capability. - Stdio client supervision. A stdio subprocess that exits unexpectedly is respawned behind the same transport under a time-bounded restart budget. Read-only requests lost to the crash are retried against the replacement peer when
ClientBuilder::setRetryLostRequests()allows, and open subscription streams are re-opened against it. - Server-side lifecycle guards: a handler can refuse a request needing an undeclared client capability (
MissingRequiredClientCapabilityException), the per-request_metaprotocol version is gated, and the new protocol error codes (-32020 HeaderMismatch,-32021 MissingRequiredClientCapability,-32022 UnsupportedProtocolVersion) are modelled onProtocolErrorCode. ServerBuilderconfiguration:setPageSize(),setTtlMs(), andsetCacheScope()set the list pagination and cache hints for every store assembled fromadd*()entries.getToolStore(),getPromptStore(),getResourceStore(),getResourceTemplateStore(), andgetCompletionStore()expose the assembled stores.setMaxInFlightDispatches()caps concurrent dispatches, andsetServerInfoDisclosure()controls how much identityserver/discoverreveals.ClientBuilder::setClientCapabilities()declares the client's capabilities, carried on every request's_meta.- Tool schemas accept full JSON Schema 2020-12 (SEP-2106): every top-level keyword (
$defs,allOf, conditionals,additionalProperties) is preserved verbatim through listing and validation. - Streamable HTTP examples (
examples/http-server.php,examples/http-client.php) and a dockerised Keycloak end-to-end example (examples/keycloak-e2e/) walking discovery, anonymous Dynamic Client Registration, PKCE, and the token exchange against a real authorization server.
Changed
Client::discover()(server/discover) replaces theinitializehandshake as the way to learn the server's identity and capabilities, and the SDK stamps the per-request_metalifecycle fields (protocol version, client info, client capabilities) on every outbound request.- Every result carries the required
resultTypediscriminator, and cacheable results (ReadResourceResultplus the four list results) require the SEP-2549ttlMsandcacheScopehints. Client::sendRequest()takes the expected response-envelope class, an optionalSendContext, and an optional per-request timeout.- The
ServerRequestmarker interface is renamedInputRequest, and elicitation is remodelled as bareInputRequest/InputResponsebodies ridingInputRequiredResult. The concrete*ResultResponseenvelopes moved to theCore\Schema\ResultResponsenamespace. - A misrouted envelope is decided by its
idalone, never the method name: an id-less envelope is a notification and goes unanswered, an id-carrying one is answered, uniformly across every parse-failure arm. - Reading an unknown resource answers
-32602with the requested URI inerror.data.uri(SEP-2164) instead of an emptycontentslist.
Removed
- The
initialize/notifications/initializedhandshake,Client::initialize(), and handshake-time version negotiation. Sessions are gone with it:TransportInterfacecarries no session id and noMcp-Session-Idheader exists. pingandClient::ping(), removed by the 2026-07-28 revision.- Roots, Sampling, and the Logging emission path, deprecated by SEP-2577 and omitted by this greenfield SDK per SEP-2596:
roots/list,sampling/createMessage,logging/setLevel,Client::setLoggingLevel(),notifications/message, and the matching capability slots. TheLoggingLevelenum survives only to round-trip the deprecated_metalogLevelfield. resources/subscribeandresources/unsubscribe, replaced bysubscriptions/listen.- The task methods from core (they return as the
io.modelcontextprotocol/tasksextension),Tool.executionwith itsToolExecution/TaskSupporttypes, theElicitationCompleteNotification, theelicitationIdfield, and theUrlElicitationRequiredError(-32042) mechanism.
Fixed
- A handler argument-binding failure on an attribute-discovered method is reported as a JSON-RPC
-32602Invalid Params error naming the offending argument, instead of surfacing as an internal error. Resource::$sizeis typedintper the spec'sinteger, andNumberSchemabounds plusElicitResultnumeric content accept floats per the spec'snumber.
Full Changelog: v0.5.0...v0.6.0