[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:
parent
cbceef06eb
commit
8c06dcecc6
1 changed files with 2443 additions and 248 deletions
93
tests/Unit/Schema/Wire/McpHeaderTest.php
Normal file
93
tests/Unit/Schema/Wire/McpHeaderTest.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Tests\Unit\Schema\Wire;
|
||||
|
||||
use Mcp\Schema\Wire\McpHeader;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* The encoding rules both sides of the wire depend on (SEP-2243).
|
||||
*
|
||||
* The round-trip property is what actually matters: whatever the client wraps,
|
||||
* the server's reader must recover unchanged. A rule that only one side gets
|
||||
* right produces a header mismatch the caller cannot explain.
|
||||
*/
|
||||
final class McpHeaderTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return iterable<string, array{mixed, string}>
|
||||
*/
|
||||
public static function provideValues(): iterable
|
||||
{
|
||||
yield 'plain ascii' => ['us-west1', 'us-west1'];
|
||||
yield 'empty string' => ['', ''];
|
||||
yield 'interior spaces stay plain' => ['us west 1', 'us west 1'];
|
||||
yield 'integer' => [42, '42'];
|
||||
yield 'boolean true' => [true, 'true'];
|
||||
yield 'boolean false' => [false, 'false'];
|
||||
yield 'non-ascii is wrapped' => ['Hello, 世界', '=?base64?SGVsbG8sIOS4lueVjA==?='];
|
||||
yield 'leading space is wrapped' => [' us-west1', '=?base64?IHVzLXdlc3Qx?='];
|
||||
yield 'trailing space is wrapped' => ['us-west1 ', '=?base64?dXMtd2VzdDEg?='];
|
||||
yield 'newline is wrapped' => ["line1\nline2", '=?base64?bGluZTEKbGluZTI=?='];
|
||||
yield 'tab is wrapped' => ["\tindented", '=?base64?CWluZGVudGVk?='];
|
||||
yield 'a literal already shaped like the wrapper is wrapped again' => ['=?base64?SGVsbG8=?=', '=?base64?PT9iYXNlNjQ/U0dWc2JHOD0/PQ==?='];
|
||||
}
|
||||
|
||||
#[DataProvider('provideValues')]
|
||||
#[TestDox('renders a mirrored argument as a header value')]
|
||||
public function testEncode(mixed $value, string $expected): void
|
||||
{
|
||||
$this->assertSame($expected, McpHeader::encode($value));
|
||||
}
|
||||
|
||||
#[DataProvider('provideValues')]
|
||||
#[TestDox('what the client wraps, the server recovers unchanged')]
|
||||
public function testRoundTrip(mixed $value, string $encoded): void
|
||||
{
|
||||
$expected = match (true) {
|
||||
\is_bool($value) => $value ? 'true' : 'false',
|
||||
default => (string) $value,
|
||||
};
|
||||
|
||||
$this->assertSame($expected, McpHeader::decode($encoded));
|
||||
}
|
||||
|
||||
#[TestDox('a value that cannot be mirrored gets no header at all')]
|
||||
public function testUnmirrorableValue(): void
|
||||
{
|
||||
// A float has no single decimal spelling for a receiver to compare
|
||||
// against, which is why SEP-2243 forbids the annotation on `number`.
|
||||
$this->assertNull(McpHeader::encode(3.14159));
|
||||
$this->assertNull(McpHeader::encode(['a']));
|
||||
$this->assertNull(McpHeader::encode(null));
|
||||
}
|
||||
|
||||
#[TestDox('a corrupted wrapper is refused rather than silently decoded')]
|
||||
public function testCorruptWrapperIsRefused(): void
|
||||
{
|
||||
$this->assertNull(McpHeader::decode('=?base64?not valid base64!?='));
|
||||
}
|
||||
|
||||
#[TestDox('names the subject of the methods that address one, and nothing else')]
|
||||
public function testNameFor(): void
|
||||
{
|
||||
$this->assertSame('my-tool', McpHeader::nameFor('tools/call', ['name' => 'my-tool']));
|
||||
$this->assertSame('my-prompt', McpHeader::nameFor('prompts/get', ['name' => 'my-prompt']));
|
||||
$this->assertSame('file:///x', McpHeader::nameFor('resources/read', ['uri' => 'file:///x']));
|
||||
$this->assertSame('t-1', McpHeader::nameFor('tasks/get', ['taskId' => 't-1']));
|
||||
|
||||
$this->assertNull(McpHeader::nameFor('tools/list', []));
|
||||
$this->assertNull(McpHeader::nameFor('tools/call', null));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue