php-sdk/tests/Integration/HandshakeTest.php
Christopher Hertel 7b12dc4010
[Tests] Introduce client-server integration tests (#419)
* [Tests] Introduce client-server integration tests

* Add an in-process loopback that connects a real client transport to a real
  server transport, draining both Fiber loops in one synchronous pass instead
  of polling. A stalled exchange fails where it happened rather than hanging.
* Cover protocol version negotiation across both implementations, where the
  unit tests only ever drive one side against a canned counter-offer.
* Cover the server-to-client round-trips end to end: elicitation, sampling,
  roots, plus progress and logging notifications.
* Add the `integration` test suite and a `make integration-tests` target.

* [Tests] Run the integration suite in CI

The unit job pins `--testsuite=unit`, so the new suite would never have run on
GitHub. It goes in that job rather than its own: the loopback leans on Fibers,
and the matrix is what covers them down to the PHP 8.1 floor.

* [Tests] Run integration tests against a real server process

Replaces the in-process loopback with the setup from examples/client: the
client spawns a fixture server over the real StdioTransport, so the tests
exercise the shipped transports and no longer nest both sides' Fibers.

Moves the suite into its own CI job, matrixed over PHP 8.1-8.5.
2026-08-15 02:24:48 +02:00

85 lines
3.4 KiB
PHP

<?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\Integration;
use Mcp\Schema\Enum\ProtocolVersion;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\TestDox;
/**
* What the two sides settle on before anything else can happen.
*
* @see Fixture/handshake.php for the server under test
*/
final class HandshakeTest extends IntegrationTestCase
{
#[TestDox('client and server agree on a revision')]
#[DataProvider('provideNegotiations')]
public function testNegotiatedVersion(?ProtocolVersion $clientVersion, ?ProtocolVersion $serverVersion, ProtocolVersion $expected): void
{
$client = $this->clientBuilder();
if (null !== $clientVersion) {
$client->setProtocolVersion($clientVersion);
}
$connected = $this->connect(
'handshake',
$client,
null !== $serverVersion ? ['MCP_INTEGRATION_PROTOCOL_VERSION' => $serverVersion->value] : [],
);
$this->assertSame($expected, $connected->getProtocolVersion());
}
/**
* @return iterable<string, array{?ProtocolVersion, ?ProtocolVersion, ProtocolVersion}>
*/
public static function provideNegotiations(): iterable
{
$latest = ProtocolVersion::latestHandshake();
yield 'both unconfigured' => [null, null, $latest];
// Whichever end of the supported range it sits at.
foreach (ProtocolVersion::handshakeVersions() as $version) {
yield \sprintf('client asks for %s', $version->value) => [$version, null, $version];
}
// A pinned server answers with its pin, and the client continues on it.
yield 'server pins an older revision' => [ProtocolVersion::V2025_11_25, ProtocolVersion::V2025_03_26, ProtocolVersion::V2025_03_26];
yield 'server pins a newer revision' => [ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_11_25, ProtocolVersion::V2025_11_25];
yield 'both pin the same revision' => [ProtocolVersion::V2025_06_18, ProtocolVersion::V2025_06_18, ProtocolVersion::V2025_06_18];
// Neither side reaches the modern era through `initialize`, so
// configuring it falls back to the handshake set on both ends.
yield 'client configured modern' => [ProtocolVersion::V2026_07_28, null, $latest];
yield 'server configured modern' => [ProtocolVersion::V2025_06_18, ProtocolVersion::V2026_07_28, ProtocolVersion::V2025_06_18];
yield 'both configured modern' => [ProtocolVersion::V2026_07_28, ProtocolVersion::V2026_07_28, $latest];
}
#[TestDox('the handshake carries the server identity to the client')]
public function testServerInfoIsExchanged(): void
{
$client = $this->connect('handshake');
$this->assertSame('integration-server', $client->getServerInfo()->name);
$this->assertSame('1.0.0', $client->getServerInfo()->version);
$this->assertSame('Be brief.', $client->getInstructions());
$this->assertTrue($client->isConnected());
}
#[TestDox('the negotiated revision is unset before the handshake')]
public function testProtocolVersionIsNullBeforeConnecting(): void
{
$this->assertNull($this->clientBuilder()->build()->getProtocolVersion());
}
}