Skip to content

Commit e2f8f6d

Browse files
chr-hertelclaude
andcommitted
[Client] add client conformance test suite
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 85fddf5 commit e2f8f6d

7 files changed

Lines changed: 183 additions & 7 deletions

File tree

.github/workflows/pipeline.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,46 @@ jobs:
122122
if: always()
123123
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
124124

125+
client-conformance:
126+
runs-on: ubuntu-latest
127+
steps:
128+
- name: Checkout
129+
uses: actions/checkout@v6
130+
131+
- name: Setup PHP
132+
uses: shivammathur/setup-php@v2
133+
with:
134+
php-version: '8.4'
135+
coverage: "none"
136+
137+
- name: Setup Node
138+
uses: actions/setup-node@v6
139+
with:
140+
node-version: '22'
141+
142+
- name: Install Composer
143+
uses: "ramsey/composer-install@v4"
144+
145+
- name: Create log directory
146+
run: mkdir -p tests/Conformance/logs
147+
148+
- name: Run client conformance tests
149+
working-directory: ./tests/Conformance
150+
run: npx @modelcontextprotocol/conformance client --command "php ${{ github.workspace }}/tests/Conformance/client.php" --suite all --expected-failures conformance-baseline.yml
151+
152+
- name: Show logs on failure
153+
if: failure()
154+
run: |
155+
echo "=== Client Conformance Log ==="
156+
cat tests/Conformance/logs/client-conformance.log 2>/dev/null || echo "No client conformance log found"
157+
echo ""
158+
echo "=== Test Results ==="
159+
find tests/Conformance/results -name "checks.json" 2>/dev/null | head -3 | while read f; do
160+
echo "--- $f ---"
161+
cat "$f"
162+
echo ""
163+
done || echo "No results found"
164+
125165
qa:
126166
runs-on: ubuntu-latest
127167
steps:

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: deps-stable deps-low cs phpstan tests unit-tests inspector-tests coverage ci ci-stable ci-lowest conformance-tests docs
1+
.PHONY: deps-stable deps-low cs phpstan tests unit-tests inspector-tests coverage ci ci-stable ci-lowest conformance-tests client-conformance-tests docs
22

33
deps-stable:
44
composer update --prefer-stable
@@ -28,6 +28,9 @@ conformance-tests:
2828
cd tests/Conformance && npx @modelcontextprotocol/conformance server --url http://localhost:8000/ || true
2929
docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
3030

31+
client-conformance-tests:
32+
cd tests/Conformance && npx @modelcontextprotocol/conformance client --command "php $(CURDIR)/tests/Conformance/client.php" --suite all --expected-failures conformance-baseline.yml || true
33+
3134
coverage:
3235
XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit --coverage-html=coverage
3336

src/Schema/ClientCapabilities.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ public static function fromArray(array $data): self
7878
* sampling?: object,
7979
* elicitation?: object,
8080
* experimental?: object,
81-
* }
81+
* }|\stdClass
8282
*/
83-
public function jsonSerialize(): array
83+
public function jsonSerialize(): array|object
8484
{
8585
$data = [];
8686
if ($this->roots || $this->rootsListChanged) {
@@ -102,6 +102,6 @@ public function jsonSerialize(): array
102102
$data['experimental'] = (object) $this->experimental;
103103
}
104104

105-
return $data;
105+
return $data ?: new \stdClass();
106106
}
107107
}

src/Schema/Request/CallToolRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ protected function getParams(): array
6565
{
6666
return [
6767
'name' => $this->name,
68-
'arguments' => $this->arguments,
68+
'arguments' => $this->arguments ?: new \stdClass(),
6969
];
7070
}
7171
}

tests/Conformance/client.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
13+
14+
use Mcp\Client;
15+
use Mcp\Client\Handler\Request\RequestHandlerInterface;
16+
use Mcp\Client\Transport\HttpTransport;
17+
use Mcp\Schema\ClientCapabilities;
18+
use Mcp\Schema\Enum\ElicitAction;
19+
use Mcp\Schema\JsonRpc\Request;
20+
use Mcp\Schema\JsonRpc\Response;
21+
use Mcp\Schema\Request\ElicitRequest;
22+
use Mcp\Schema\Result\ElicitResult;
23+
use Mcp\Tests\Conformance\FileLogger;
24+
25+
$url = $argv[1] ?? null;
26+
$scenario = getenv('MCP_CONFORMANCE_SCENARIO') ?: null;
27+
28+
if (!$url || !$scenario) {
29+
fwrite(\STDERR, "Usage: MCP_CONFORMANCE_SCENARIO=<scenario> php client.php <server-url>\n");
30+
exit(1);
31+
}
32+
33+
@mkdir(__DIR__.'/logs', 0777, true);
34+
$logger = new FileLogger(__DIR__.'/logs/client-conformance.log', true);
35+
$logger->info(sprintf('Starting client conformance test: scenario=%s, url=%s', $scenario, $url));
36+
37+
$builder = Client::builder()
38+
->setClientInfo('mcp-conformance-test-client', '1.0.0')
39+
->setInitTimeout(30)
40+
->setRequestTimeout(60)
41+
->setLogger($logger);
42+
43+
if ('elicitation-sep1034-client-defaults' === $scenario) {
44+
$builder->setCapabilities(new ClientCapabilities(elicitation: true));
45+
$builder->addRequestHandler(new class($logger) implements RequestHandlerInterface {
46+
public function __construct(private readonly Psr\Log\LoggerInterface $logger)
47+
{
48+
}
49+
50+
public function supports(Request $request): bool
51+
{
52+
return $request instanceof ElicitRequest;
53+
}
54+
55+
public function handle(Request $request): Response
56+
{
57+
$this->logger->info('Received elicitation request, accepting with empty content');
58+
59+
return new Response($request->getId(), new ElicitResult(ElicitAction::Accept, []));
60+
}
61+
});
62+
}
63+
64+
$client = $builder->build();
65+
$transport = new HttpTransport($url, logger: $logger);
66+
67+
try {
68+
$client->connect($transport);
69+
$logger->info('Connected to server');
70+
71+
$toolsResult = $client->listTools();
72+
$logger->info(sprintf('Listed %d tools', count($toolsResult->tools)));
73+
74+
switch ($scenario) {
75+
case 'initialize':
76+
break;
77+
78+
case 'tools_call':
79+
$toolName = $toolsResult->tools[0]->name ?? 'test-tool';
80+
$client->callTool($toolName, []);
81+
$logger->info(sprintf('Called tool: %s', $toolName));
82+
break;
83+
84+
case 'elicitation-sep1034-client-defaults':
85+
$toolName = $toolsResult->tools[0]->name ?? 'test_client_elicitation_defaults';
86+
$client->callTool($toolName, []);
87+
$logger->info(sprintf('Called tool: %s', $toolName));
88+
break;
89+
90+
default:
91+
$logger->warning(sprintf('Unknown scenario: %s', $scenario));
92+
break;
93+
}
94+
95+
$client->disconnect();
96+
$logger->info('Disconnected');
97+
exit(0);
98+
} catch (Throwable $e) {
99+
$logger->error(sprintf('Error: %s', $e->getMessage()));
100+
fwrite(\STDERR, sprintf("Error: %s\n%s\n", $e->getMessage(), $e->getTraceAsString()));
101+
102+
try {
103+
$client->disconnect();
104+
} catch (Throwable $ignored) {
105+
}
106+
107+
exit(1);
108+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
11
server:
22
- dns-rebinding-protection
33

4+
client:
5+
- elicitation-sep1034-client-defaults
6+
- sse-retry
7+
- auth/metadata-default
8+
- auth/metadata-var1
9+
- auth/metadata-var2
10+
- auth/metadata-var3
11+
- auth/basic-cimd
12+
- auth/scope-from-www-authenticate
13+
- auth/scope-from-scopes-supported
14+
- auth/scope-omitted-when-undefined
15+
- auth/scope-step-up
16+
- auth/scope-retry-limit
17+
- auth/token-endpoint-auth-basic
18+
- auth/token-endpoint-auth-post
19+
- auth/token-endpoint-auth-none
20+
- auth/pre-registration
21+
- auth/2025-03-26-oauth-metadata-backcompat
22+
- auth/2025-03-26-oauth-endpoint-fallback
23+
- auth/offline-access-scope
24+
- auth/offline-access-not-supported
25+
- auth/client-credentials-jwt
26+
- auth/client-credentials-basic
27+
- auth/cross-app-access-complete-flow
28+

tests/Unit/Server/Handler/Request/InitializeHandlerTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ public function testHandleUsesConfigurationProtocolVersion(): void
4141
$session = $this->createMock(SessionInterface::class);
4242
$session->expects($this->exactly(2))
4343
->method('set')
44-
->willReturnCallback(function (string $key, array $value): void {
44+
->willReturnCallback(function (string $key, mixed $value): void {
4545
match ($key) {
4646
'client_info' => $this->assertSame(['name' => 'client-app', 'version' => '1.0.0'], $value),
47-
'client_capabilities' => $this->assertSame([], $value),
47+
'client_capabilities' => $this->assertEquals(new \stdClass(), $value),
4848
default => $this->fail("Unexpected session key: {$key}"),
4949
};
5050
});

0 commit comments

Comments
 (0)