Skip to content

v0.6.0

Latest

Choose a tag to compare

@paulbalandan paulbalandan released this 03 Aug 19:21
v0.6.0
94e1f23

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. StreamableHttpServerTransport is a PSR-15 handler, and SecuredHttpEndpoint wraps it in the recommended middleware: CORS for browser clients, DNS-rebinding Origin protection with opt-in Host allow-listing, a request-body size limit answering 413, 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, and Server::listen() attaches the dispatcher without blocking, for hosts that drive the transport per request.
  • The Streamable HTTP client transport. StreamableHttpClientTransport POSTs one envelope per exchange, parses SSE responses frame by frame so progress notifications arrive mid-call, mirrors x-mcp-header tool arguments into Mcp-Param-{Name} headers, retries a call once after a HeaderMismatch answer 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 via ClientBuilder::setRequestTimeout() / setMaxRequestTimeout(). An exchange whose answer cannot settle the request it carries fails it with UnexpectedHttpStatusException instead of leaving the caller waiting.
  • The OAuth 2.1 client half. AuthorizedHttpClient decorates the HTTP client the transport already takes: on a 401 it 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-supplied UserAuthorizationInterface, validates the response iss, binds the token to the MCP server it was issued for, and replays the request. Scope selection follows the challenge, then scopes_supported, then the caller's defaultScopes. An insufficient-scope answer steps the scopes up under a capped budget or is reported via InsufficientScopeException per InsufficientScopePolicy. offline_access and refresh tokens are opt-in. Tokens and client registrations persist through TokenStoreInterface / ClientRegistrationStoreInterface, and everything that writes a token is serialised behind one cancellable lock.
  • The OAuth 2.1 resource-server half. BearerAuthenticationMiddleware validates bearer tokens through an AccessTokenValidatorInterface, binds the token audience to the server's canonical URI, and enforces required scopes. ProtectedResourceMetadataHandler serves the RFC 9728 metadata document at its two well-known paths. The validated VerifiedAccessToken reaches handlers as $context->receiveContext->authInfo.
  • JwksAccessTokenValidator, a shipped JWT validator riding the suggested firebase/php-jwt package. 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 throws MissingSuggestedDependencyException naming the install command.
  • subscriptions/listen on both sides. Client::listen(SubscriptionFilter, \Closure) opens a notification stream and returns a SubscriptionStream handle. The server serves it from a SubscriptionStore registered via ServerBuilder::setSubscriptionStore(). The built-in stores became runtime-mutable (addTool() / removeTool() and siblings on the mutable store interfaces), fire list_changed notifications onto open streams, and the matching listChanged capability flags are advertised only when genuinely deliverable.
  • The input-required flow (multi round-trip requests). callTool(), readResource(), and getPrompt() can answer with an InputRequiredResult describing the input the server needs, and the caller answers by calling again with inputResponses: and the echoed requestState:. Server-side executors, renderers, and readers may return InputRequiredResult to ask.
  • Completion registration grew a fluent surface and attribute sugar: ServerBuilder::addPromptCompletion() / addResourceTemplateCompletion() take a closure or a CompletionProviderInterface per argument, and #[AsCompletion] (repeatable) marks a method as a provider discovered through ServerBuilder::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 _meta protocol version is gated, and the new protocol error codes (-32020 HeaderMismatch, -32021 MissingRequiredClientCapability, -32022 UnsupportedProtocolVersion) are modelled on ProtocolErrorCode.
  • ServerBuilder configuration: setPageSize(), setTtlMs(), and setCacheScope() set the list pagination and cache hints for every store assembled from add*() entries. getToolStore(), getPromptStore(), getResourceStore(), getResourceTemplateStore(), and getCompletionStore() expose the assembled stores. setMaxInFlightDispatches() caps concurrent dispatches, and setServerInfoDisclosure() controls how much identity server/discover reveals.
  • 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 the initialize handshake as the way to learn the server's identity and capabilities, and the SDK stamps the per-request _meta lifecycle fields (protocol version, client info, client capabilities) on every outbound request.
  • Every result carries the required resultType discriminator, and cacheable results (ReadResourceResult plus the four list results) require the SEP-2549 ttlMs and cacheScope hints.
  • Client::sendRequest() takes the expected response-envelope class, an optional SendContext, and an optional per-request timeout.
  • The ServerRequest marker interface is renamed InputRequest, and elicitation is remodelled as bare InputRequest / InputResponse bodies riding InputRequiredResult. The concrete *ResultResponse envelopes moved to the Core\Schema\ResultResponse namespace.
  • A misrouted envelope is decided by its id alone, 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 -32602 with the requested URI in error.data.uri (SEP-2164) instead of an empty contents list.

Removed

  • The initialize / notifications/initialized handshake, Client::initialize(), and handshake-time version negotiation. Sessions are gone with it: TransportInterface carries no session id and no Mcp-Session-Id header exists.
  • ping and Client::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. The LoggingLevel enum survives only to round-trip the deprecated _meta logLevel field.
  • resources/subscribe and resources/unsubscribe, replaced by subscriptions/listen.
  • The task methods from core (they return as the io.modelcontextprotocol/tasks extension), Tool.execution with its ToolExecution / TaskSupport types, the ElicitationCompleteNotification, the elicitationId field, and the UrlElicitationRequiredError (-32042) mechanism.

Fixed

  • A handler argument-binding failure on an attribute-discovered method is reported as a JSON-RPC -32602 Invalid Params error naming the offending argument, instead of surfacing as an internal error.
  • Resource::$size is typed int per the spec's integer, and NumberSchema bounds plus ElicitResult numeric content accept floats per the spec's number.

Full Changelog: v0.5.0...v0.6.0