php-sdk/tests/Integration/SamplingTest.php

121 lines
4.7 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\Client\Builder as ClientBuilder;
use Mcp\Client\Handler\Request\SamplingCallbackInterface;
use Mcp\Client\Handler\Request\SamplingRequestHandler;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\Role;
use Mcp\Schema\Request\CreateSamplingMessageRequest;
use Mcp\Schema\Result\CreateSamplingMessageResult;
use PHPUnit\Framework\Attributes\TestDox;
/**
* Sampling: the server borrowing the client's model mid-tool-call.
*
* @see Fixture/sampling.php for the server under test
*/
final class SamplingTest extends IntegrationTestCase
{
#[TestDox('the sampled completion reaches the tool that asked for it')]
public function testSampledCompletionReachesTheTool(): void
{
$client = $this->connect('sampling', $this->clientSampling());
$result = $client->callTool('summarize', ['text' => 'a long report']);
$this->assertInstanceOf(TextContent::class, $result->content[0]);
$this->assertSame('test-model said: a long report', $result->content[0]->text);
}
#[TestDox('the prompt the tool passed arrives at the client')]
public function testPromptReachesTheClient(): void
{
// The client stays in this process, so what it was asked can be
// collected by reference even though the tool asking runs in another.
/** @var \ArrayObject<int, CreateSamplingMessageRequest> $seen */
$seen = new \ArrayObject();
$client = $this->connect('sampling', $this->clientSampling($seen));
$client->callTool('summarize', ['text' => 'inspect me']);
$this->assertCount(1, $seen);
$this->assertInstanceOf(TextContent::class, $seen[0]->messages[0]->content);
$this->assertSame('inspect me', $seen[0]->messages[0]->content->text);
$this->assertSame(64, $seen[0]->maxTokens);
}
#[TestDox('a gateway parameter is injected, not published in the schema')]
public function testGatewayParameterIsInjectedNotPublished(): void
{
$client = $this->connect('sampling', $this->clientSampling());
$tool = null;
foreach ($client->listTools()->tools as $candidate) {
if ('summarize_via_gateway' === $candidate->name) {
$tool = $candidate;
}
}
$this->assertNotNull($tool);
$this->assertArrayNotHasKey('client', $tool->inputSchema['properties']);
$this->assertArrayHasKey('text', $tool->inputSchema['properties']);
$this->assertSame(['text'], $tool->inputSchema['required']);
$result = $client->callTool('summarize_via_gateway', ['text' => 'a long report']);
$this->assertInstanceOf(TextContent::class, $result->content[0]);
$this->assertSame('test-model said: a long report', $result->content[0]->text);
}
#[TestDox('a client that cannot sample refuses instead of stalling the tool')]
public function testClientWithoutSamplingRefuses(): void
{
// The gateway has no supportsSampling() to consult, so the tool finds
// out by asking and the refusal surfaces as a ClientException.
$client = $this->connect('sampling');
$result = $client->callTool('summarize', ['text' => 'anything']);
$this->assertInstanceOf(TextContent::class, $result->content[0]);
$this->assertSame('Client does not handle "sampling/createMessage" requests.', $result->content[0]->text);
}
/**
* @param \ArrayObject<int, CreateSamplingMessageRequest>|null $seen collects what the server asked for
*/
private function clientSampling(?\ArrayObject $seen = null): ClientBuilder
{
$callback = new class($seen ?? new \ArrayObject()) implements SamplingCallbackInterface {
/** @param \ArrayObject<int, CreateSamplingMessageRequest> $seen */
public function __construct(private readonly \ArrayObject $seen)
{
}
public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult
{
$this->seen[] = $request;
$prompt = $request->messages[0]->content;
\assert($prompt instanceof TextContent);
return new CreateSamplingMessageResult(Role::Assistant, new TextContent($prompt->text), 'test-model');
}
};
return $this->clientBuilder()
->setCapabilities(new ClientCapabilities(sampling: true))
->addRequestHandler(new SamplingRequestHandler($callback));
}
}