v0.11.0
·
206 commits
to 1.x
since this release
Immutable
release. Only release title and notes can be modified.
What's Changed
Closes a family of security defects where peer-supplied bytes reached a renderer unbounded and unescaped, in a JSON-RPC error, a log record, or an exception message. The rest is decode correctness for names and keys the 2026-07-28 schema permits and this SDK refused. Three breaking changes are listed in BREAKING_CHANGES.md.
Added
SafeDisplayis now public API, for bounding and escaping a peer value your own handler quotes back.
Changed
AuthorizedHttpClienttakes anHttpClientBuilderand runs credentialed traffic on a client that follows no redirect, so a hop off the MCP server's origin is refused before the credential travels (see BREAKING_CHANGES.md).JwksAccessTokenValidatortakes the issuer it accepts and refuses a token whoseissis absent or different, or which carries noexp(see BREAKING_CHANGES.md).
Fixed
- Every peer value this SDK quotes back in a JSON-RPC error, a log record, or a client-side exception is bounded and escaped.
error.data.uriandRemoteCallFailedException::$errorstill carry it whole. - A malformed envelope is no longer copied whole into the log record reporting it. The reason and the request id ride the logged exception as before.
- A parsed OAuth scope is held to the RFC 6749
scope-tokengrammar, and a segment that is not one is dropped. - A resource-template variable that percent-decodes out of the segment it matched is refused rather than handed to the reader.
files://%2E%2E%2Fetcresolved wherefiles://../etcnever matched. - A client reconnected to a new transport is no longer driven by the old one.
disconnect()left its five listeners attached, so a stale error, reconnect, or message could still reach the live connection. - A header-mismatch retry no longer re-lists tools without bound. The walk stops on a repeated cursor or at 100 pages, and a tool it never reaches is retried unmirrored rather than with the rejected header.
- A JSON Schema property name made only of digits is decoded rather than refused, and re-encodes as an object. Covers a tool's
inputSchemaandoutputSchema, an elicitation'srequestedSchema, and an elicit result'scontent, whose keys are those same names. An emptycontentnow emits{}too. A JSON array arriving in one of those object-typed slots is normalised to an object rather than refused, sincejson_decodecannot tell it from an object whose names run0…n-1. - A tool or prompt argument name made only of digits is decoded rather than refused, and re-encodes as an object. Covers
tools/callandprompts/getargumentspluscompletion/complete'scontext.arguments, whose names are the schema property names widened above, so a tool declaring such a property can now be called as well as listed.ParameterHeaderValidationMiddlewareno longer drops those arguments before checking them against theMcp-Param-*headers. - A server-assigned
inputRequests/inputResponseskey made only of digits is decoded rather than refused, and re-encodes as an object. The spec puts no format on those keys, so a server numbering them from a counter had its whole multi-round-trip exchange rejected. A JSON array in one of those slots is normalised to an object, for the same reason as above. - A tool's
inputSchemaandoutputSchemaemit an empty sub-schema as{}rather than[], at any nesting depth, so{"type":"object","properties":{}}survives a round trip as valid JSON Schema. An elicitation'srequestedSchemadoes the same, including when it rides on atools/callortasks/getresult, as do an elicit result'scontentand atasks/getresult'sresultanderror. Those last two also take a key made only of digits, matching the rest of the class. - A
_metaname made only of digits is decoded rather than refused.json_decodeturns such a key into a PHP int, which the guard read as a malformed object. A name set running0…n-1stays refused: it decodes identically to a JSON array, so the two cannot be told apart. - A tool can now return an array, string, number or boolean as its structured content, not only an object.
CallToolResultpreviously refused everything but an object on both construction and decode. - A peer's tool, prompt, resource or resource-template name is decoded whatever characters it carries. The SDK still holds its stricter handle format on the names it authors, in
ServerBuilder::addTool()and friends. A client can now also call a tool whose name sits outside that format. - A timestamp carrying any number of fractional-second digits is accepted, so a peer emitting microseconds (Python's
isoformat()) or nanoseconds no longer has the whole payload rejected. Only 0 to 3 digits parsed before. Anything finer than a microsecond is truncated. - An emitted timestamp now carries microseconds rather than milliseconds, so a value survives a round trip.
2026-03-09T12:00:00.500+00:00becomes…12:00:00.500000+00:00, and a sub-millisecond value is no longer flattened to.000. - An ISO 8601 parse failure caused by an overflowed date now names the field that carried it, rather than reporting a bare
The parsed date was invalid. - A client reconnected to a different server no longer answers with the previous server's identity, nor refuses a typed call on the previous server's advertisement.
disconnect()left both in place. - A stdio server no longer loses in-flight responses when the transport is closed explicitly.
Server::run()could return while handlers were still running, and their sends were refused because the transport wentClosedbefore draining. - A stdio transport closed before it was started, or in the same tick it was started, now fires
onDrainbeforeonCloselike every other close path. - A
resources/readhandler receives the client'sinputResponsesandrequestState. Both were parsed from the envelope and then dropped, so a resource could ask for input but never see the answer. BearerAuthenticationMiddlewarerefuses a token whoseexpiresAthas passed. It took the validator's reported expiry on trust, so a custom validator's lapsed token was served. PassexpiryLeewaySecondsto match a validator configured for clock skew, such as one settingJWT::$leeway.- A PSR-7 host whose request body cannot rewind can serve tool calls.
ParameterHeaderValidationMiddlewareconsumed the body while peeking at it, so the transport answered every POST with-32700 Parse error. SecuredHttpEndpointappliesmaxBodyBytesbefore the parameter-header middleware rather than after, so an oversized body is refused without first being buffered and JSON-decoded.- A discovered tool taking a
mixedor untyped parameter is callable. It advertised[]as that property's schema, which is not a JSON Schema, so everytools/callfailed before the executor ran. Toolaccepts boolean sub-schemas ininputSchemaandoutputSchema. One such entry previously failed the wholetools/listpage.
Full Changelog: v0.10.0...v0.11.0