Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ jobs:
- name: Tests
run: vendor/bin/phpunit --testsuite=unit

integration:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4', '8.5']

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: "none"

- name: Install Composer
uses: "ramsey/composer-install@v4"

- name: Tests
run: vendor/bin/phpunit --testsuite=integration

inspector:
runs-on: ubuntu-latest
steps:
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: deps-stable deps-low cs phpstan tests unit-tests inspector-tests coverage ci ci-stable ci-lowest conformance-tests conformance-server conformance-client docs
.PHONY: deps-stable deps-low cs phpstan tests unit-tests integration-tests inspector-tests coverage ci ci-stable ci-lowest conformance-tests conformance-server conformance-client docs

deps-stable:
composer update --prefer-stable
Expand All @@ -18,6 +18,9 @@ tests:
unit-tests:
vendor/bin/phpunit --testsuite=unit

integration-tests:
vendor/bin/phpunit --testsuite=integration

inspector-tests:
vendor/bin/phpunit --testsuite=inspector

Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="examples">
<directory>examples/server/oauth-microsoft/tests</directory>
</testsuite>
Expand Down
107 changes: 107 additions & 0 deletions tests/Integration/ElicitationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?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\ElicitationCallbackInterface;
use Mcp\Client\Handler\Request\ElicitationRequestHandler;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\ElicitAction;
use Mcp\Schema\Request\ElicitRequest;
use Mcp\Schema\Result\ElicitResult;
use PHPUnit\Framework\Attributes\TestDox;

/**
* Elicitation, driven all the way around the loop.
*
* The round-trip that suspends both sides at once: the tool's Fiber waits on
* the client while the client's request Fiber waits on the tool.
*
* @see Fixture/elicitation.php for the server under test
*/
final class ElicitationTest extends IntegrationTestCase
{
#[TestDox('an accepted elicitation hands the content back to the tool')]
public function testAcceptedElicitation(): void
{
$client = $this->connect('elicitation', $this->clientAnswering(
new ElicitResult(ElicitAction::Accept, ['name' => 'Ada']),
));

$result = $client->callTool('ask_name');

$this->assertFalse($result->isError);
$this->assertInstanceOf(TextContent::class, $result->content[0]);
$this->assertSame('accept:Ada', $result->content[0]->text);
}

#[TestDox('a declined elicitation reaches the tool as a decline, not an error')]
public function testDeclinedElicitation(): void
{
$client = $this->connect('elicitation', $this->clientAnswering(
new ElicitResult(ElicitAction::Decline),
));

$result = $client->callTool('ask_name');

$this->assertInstanceOf(TextContent::class, $result->content[0]);
$this->assertSame('decline:', $result->content[0]->text);
}

#[TestDox('a client that does not advertise elicitation is not asked')]
public function testCapabilityIsVisibleToTheServer(): void
{
// The tool consults supportsElicitation(), which answers from the
// capabilities this client sent during the handshake.
$client = $this->connect('elicitation');

$result = $client->callTool('ask_name');

$this->assertInstanceOf(TextContent::class, $result->content[0]);
$this->assertSame('unsupported', $result->content[0]->text);
}

#[TestDox('a client advertising elicitation without a handler fails the tool call')]
public function testAdvertisedCapabilityWithoutHandler(): void
{
// The client answers "method not found", which the gateway raises inside
// the tool as a ClientException rather than leaving it waiting.
$client = $this->connect(
'elicitation',
$this->clientBuilder()->setCapabilities(new ClientCapabilities(elicitation: true)),
);

$result = $client->callTool('ask_name');

$this->assertInstanceOf(TextContent::class, $result->content[0]);
$this->assertSame('Client does not handle "elicitation/create" requests.', $result->content[0]->text);
}

private function clientAnswering(ElicitResult $answer): ClientBuilder
{
$callback = new class($answer) implements ElicitationCallbackInterface {
public function __construct(private readonly ElicitResult $answer)
{
}

public function __invoke(ElicitRequest $request): ElicitResult
{
return $this->answer;
}
};

return $this->clientBuilder()
->setCapabilities(new ClientCapabilities(elicitation: true))
->addRequestHandler(new ElicitationRequestHandler($callback));
}
}
49 changes: 49 additions & 0 deletions tests/Integration/Fixture/elicitation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?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.
*/

/*
* Server for {@see \Mcp\Tests\Integration\ElicitationTest}.
*/

use Mcp\Exception\ClientException;
use Mcp\Schema\Elicitation\ElicitationSchema;
use Mcp\Schema\Elicitation\StringSchemaDefinition;
use Mcp\Server;
use Mcp\Server\RequestContext;
use Mcp\Server\Transport\StdioTransport;

require_once dirname(__DIR__, 3).'/vendor/autoload.php';

Server::builder()
->setServerInfo('integration-server', '1.0.0')
->addTool(
static function (RequestContext $context): string {
$gateway = $context->getClientGateway();

if (!$gateway->supportsElicitation()) {
return 'unsupported';
}

try {
$result = $gateway->elicit('What is your name?', new ElicitationSchema([
'name' => new StringSchemaDefinition(title: 'Name'),
]));
} catch (ClientException $e) {
return $e->getMessage();
}

return sprintf('%s:%s', $result->action->value, $result->content['name'] ?? '');
},
name: 'ask_name',
description: 'Asks the client for a name.',
)
->build()
->run(new StdioTransport());
32 changes: 32 additions & 0 deletions tests/Integration/Fixture/handshake.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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.
*/

/*
* Server for {@see \Mcp\Tests\Integration\HandshakeTest}.
*
* The test pins the revision through the environment; unset negotiates freely.
*/

use Mcp\Schema\Enum\ProtocolVersion;
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;

require_once dirname(__DIR__, 3).'/vendor/autoload.php';

$builder = Server::builder()
->setServerInfo('integration-server', '1.0.0')
->setInstructions('Be brief.');

if (is_string($pinned = getenv('MCP_INTEGRATION_PROTOCOL_VERSION')) && '' !== $pinned) {
$builder->setProtocolVersion(ProtocolVersion::from($pinned));
}

$builder->build()->run(new StdioTransport());
39 changes: 39 additions & 0 deletions tests/Integration/Fixture/notification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?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.
*/

/*
* Server for {@see \Mcp\Tests\Integration\NotificationTest}.
*/

use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Server;
use Mcp\Server\RequestContext;
use Mcp\Server\Transport\StdioTransport;

require_once dirname(__DIR__, 3).'/vendor/autoload.php';

Server::builder()
->setServerInfo('integration-server', '1.0.0')
->addTool(
static function (RequestContext $context): string {
$gateway = $context->getClientGateway();

$gateway->log(LoggingLevel::Info, 'starting work');
$gateway->progress(0.5, 1.0, 'halfway');
$gateway->progress(1.0, 1.0, 'done');

return 'finished';
},
name: 'work',
description: 'Reports progress and logs while working.',
)
->build()
->run(new StdioTransport());
43 changes: 43 additions & 0 deletions tests/Integration/Fixture/roots.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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.
*/

/*
* Server for {@see \Mcp\Tests\Integration\RootsTest}.
*/

use Mcp\Server;
use Mcp\Server\RequestContext;
use Mcp\Server\Transport\StdioTransport;

require_once dirname(__DIR__, 3).'/vendor/autoload.php';

Server::builder()
->setServerInfo('integration-server', '1.0.0')
->addTool(
static function (RequestContext $context): string {
$gateway = $context->getClientGateway();

if (!$gateway->supportsRoots()) {
return 'unsupported';
}

$described = [];
foreach ($gateway->listRoots()->roots as $root) {
$described[] = sprintf('%s (%s)', $root->uri, $root->name ?? '-');
}

return implode(', ', $described);
},
name: 'inspect_roots',
description: 'Reports the workspace roots the client exposes.',
)
->build()
->run(new StdioTransport());
42 changes: 42 additions & 0 deletions tests/Integration/Fixture/sampling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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.
*/

/*
* Server for {@see \Mcp\Tests\Integration\SamplingTest}.
*/

use Mcp\Exception\ClientException;
use Mcp\Schema\Content\TextContent;
use Mcp\Server;
use Mcp\Server\RequestContext;
use Mcp\Server\Transport\StdioTransport;

require_once dirname(__DIR__, 3).'/vendor/autoload.php';

Server::builder()
->setServerInfo('integration-server', '1.0.0')
->addTool(
static function (RequestContext $context, string $text): string {
try {
$result = $context->getClientGateway()->sample($text, maxTokens: 64);
} catch (ClientException $e) {
return $e->getMessage();
}

assert($result->content instanceof TextContent);

return sprintf('%s said: %s', $result->model, $result->content->text);
},
name: 'summarize',
description: 'Summarizes text by asking the client to sample.',
)
->build()
->run(new StdioTransport());
Loading