[Client] Speak the 2026-07-28 lifecycle (#456)

* [Client] Speak the 2026-07-28 lifecycle

On that revision the client opens with server/discover instead of
initialize, stamps each request's _meta with the protocol version, its own
capabilities and client info, and sends the standard Mcp-Method / Mcp-Name /
Mcp-Param-* headers an intermediary routes on. ToolCatalog is what knows,
from the tool list, which arguments a call has to mirror.

An input_required result is answered rather than surfaced: InputRequestResolver
asks the host's elicitation, sampling and roots handlers and retries the same
request carrying inputResponses and the requestState the server sent.

McpHeader holds the header names and the =?base64?…?= sentinel, so the two
sides no longer keep their own copies of the same rules.

* Fix CI baseline drift and address Copilot review findings

Drop stale expected-failure entries the new client fixture now passes.

Copilot review: fix McpHeader round-trip for a wrapper-shaped literal,
encode empty/numeric-keyed inputResponses as a JSON object, stop the
incident example repeating side effects on MRTR retry, and reset
ToolCatalog on reconnect instead of carrying the previous server's
verdicts.

* Skip DualEraExampleTestCase on PHP 8.1

PHP_CLI_SERVER_WORKERS does not reliably fork multiple php -S
workers there (php/php-src#9400), reproducing the single-worker
deadlock the extra workers exist to avoid.
This commit is contained in:
Christopher Hertel 2026-08-19 02:41:02 +02:00 • committed by GitHub
commit 8c06dcecc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -13,6 +13,7 @@ namespace Mcp\Server\Stateless;
use Mcp\Capability\RegistryInterface;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Wire\McpHeader;
/**
* Checks that a request's HTTP headers agree with its JSON-RPC body (SEP-2243).
@ -25,13 +26,9 @@ use Mcp\Exception\ToolNotFoundException;
*/
final class StandardHeaderValidator
{
public const METHOD_HEADER = 'Mcp-Method';
public const NAME_HEADER = 'Mcp-Name';
public const PARAM_HEADER_PREFIX = 'Mcp-Param-';
/** Wrapper marking a header value as Base64 of its UTF-8 representation. */
private const BASE64_PREFIX = '=?base64?';
private const BASE64_SUFFIX = '?=';
public const METHOD_HEADER = McpHeader::METHOD;
public const NAME_HEADER = McpHeader::NAME;
public const PARAM_HEADER_PREFIX = McpHeader::PARAM_PREFIX;
public function __construct(
private readonly ?RegistryInterface $registry = null,
@ -113,14 +110,7 @@ final class StandardHeaderValidator
*/
public static function nameFor(string $method, ?array $params): ?string
{
$value = match ($method) {
'tools/call', 'prompts/get' => $params['name'] ?? null,
'resources/read' => $params['uri'] ?? null,
'tasks/get', 'tasks/update', 'tasks/cancel' => $params['taskId'] ?? null,
default => null,
};
return \is_string($value) ? $value : null;
return McpHeader::nameFor($method, $params);
}
/**
@ -285,26 +275,10 @@ final class StandardHeaderValidator
/**
* Unwraps a `=?base64?…?=` value, or returns a plain value unchanged.
*
* Strict: PHP's decoder accepts mispadded input and returns plausible
* bytes, which would turn a corrupted header into a silent mismatch.
* Null when the wrapper is present but its contents are not valid Base64.
*/
public static function decode(string $value): ?string
{
if (!str_starts_with($value, self::BASE64_PREFIX) || !str_ends_with($value, self::BASE64_SUFFIX)) {
return $value;
}
$encoded = substr($value, \strlen(self::BASE64_PREFIX), -\strlen(self::BASE64_SUFFIX));
$decoded = base64_decode($encoded, true);
if (false === $decoded || base64_encode($decoded) !== $encoded) {
return null;
}
return $decoded;
return McpHeader::decode($value);
}
/**