Skip to content
Open
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
44 changes: 43 additions & 1 deletion .github/workflows/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ jobs:
- name: Tests
run: vendor/bin/phpunit --testsuite=inspector

conformance:
conformance-server:
name: conformance / server
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down Expand Up @@ -122,6 +123,47 @@ jobs:
if: always()
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml down

conformance-client:
name: conformance / client
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: "none"

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22'

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

- name: Create log directory
run: mkdir -p tests/Conformance/logs

- name: Run client conformance tests
working-directory: ./tests/Conformance
run: npx @modelcontextprotocol/conformance client --command "php ${{ github.workspace }}/tests/Conformance/client.php" --suite all --expected-failures conformance-baseline.yml

- name: Show logs on failure
if: failure()
run: |
echo "=== Client Conformance Log ==="
cat tests/Conformance/logs/client-conformance.log 2>/dev/null || echo "No client conformance log found"
echo ""
echo "=== Test Results ==="
find tests/Conformance/results -name "checks.json" 2>/dev/null | head -3 | while read f; do
echo "--- $f ---"
cat "$f"
echo ""
done || echo "No results found"

qa:
runs-on: ubuntu-latest
steps:
Expand Down
9 changes: 7 additions & 2 deletions 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 docs
.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

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

conformance-tests:
conformance-tests: conformance-server conformance-client

conformance-server:
docker compose -f tests/Conformance/Fixtures/docker-compose.yml up -d
@echo "Waiting for server to start..."
@sleep 5
cd tests/Conformance && npx @modelcontextprotocol/conformance server --url http://localhost:8000/ || true
docker compose -f tests/Conformance/Fixtures/docker-compose.yml down

conformance-client:
cd tests/Conformance && npx @modelcontextprotocol/conformance client --command "php $(CURDIR)/tests/Conformance/client.php" --suite all --expected-failures conformance-baseline.yml || true

coverage:
XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit --coverage-html=coverage

Expand Down
6 changes: 3 additions & 3 deletions src/Schema/ClientCapabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ public static function fromArray(array $data): self
* sampling?: object,
* elicitation?: object,
* experimental?: object,
* }
* }|\stdClass
*/
public function jsonSerialize(): array
public function jsonSerialize(): array|object
{
$data = [];
if ($this->roots || $this->rootsListChanged) {
Expand All @@ -102,6 +102,6 @@ public function jsonSerialize(): array
$data['experimental'] = (object) $this->experimental;
}

return $data;
return $data ?: new \stdClass();
}
}
2 changes: 1 addition & 1 deletion src/Schema/Request/CallToolRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ protected function getParams(): array
{
return [
'name' => $this->name,
'arguments' => $this->arguments,
'arguments' => $this->arguments ?: new \stdClass(),
];
}
}
108 changes: 108 additions & 0 deletions tests/Conformance/client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?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.
*/

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

use Mcp\Client;
use Mcp\Client\Handler\Request\RequestHandlerInterface;
use Mcp\Client\Transport\HttpTransport;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Enum\ElicitAction;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ElicitRequest;
use Mcp\Schema\Result\ElicitResult;
use Mcp\Tests\Conformance\FileLogger;

$url = $argv[1] ?? null;
$scenario = getenv('MCP_CONFORMANCE_SCENARIO') ?: null;

if (!$url || !$scenario) {
fwrite(\STDERR, "Usage: MCP_CONFORMANCE_SCENARIO=<scenario> php client.php <server-url>\n");
exit(1);
}

@mkdir(__DIR__.'/logs', 0777, true);
$logger = new FileLogger(__DIR__.'/logs/client-conformance.log', true);
$logger->info(sprintf('Starting client conformance test: scenario=%s, url=%s', $scenario, $url));

$builder = Client::builder()
->setClientInfo('mcp-conformance-test-client', '1.0.0')
->setInitTimeout(30)
->setRequestTimeout(60)
->setLogger($logger);

if ('elicitation-sep1034-client-defaults' === $scenario) {
$builder->setCapabilities(new ClientCapabilities(elicitation: true));
$builder->addRequestHandler(new class($logger) implements RequestHandlerInterface {
public function __construct(private readonly Psr\Log\LoggerInterface $logger)
{
}

public function supports(Request $request): bool
{
return $request instanceof ElicitRequest;
}

public function handle(Request $request): Response
{
$this->logger->info('Received elicitation request, accepting with empty content');

return new Response($request->getId(), new ElicitResult(ElicitAction::Accept, []));
}
});
}

$client = $builder->build();
$transport = new HttpTransport($url, logger: $logger);

try {
$client->connect($transport);
$logger->info('Connected to server');

$toolsResult = $client->listTools();
$logger->info(sprintf('Listed %d tools', count($toolsResult->tools)));

switch ($scenario) {
case 'initialize':
break;

case 'tools_call':
$toolName = $toolsResult->tools[0]->name ?? 'test-tool';
$client->callTool($toolName, []);
$logger->info(sprintf('Called tool: %s', $toolName));
break;

case 'elicitation-sep1034-client-defaults':
$toolName = $toolsResult->tools[0]->name ?? 'test_client_elicitation_defaults';
$client->callTool($toolName, []);
$logger->info(sprintf('Called tool: %s', $toolName));
break;

default:
$logger->warning(sprintf('Unknown scenario: %s', $scenario));
break;
}

$client->disconnect();
$logger->info('Disconnected');
exit(0);
} catch (Throwable $e) {
$logger->error(sprintf('Error: %s', $e->getMessage()));
fwrite(\STDERR, sprintf("Error: %s\n%s\n", $e->getMessage(), $e->getTraceAsString()));

try {
$client->disconnect();
} catch (Throwable $ignored) {
}

exit(1);
}
25 changes: 25 additions & 0 deletions tests/Conformance/conformance-baseline.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
server:
- dns-rebinding-protection

client:
- elicitation-sep1034-client-defaults
- sse-retry
- auth/metadata-default
- auth/metadata-var1
- auth/metadata-var2
- auth/metadata-var3
- auth/basic-cimd
- auth/scope-from-www-authenticate
- auth/scope-from-scopes-supported
- auth/scope-omitted-when-undefined
- auth/scope-step-up
- auth/scope-retry-limit
- auth/token-endpoint-auth-basic
- auth/token-endpoint-auth-post
- auth/token-endpoint-auth-none
- auth/pre-registration
- auth/2025-03-26-oauth-metadata-backcompat
- auth/2025-03-26-oauth-endpoint-fallback
- auth/offline-access-scope
- auth/offline-access-not-supported
- auth/client-credentials-jwt
- auth/client-credentials-basic
- auth/cross-app-access-complete-flow

4 changes: 2 additions & 2 deletions tests/Unit/Server/Handler/Request/InitializeHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ public function testHandleUsesConfigurationProtocolVersion(): void
$session = $this->createMock(SessionInterface::class);
$session->expects($this->exactly(2))
->method('set')
->willReturnCallback(function (string $key, array $value): void {
->willReturnCallback(function (string $key, mixed $value): void {
match ($key) {
'client_info' => $this->assertSame(['name' => 'client-app', 'version' => '1.0.0'], $value),
'client_capabilities' => $this->assertSame([], $value),
'client_capabilities' => $this->assertEquals(new \stdClass(), $value),
default => $this->fail("Unexpected session key: {$key}"),
};
});
Expand Down
Loading